Finished libpit 1.1.0. Added change operations (priority, category, etc)

Began unit testing of libpit.
Made most of the changes to pit-cli needed to incorporate the new libpit
features.
This commit is contained in:
Jonathan Bernard 2010-02-18 11:02:20 -06:00
parent cfed10c3ed
commit 4035f366f3
33 changed files with 425 additions and 112 deletions

View File

@ -1,5 +1,5 @@
Add the ability to change an issue's status Add the ability to change an issue's category
=========================================== =============================================
The -l/--list options lists issues, add a set of options to reclassify The -l/--list options lists issues, add a set of options to reclassify
an issue from one category to another. The most common is CLOSED, but an issue from one category to another. The most common is CLOSED, but

View File

@ -1,4 +1,2 @@
Add the ability to re-prioritize a set of issues Add the ability to re-prioritize an issue.
================================================ ==========================================
Add a mode that changes the priority of a selection of issues.

View File

@ -1,2 +1,4 @@
Add the ability to enter new issues Add the ability to enter new issues
=================================== ===================================
Implemented on 2010/02/17

View File

@ -1,2 +1,4 @@
Add unit tests for changing an issue's category. Add unit tests for changing an issue's category.
================================================ ================================================
Test defined in test/com/jdbernard/pit/IssueTest.groovy

View File

@ -1,2 +1,5 @@
Add unit tests for changing an issue's priority. Add unit tests for changing an issue's priority.
================================================ ================================================
Test defined in test/com/jdbernard/pit/IssueTest.groovy

View File

@ -1,3 +1,4 @@
Add unit tests covering Issue construction. Add unit tests covering Issue construction.
=========================================== ===========================================
Test defined in test/com/jdbernard/pit/IssueTest.groovy

View File

@ -0,0 +1,2 @@
Add the ability to change an Issue's priority.
==============================================

View File

@ -0,0 +1,2 @@
Add the ability to change an Issue's category.
==============================================

View File

@ -0,0 +1,2 @@
Add the ability to create a new issue.
======================================

View File

@ -14,6 +14,12 @@
<fileset dir="${lib.dir}"> <fileset dir="${lib.dir}">
<include name="**/*.jar"/> <include name="**/*.jar"/>
</fileset> </fileset>
<pathelement path="${build.dir}/classes"/>
</path>
<path id="test.classpath">
<path refid="groovyc.classpath"/>
<pathelement path="${build.dir}/tests"/>
</path> </path>
<taskdef name="groovyc" <taskdef name="groovyc"
@ -68,7 +74,27 @@
classpathref="groovyc.classpath"/> classpathref="groovyc.classpath"/>
</target> </target>
<target name="build" depends="compile"> <target name="compile-tests" depends="init,compile">
<mkdir dir="${build.dir}/tests"/>
<groovyc
srcdir="${test.dir}"
destdir="${build.dir}/tests"
classpathref="groovyc.classpath"/>
</target>
<target name="run-tests" depends="compile-tests">
<junit fork="yes" haltonfailure="yes">
<classpath refid="test.classpath"/>
<formatter type="brief" usefile="false" />
<batchtest>
<fileset dir="${build.dir}/tests">
<include name="**/*.class"/>
</fileset>
</batchtest>
</junit>
</target>
<target name="build" depends="compile,run-tests">
<mkdir dir="${build.dir}/jar"/> <mkdir dir="${build.dir}/jar"/>
<jar <jar
destfile="${build.dir}/jar/pit-${application.version}.${build.number.final}.jar" destfile="${build.dir}/jar/pit-${application.version}.${build.number.final}.jar"

View File

@ -1,8 +1,10 @@
#Mon Feb 15 21:53:26 CST 2010 #Thu Feb 18 10:56:00 CST 2010
expected.application.version=1.1.0
build.number=2
src.dir=src
release.dir=release
build.dir=build build.dir=build
src.dir=src
lib.shared.dir=../shared-libs
test.dir=test
build.number=58
expected.application.version=1.1.0
lib.dir=lib lib.dir=lib
release.dir=release
release.jar=pit-${application.version}.jar release.jar=pit-${application.version}.jar

Binary file not shown.

View File

@ -11,4 +11,6 @@ public enum Category {
if (c.toString().startsWith(s.toUpperCase())) return c if (c.toString().startsWith(s.toUpperCase())) return c
throw new IllegalArgumentException("No category matches ${s}.") throw new IllegalArgumentException("No category matches ${s}.")
} }
public String getSymbol() { toString()[0].toLowerCase() }
} }

View File

@ -7,8 +7,11 @@ class Filter {
List<String> ids = null List<String> ids = null
int priority = 9 int priority = 9
boolean acceptProjects = true boolean acceptProjects = true
Closure projectSorter Closure projectSorter = defaultIssueSorter
Closure issueSorter Closure issueSorter = defaultProjectSorter
public static Closure defaultIssueSorter = { it.id.toInteger() }
public static Closure defaultProjectSorter = { it.name }
public boolean accept(Issue i) { public boolean accept(Issue i) {
return (i.priority <= priority && return (i.priority <= priority &&

View File

@ -26,16 +26,22 @@ public class Issue {
void setCategory(Category c) { void setCategory(Category c) {
this.category = c this.category = c
source.renameTo(getFilename()) source.renameTo(new File(source.canonicalFile.parentFile, getFilename()))
} }
void setPriority(int p) { void setPriority(int p) {
if (p < 0) priority = 0 if (p < 0) priority = 0
else if (p > 9) priority = 9 else if (p > 9) priority = 9
else priority = p else priority = p
source.renameTo(getFilename()) source.renameTo(new File(source.canonicalFile.parentFile, getFilename()))
} }
String getFilename() { return id + category.symbol + priority + ".rst"; } String getFilename() { return makeFilename(id, category, priority) }
static String makeFilename(String id, Category category, int priority) {
return id + category.symbol + priority + ".rst";
}
@Override
String toString() { return "${id}(${priority}): ${category} ${title}" }
} }

View File

@ -38,16 +38,26 @@ class Project {
} }
} }
public void rename(String newName) { source.renameTo(newName) } public void rename(String newName) {
this.name = newName
source.renameTo(new File(source.canonicalFile.parentFile, newName))
}
public void eachIssue(Closure c) { public void setName(String name) { rename(name) }
for (i in issues.values()) c.call(i)
for (p in projects.values()) p.eachIssue(c) public void eachIssue(Filter filter = null, Closure c) {
def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
for (i in issues.values().sort(sorter)) c.call(i)
}
public void eachProject(Filter filter = null, Closure c) {
def sorter = filter?.projectSorter ?: Filter.defaultProjectSorter
for (p in projects.values().sort(sorter)) c.call(p)
} }
public void each(Filter filter = null, Closure c) { public void each(Filter filter = null, Closure c) {
def is = filter?.issueSorter ?: { it.id.toInteger() } def is = filter?.issueSorter ?: Filter.defaultIssueSorter
def ps = filter?.projectSorter ?: { it.name } def ps = filter?.projectSorter ?: Filter.defaultProjectSorter
for (issue in issues.values().sort(is)) { for (issue in issues.values().sort(is)) {
if (filter && !filter.accept(issue)) if (filter && !filter.accept(issue))
@ -61,23 +71,25 @@ class Project {
return return
c.call(project) c.call(project)
project.each(c)
} }
} }
public void list(Map options = [:]) { public Issue createNewIssue(Map options) {
if (!options.offset) options.offset = "" if (!options.category) options.category = Category.TASK
if (!options.verbose) options.verbose = false if (!options.priority) options.priority = 5
if (!options.text) options.text = "Default issue title.\n" +
"====================\n"
String id = (issues.values().max { it.id.toInteger() }).id
each(options.filter) { def issueFile = new File(source, Issue.makeFilename(id, options.category, options.priority))
if (it instanceof Project) { assert !issueFile.exists()
println "\n${options.offset}${it.name}" issueFile.createNewFile()
println "${options.offset}${'-'.multiply(it.name.length())}" issueFile.write(options.text)
} else {
println "${options.offset}${it.id}(${it.priority}): " + return new Issue(issueFile)
"${it.category} ${it.title}"
if (options.verbose) println "\n${it.text}"
}
}
} }
@Override
String toString() { return name }
} }

View File

@ -0,0 +1,12 @@
package com.jdbernard.pit
import org.junit.Test
import static org.junit.Assert.assertTrue
class FilterTest {
@Test void emptyTest() {
}
}

View File

@ -1,6 +1,10 @@
package com.jdbernard.pit package com.jdbernard.pit
class IssueTest import org.junit.*
import static org.junit.Assert.assertTrue
import static org.junit.Assert.assertFalse
class IssueTest {
def issues def issues
File testDir File testDir
@ -23,43 +27,60 @@ class IssueTest
issueFile.write( issueFile.write(
"Obtain donuts.\n" + "Obtain donuts.\n" +
"==============\n\n" + "==============\n\n" +
"The office is seriously lacking in sugary donuts.\n\n "The office is seriously lacking in sugary donuts.\n\n" +
"We must rectify this at once!") "We must rectify this at once!")
issues << new Issue(issueFile) issues << new Issue(issueFile)
} }
@AfterClass void deleteIssueFiles() { @After void deleteIssueFiles() {
testDir.deleteDir() testDir.deleteDir()
} }
@Test void testSetCategory() { @Test void testSetCategory() {
assert issues[0].category == Category.FEATURE assertTrue issues[0].category == Category.FEATURE
assert issues[1].category == Category.TASK assertTrue issues[1].category == Category.TASK
issues[0].category == Category.CLOSED issues[0].category = Category.CLOSED
issues[1].category == Category.TASK issues[1].category = Category.BUG
assert issues[0].category == Category.CLOSED assertTrue issues[0].category == Category.CLOSED
assert issues[1].category == Category.BUG assertTrue issues[1].category == Category.BUG
assert new File(testDir, '0001c1.rst').exists() assertTrue new File(testDir, '0001c1.rst').exists()
assert new File(testDir, '0002b5.rst').exists() assertTrue new File(testDir, '0002b5.rst').exists()
assertFalse new File(testDir, '0001f1.rst').exists() assertFalse new File(testDir, '0001f1.rst').exists()
assertFalse new File(testDir, '0002t5.rst').exists() assertFalse new File(testDir, '0002t5.rst').exists()
} }
@Test void testIssueConstructor() { @Test void testSetPriority() {
assertTrue issues[0].priority == 1
assertTrue issues[1].priority == 5
issues[0].priority = 2
issues[1].priority = 9
assertTrue issues[0].priority == 2
assertTrue issues[1].priority == 9
assertTrue new File(testDir, '0001f2.rst').exists()
assertTrue new File(testDir, '0002t9.rst').exists()
assertFalse new File(testDir, '0001f1.rst').exists()
assertFalse new File(testDir, '0002t5.rst').exists()
}
@Test void testConstruction() {
File issueFile = new File(testDir, '0001f1.rst') File issueFile = new File(testDir, '0001f1.rst')
Issue issue = new Issue(issueFile) Issue issue = new Issue(issueFile)
assert issue.id == "0001" assertTrue issue.id == "0001"
assert issue.category == Category.FEATURE assertTrue issue.category == Category.FEATURE
assert issue.priority == 1 assertTrue issue.priority == 1
assert issue.title == "Add the killer feature to the killer app." assertTrue issue.title == "Add the killer feature to the killer app."
assert issue.text == "Add the killer feature to the killer app.\n" + assertTrue issue.text == "Add the killer feature to the killer app.\n" +
"=========================================\n\n" + "=========================================\n\n" +
"Make our killer app shine!." "Make our killer app shine!."
assert issue.source == issueFile assertTrue issue.source == issueFile
} }
} }

View File

@ -0,0 +1,115 @@
package com.jdbernard.pit
import org.junit.After
import org.junit.Before
import org.junit.Test
import static org.junit.Assert.assertTrue
import static org.junit.Assert.assertFalse
class ProjectTest {
File testDir
Project rootProj
@Before void createTestProjects() {
testDir = new File('testdir')
testDir.mkdirs()
def issueFile = new File(testDir, '0001t5.rst')
issueFile.createNewFile()
issueFile.write('Test Issue 1\n' +
'============\n\n' +
'This is the first test issue.')
issueFile = new File(testDir, '0002b5.rst')
issueFile.createNewFile()
issueFile.write('Test Bug\n' +
'========\n\n' +
'Yeah, it is a test bug.')
issueFile = new File(testDir, '0003f2.rst')
issueFile.createNewFile()
issueFile.write('Important Feature Request\n' +
'=========================\n\n' +
'Here is our sweet feature. Please implement it!')
def subDir = new File(testDir, 'subproj1')
subDir.mkdirs()
issueFile = new File(subDir, '0001f3.rst')
issueFile.createNewFile()
issueFile.write('First feature in subproject\n' +
'===========================\n\n' +
'Please make the grubblers grobble.')
issueFile = new File(subDir, '0002b4.rst')
issueFile.createNewFile()
issueFile.write('Zippners are not zippning.\n' +
'==========================\n\n' +
'For some reason, the Zippners are bilperring, not zippning.')
rootProj = new Project(testDir)
}
@After void deleteTestProjects() {
testDir.delete()
}
@Test void testConstruction() {
Project proj = new Project(testDir, null)
assertTrue proj.name == 'testdir'
assertTrue proj.issues.size() == 3
assertTrue proj.projects.size() == 1
// Issue construction in general is under test in IssueTest
// just check that the issues actually exists
assertTrue proj.issues['0001'].id == '0001'
assertTrue proj.issues['0001'].title == 'Test Issue 1'
assertTrue proj.issues['0002'].id == '0002'
assertTrue proj.issues['0002'].title == 'Test Bug'
assertTrue proj.issues['0003'].id == '0003'
assertTrue proj.issues['0003'].title == 'Important Feature Request'
// check sub-project behaviour
assertTrue proj.projects.subproj1 != null
assertTrue proj.projects.subproj1.name == 'subproj1'
assertTrue proj.projects.subproj1.issues.size() == 2
assertTrue proj.projects.subproj1.projects.size() == 0
assertTrue proj.projects.subproj1.issues['0001'].id == '0001'
assertTrue proj.projects.subproj1.issues['0001'].title == 'First feature in subproject'
assertTrue proj.projects.subproj1.issues['0002'].id == '0002'
assertTrue proj.projects.subproj1.issues['0002'].title == 'Zippners are not zippning.'
}
@Test void testRename() {
assert rootProj.name == 'testdir'
rootProj.rename('renamedTestDir')
assertTrue rootProj.name == 'renamedTestDir'
assertTrue new File('renamedTestDir').exists()
}
/*@Test void testEachIssue() {
def expectedList = [rootProj.issues['0001'],
rootProj.issues['0002'], rootProj.issues['0003']]
// sort using default ordering (ids ascending)
def actualList = []
rootProj.eachIssue { actualList << it }
assertArrayEquals expectedList, actualList
// sort using reverse ordering (ids descending)
expectedList = expectedList.reverse()
actualList = []
rootProj.eachIssue(
new Filter(issueSorter: { -(it.id.toInteger()) }))
{ actualList << it }
}*/
}

View File

@ -9,13 +9,29 @@
</fileset> </fileset>
</path> </path>
<path id="groovyc.classpath"> <path id="groovy.embeddable">
<path refid="groovy.libs"/> <fileset dir="${env.GROOVY_HOME}/embeddable">
<include name="**/*.jar"/>
</fileset>
</path>
<path id="project.libs">
<fileset dir="${lib.dir}"> <fileset dir="${lib.dir}">
<include name="**/*.jar"/> <include name="**/*.jar"/>
</fileset> </fileset>
</path> </path>
<path id="groovyc.classpath">
<path refid="groovy.libs"/>
<path refid="project.libs"/>
</path>
<path id="package.jars">
<path refid="groovy.embeddable"/>
<path refid="project.libs"/>
</path>
<taskdef name="groovyc" <taskdef name="groovyc"
classname="org.codehaus.groovy.ant.Groovyc" classname="org.codehaus.groovy.ant.Groovyc"
classpathref="groovy.libs"/> classpathref="groovy.libs"/>
@ -80,7 +96,7 @@
<target name="build" depends="compile"> <target name="build" depends="compile">
<mkdir dir="${build.dir}/jar"/> <mkdir dir="${build.dir}/jar"/>
<unjar dest="${build.dir}/classes"> <unjar dest="${build.dir}/classes">
<path refid="groovy.libs"/> <path refid="package.jars"/>
</unjar> </unjar>
<jar <jar
destfile="${build.dir}/jar/${build.jar}" destfile="${build.dir}/jar/${build.jar}"

Binary file not shown.

Binary file not shown.

View File

@ -1,9 +1,9 @@
#Sun Feb 14 02:05:14 CST 2010 #Wed Feb 17 17:38:59 CST 2010
build.dir=build build.dir=build
src.dir=src src.dir=src
build.jar=pit-cli-${application.version}.${build.number}.jar build.jar=pit-cli-${application.version}.${build.number}.jar
build.number=1 build.number=28
expected.application.version=1.0.0 expected.application.version=1.1.0
lib.dir=lib lib.dir=lib
release.dir=release release.dir=release
release.jar=pit-cli-${application.version}.jar release.jar=pit-cli-${application.version}.jar

View File

@ -0,0 +1,137 @@
package com.jdbernard.pit
import static java.lang.Math.max
import static java.lang.Math.min
def cli = new CliBuilder(usage: '')
cli.h(longOpt: 'help', 'Show help information.')
cli.v(longOpt: 'verbose', 'Show verbose task information')
cli.l(longOpt: 'list', 'List issues. Unless otherwise specified it lists all '
+ 'sub projects and all unclosed issue categories.')
cli.i(argName: 'id', longOpt: 'id', args: 1,
'Filter issues by id. Accepts a comma-delimited list.')
cli.c(argName: 'category', longOpt: 'category', args: 1,
'Filter issues by category (bug, feature, task, closed). Accepts a '
+ 'comma-delimited list.')
cli.p(argName: 'priority', longOpt: 'priority', args: 1,
'Filter issues by priority. This acts as a threshhold, listing all issues '
+ 'greater than or equal to the given priority.')
cli.r(argName: 'project', longOpt: 'project', args: 1,
'Filter issues by project (relative to the current directory). Accepts a '
+ 'comma-delimited list.')
cli.s(longOpt: 'show-subprojects',
'Include sup projects in listing (default behaviour)')
cli.S(longOpt: 'no-subprojects', 'Do not list subprojects.')
cli.P(argName: 'new-priority', longOpt: 'set-priority', args: 1,
required: false, 'Modify the priority of the selected issues.')
cli.C(argName: 'new-category', longOpt: 'set-category', args: 1,
required: false, 'Modify the category of the selected issues.')
cli.n(longOpt: 'new-issue', 'Create a new issue.')
def opts = cli.parse(args)
def issuedb = [:]
if (!opts) System.exit(1) // better solution?
if (opts.h) cli.usage()
def categories = ['bug','feature','task']
if (opts.c) categories = opts.c.split(/[,\s]/)
categories = categories.collect { Category.toCategory(it) }
// build issue list
issuedb = new Project(new File('.'),
new Filter('categories': categories,
'priority': (opts.p ? opts.p.toInteger() : 9),
'projects': (opts.r ? opts.r.toLowerCase().split(/[,\s]/).asType(List.class) : []),
'ids': (opts.i ? opts.i.split(/[,\s]/).asType(List.class) : []),
'acceptProjects': (opts.s || !opts.S)))
// list first
if (opts.l) {
def issuePrinter = { issue, offset ->
println "${offset}${issue}"
if (opts.v) {
println ""
issue.text.eachLine { println "${offset} ${it}" }
println ""
}
}
def projectPrinter
projectPrinter = { project, offset ->
println "\n${offset}${project.name}"
println "${offset}${'-'.multiply(project.name.length())}"
project.eachIssue { issuePrinter.call(it, offset) }
project.eachProject { projectPrinter.call(it, offset + " ") }
}
issuedb.eachIssue { issuePrinter.call(it, "") }
issuedb.eachProject { projectPrinter.call(it, "") }
}
// change priority second
else if (opts.P) {
def priority
try { priority = max(0, min(9, opts.P.toInteger())) }
catch (e) { println "Invalid priority: ${opts.P}"; return 1 }
walkProject(issuedb) { it.priority = priority }
}
// change category third
else if (opts.C) {
def cat
try { cat = Category.toCategory(opts.C) }
catch (e) { println "Invalid category: ${opts.C}"; return 1 }
walkProject(issuedb) { it.category = cat }
}
// new entry last
else if (opts.n) {
def cat, priority
String text = ""
Issue ussie
def sin = System.in.newReader()
while(true) {
try {
print "Category (bug, feature, task, closed): "
cat = Category.toCategory(sin.readLine())
break
} catch (e) {
println "Invalid category: " + e.getLocalizedMessage()
println "Valid options are: \n${Category.values().join(', ')}\n " +
"(abbreviations are accepted.)"
}
}
while (true) {
try {
print "Priority (0-9): "
priority = max(0, min(9, sin.readLine().toInteger()))
break
} catch (e) { println "Not a valid value." }
}
println "Enter issue (use EOF of ^D to end): "
try {
sin.eachLine { line ->
def m = line =~ /(.*)EOF.*/
if (m) {
text << m[0][1]
sin.close()
} else text << line
}
} catch (e) {}
issue = issuedb.createNewIssue(category: cat, priority: priority, text: text)
println "New issue created: "
println issue
}
def walkProject(Project p, Closure c) {
p.eachIssue(c)
p.eachProject { walkProject(it, c) }
}

View File

@ -1,51 +0,0 @@
package com.jdbernard.pit
def cli = new CliBuilder(usage: '')
cli.h(longOpt: 'help', 'Show help information.')
cli.v(longOpt: 'verbose', 'Show verbose task information')
cli.l(longOpt: 'list', 'List issues. Unless otherwise specified it lists all '
+ 'sub projects and all unclosed issue categories.')
cli.i(argName: 'id', longOpt: 'id', args: 1,
'Filter issues by id. Accepts a comma-delimited list.')
cli.c(argName: 'category', longOpt: 'category', args: 1,
'Filter issues by category (bug, feature, task, closed). Accepts a '
+ 'comma-delimited list.')
cli.p(argName: 'priority', longOpt: 'priority', args: 1,
'Filter issues by priority. This acts as a threshhold, listing all issues '
+ 'greater than or equal to the given priority.')
cli.r(argName: 'project', longOpt: 'project', args: 1,
'Filter issues by project (relative to the current directory). Accepts a '
+ 'comma-delimited list.')
cli.s(longOpt: 'show-subprojects',
'Include sup projects in listing (default behaviour)')
cli.S(longOpt: 'no-subprojects', 'Do not list subprojects.')
def opts = cli.parse(args)
def issuedb = [:]
if (!opts) System.exit(1) // better solution?
if (opts.h) cli.usage()
def categories = ['bug','feature','task']
if (opts.c) categories = opts.c.split(/[,\s]/)
categories = categories.collect { Category.toCategory(it) }
// build issue list
issuedb = new Project(new File('.'),
new Filter('categories': categories,
'priority': (opts.p ? opts.p.toInteger() : 9),
'projects': (opts.r ? opts.r.toLowerCase().split(/[,\s]/).asType(List.class) : []),
'ids': (opts.i ? opts.i.split(/[,\s]/).asType(List.class) : []),
'acceptProjects': (opts.s || !opts.S)))
// list first
if (opts.l) issuedb.list('verbose': opts.v)
// change priority second
//else if (opts.cp)
// change category third
//else if (opts.cc)
// new entry last