I'm evaluating Elwood development with Gitflow. One thing I like about Gitflow is that it gives the ability to better track changes, releases and bug fixes. It also allows me to stay focus in writing one feature at a time. You see, there is an adrenaline rush in me that I sometimes find myself shoving unrelated changes to my timeline. I could commit one large snowball of unrelated codes that may stretch the completion time to 1.5 times longer if I'm lucky or 3 times longer if I'm not-so-lucky.
This definitely contradicts the spirit of agile with the emphasis of delivering in small increments. (Ahhh, agile that starts with a lower case... How pragmatic do I sound?)
I did ask myself if this could be too much of an overhead for a lone developer like me?
Well, time will tell...
Tuesday, July 12, 2016
Sunday, June 26, 2016
Elwood Release 1.1.0 (Aluminium+AL) Is Now Officially Released!
Please see the official release wiki page for more information:
http://www.hostedredmine.com/projects/elwood-parent/wiki/110_Release
http://www.hostedredmine.com/projects/elwood-parent/wiki/110_Release
Monday, June 20, 2016
How Do I Laugh At Thee? Let Me Count The Ways...
I hate to admit that I'm resource poor, in the sense that I can't afford to keep running my hardwares for 24x7. Neither can I afford to keep installing a new Linux image (JDK/Maven/Jenkins) every time I find a temporary shelter.
Currently, I have Jenkins/Linux running on an old machine. There were a few attempts to have it "VirtualBoxed" on my old laptop. However, since there is a sad fact about me and my laziness, all attempts ended in failure.
How do I laugh at thee? Let me count the waysI laugh at thee from morning till noon till nightMy soul rants which, when Jenkins s'out of sightFor constant installs take hours to days.
By nature, VirtualBox allows me to move the image between hardwares, but I need something much more than this. I need something that I could bring up on demand, something that I can move the data around, something that is lightweight - and Docker seems to fit the bill!
I was fortunate to have a hands-on experience with Docker at work, coincidentally, after playing around a bit with it in my spare time. At that time, I was looking for possible solutions for my quest on CI/CD.
After spending roughly 2 months, I finally ended up getting Jenkins 1.61 and Redis images running on its own subnet with data directories mounted to host. I had to build my own Redis image as the official Docker image had a few issues when persisting to files. It could be something as easy as disabling persistence which I don't need, but nonetheless, time is ticking and I have to get back to Elwood development!
After spending roughly 2 months, I finally ended up getting Jenkins 1.61 and Redis images running on its own subnet with data directories mounted to host. I had to build my own Redis image as the official Docker image had a few issues when persisting to files. It could be something as easy as disabling persistence which I don't need, but nonetheless, time is ticking and I have to get back to Elwood development!
Below is a diagram of POC:
The proof of concept is very basic, something that's good enough to keep me going and something to iron out when I come back.
The proof of concept is very basic, something that's good enough to keep me going and something to iron out when I come back.
Hopefully, when the dust settles, I'll push these images into Docker Hub.
Repository: https://github.com/lyeung/lyeung-jenkins-ci-tools
1Why am I using Jenkins as oppose to using Elwood for my build? Elwood is light years behind Jenkins and any mature products out there.
Repository: https://github.com/lyeung/lyeung-jenkins-ci-tools
1Why am I using Jenkins as oppose to using Elwood for my build? Elwood is light years behind Jenkins and any mature products out there.
Labels:
continuous delivery,
continuous integration,
docker,
elwood
Thursday, March 3, 2016
AngularJS 1.5 And PhantomJS Compatibility Issue
Last week, I've committed the relevant changes1 to issue #5086802 but Codeship3 reported a broken build. I swear I ran the UI test suite before committing and Grunt said everything was good! I decided to take some time to investigate this issue tonight to get to the very bottom of this.
Broken builds keep me awake at night.
It appears that I've been building from the recent release of AngularJS 1.5 (previously 1.4) and there seems to be some incompatibility with PhantomJS 1.x.
Interestingly, Codeship reported:
Error: [$injector:modulerr] Failed to instantiate module ng due to:
TypeError: '' is not an object (evaluating 'Function.prototype.bind.apply')
After some lucky searching, this link came up:
https://github.com/angular/angular.js/issues/13794
The suggested fix was to move up to PhantomJS2 and update the karma-phantomjs-launcher module.
As to why the test suite worked on my machine but not Codeship's is a big mystery to me...
Note: Last October, as a true believer of CI, I've got Codeship to automatically build Elwood every time I push my changes to BitBucket. Codeship has been doing a great job for the past couple of months happily doing the builds in the cloud. (Hoping CD to come soon but everything must come in baby steps :)) It's an irony that I run a local copy of Elwood and manually trigger the build on Elwood sources. Does this sound like eating my own dog food?
3https://codeship.com - a cloud based Continuous Delivery platform and it comes with free tier.
Labels:
angularjs,
codeship,
continuous integration,
elwood,
phantomjs
Friday, February 19, 2016
AngularJS Custom Directive: Display Elwood Build Result Stats
I've written my first successful custom AngularJS directive to display build result stats (see issue #508680).
The directive initially displays the build status indicating success, failure or in-progress. Adjacent to this is a clickable icon1 that goes back to the server and retrieve the build result statistics and displays them on the screen.
The journey to get here is challenging (especially for someone who works at the back office) but very fulfilling except for one last thing - unit testing.
Below is the directive code:
And below is the test code:
After compiling the element at line number 30, I'm expecting the scope variables and functions will be fully defined and by the time I invoke "scope.getBuildResultStats(scope.keyCount)" the mocked GET should return the mock data.
Well, it looks like it didn't materialise for some unknown reason.
It's almost quarter past 2 in the morning.
Changes aren't committed yet.
I feel very tired but I think I'm almost there...
Update: (2016-02-23 12:10 AM) Extracting the directive's function into a full blown controller and referring the controller with a controller name might be an option. This allows me to test the controller as an standalone component outside of this directive.
1Generated from http://www.amp-what.com and I find this website very cool.
The directive initially displays the build status indicating success, failure or in-progress. Adjacent to this is a clickable icon1 that goes back to the server and retrieve the build result statistics and displays them on the screen.
The journey to get here is challenging (especially for someone who works at the back office) but very fulfilling except for one last thing - unit testing.
Below is the directive code:
angular.module('elwoodUiApp')
.constant('BuildResultStatsUrl', 'http://localhost:8080/buildResultStats/:key/:count')
.factory('BuildResultStatsResource', function($resource, BuildResultStatsUrl) {
return $resource(BuildResultStatsUrl, {}, {
'get': {method: 'GET'}
});
})
.directive('elwBuildResultStats', function() {
var controller = function($scope, BuildResultStatsResource, ToKeyCount) {
$scope.toggle = false;
$scope.getBuildResultStats = function(keyCount) {
BuildResultStatsResource.get({'key': keyCount.key, 'count': keyCount.count},
function (successResult) {
console.log(successResult);
if (!$scope.buildResultStats) {
$scope.buildResultStats = [];
$scope.buildResultStats[ToKeyCount(keyCount)] = {
'successCount': successResult.successCount,
'failedCount': successResult.failedCount,
'ignoredCount': successResult.ignoredCount
};
}
$scope.toggle = !$scope.toggle;
}, function (errorResult) {
console.log(errorResult);
}
);
};
$scope.isShowBuildResultStats = function(keyCount) {
return $scope.toggle
&& ($scope.buildResultStats
&& ($scope.status == 'SUCCEEDED' || $scope.status === 'FAILED'));
}
};
return {
restrict: 'A',
templateUrl: 'views/buildresultstats.html',
controller: controller,
scope: {
status: '@',
keyCount: '=',
}
};
});
And below is the test code:
describe('Controller: BuildResultStatsCtrl', function () {
// load the controller's module
beforeEach(module('elwoodUiApp'));
var
scope,
httpBackend;
describe('BuildResultStatsCtrl', function() {
var BuildResultStatsCtrl,
buildResultStatsResource,
toKeyCount;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope, $compile, $httpBackend, BuildResultStatsResource, ToKeyCount) {
scope = $rootScope.$new();
httpBackend = $httpBackend;
buildResultStatsResource = BuildResultStatsResource;
toKeyCount = ToKeyCount;
var elem = '<div elw-build-result-stats="" key-count="keyCountTuple" status="{{buildStatus}}"></div>';
scope.status = 'SUCCEEDED';
scope.keyCount = {
key: 'PRJ',
count: '10'
};
httpBackend.whenGET('views/buildresultstats.html').respond(200, '');
var template = $compile(elem)(scope, buildResultStatsResource, toKeyCount);
console.log(template);
scope.$digest();
}));
it('should display when status is SUCCEEDED', function() {
var url = 'http://localhost:8080/buildResultStats/PRJ/10';
expect(scope).toBeDefined();
expect(scope.toggle).toBeFalsy();
expect(scope.isShowBuildResultStats).toBeFalsy();
expect(scope.buildResultStats).toBeUndefined();
var mockData = {
successCount: 10,
failedCount: 3,
ignoredCount: 2
};
httpBackend.expectGET(url).respond(mockData);
expect(buildResultStatsResource).toBeDefined();
scope.getBuildResultStats(scope.keyCount);
httpBackend.flush();
expect(scope.toggle).toBeTruthy();
});
});
});
After compiling the element at line number 30, I'm expecting the scope variables and functions will be fully defined and by the time I invoke "scope.getBuildResultStats(scope.keyCount)" the mocked GET should return the mock data.
Well, it looks like it didn't materialise for some unknown reason.
It's almost quarter past 2 in the morning.
Changes aren't committed yet.
I feel very tired but I think I'm almost there...
Update: (2016-02-23 12:10 AM) Extracting the directive's function into a full blown controller and referring the controller with a controller name might be an option. This allows me to test the controller as an standalone component outside of this directive.
.controller('BuildResultStatsCtrl', function($scope, BuildResultStatsResource, ToKeyCount) {
// ...
}
return { restrict: 'A', templateUrl: 'views/buildresultstats.html', controller: 'BuildResultStatsCtrl', scope: { status: '@', keyCount: '=', } };
1Generated from http://www.amp-what.com and I find this website very cool.
Saturday, February 6, 2016
Java 7 and Multi-Catch Exception
Java 7 has been out for a few years and I admit this is one of the features I seem to neglect. Perhaps, I've been living under a rock, yeah?
I've got JDK 8 installed and because of my sheer laziness, I'll stick to this version of the compiler to analyse how multi-catch exception is implemented.
Let's define a few exceptions before dipping a toe in the water.
PearlNecklaceException.java
VenetianCrossException.java
DiamondNecklaceException.java
Followed by the main class we'll look at:
Mathilde.java
All three variants of find necklaces call retrieveNecklace which throws different exceptions base on input value.
The first variant of find necklace is pretty old school. We declare different catch blocks for each exception thrown.
The second variant of find necklace, similar to the first variant, but minimises code duplication by moving the print stack trace into a common method.
The third variant uses multi-catch exception that results to a more readable and more compact code.
Under the hood, these variants produce different bytecodes.
The disassembled code for the first variant:
The exception table says, "For program counter offset 0 to 5, inclusive and exclusive respectively, the program counter should jump to target offset 8 if PearlNecklaceException is thrown, 16 if VenetianCrossException is thrown and 24 if DiamondNecklaceException is thrown."
All offsets 8, 16 and 24 invoke the caught exception's printStackTrace() method.
The disassembled code for the second variant:
The exception table is not much different from the first variant.
The disassembled code for the last variant:
Now, the exception table looks different. As you can see, the target values all point at 8, where offset 8 prints the stack trace!
The generated bytecodes are definitely smaller and more compact than the previous variants.
Below is the disassembled code for entire Mathilde class:
I've got JDK 8 installed and because of my sheer laziness, I'll stick to this version of the compiler to analyse how multi-catch exception is implemented.
She saw first some bracelets, then a pearl necklace, then a Venetian gold cross set with precious stones, of admirable workmanship. She tried on the ornaments before the mirror, hesitated and could not make up her mind to part with them, to give them back.
She kept asking: "Haven't you any more?"
Let's define a few exceptions before dipping a toe in the water.
PearlNecklaceException.java
package org.lyeung.thenecklace; public class PearlNecklaceException extends RuntimeException { // do-nothing }
VenetianCrossException.java
package org.lyeung.thenecklace; public class VenetianCrossException extends RuntimeException { // do-nothing }
DiamondNecklaceException.java
package org.lyeung.thenecklace; public class DiamondNecklaceException extends RuntimeException { // do-nothing }
Followed by the main class we'll look at:
Mathilde.java
package org.lyeung.thenecklace; public class Mathilde { private void retrieveNecklace(int value) throws PearlNecklaceException, VenetianCrossException, DiamondNecklaceException { if (value == 0) { throw new PearlNecklaceException(); } else if (value == 1) { throw new VenetianCrossException(); } else { throw new DiamondNecklaceException(); } } public void findNecklace1(int value) { try { retrieveNecklace(value); } catch (PearlNecklaceException e) { e.printStackTrace(); } catch (VenetianCrossException e) { e.printStackTrace(); } catch (DiamondNecklaceException e) { e.printStackTrace(); } } public void findNecklace2(int value) { try { retrieveNecklace(value); } catch (PearlNecklaceException e) { handleException(e); } catch (VenetianCrossException e) { handleException(e); } catch (DiamondNecklaceException e) { handleException(e); } } private void handleException(Exception e) { e.printStackTrace(); } public void findNecklace3(int value) { try { retrieveNecklace(value); } catch (VenetianCrossException | PearlNecklaceException | DiamondNecklaceException e) { e.printStackTrace(); } } }
All three variants of find necklaces call retrieveNecklace which throws different exceptions base on input value.
The first variant of find necklace is pretty old school. We declare different catch blocks for each exception thrown.
The second variant of find necklace, similar to the first variant, but minimises code duplication by moving the print stack trace into a common method.
The third variant uses multi-catch exception that results to a more readable and more compact code.
Under the hood, these variants produce different bytecodes.
The disassembled code for the first variant:
public void findNecklace1(int);
Code:
0: aload_0
1: iload_1
2: invokespecial #8 // Method retrieveNecklace:(I)V
5: goto 29
8: astore_2
9: aload_2
10: invokevirtual #9 // Method org/lyeung/thenecklace/PearlNecklaceException.printStackTrace:()V
13: goto 29
16: astore_2
17: aload_2
18: invokevirtual #10 // Method org/lyeung/thenecklace/VenetianCrossException.printStackTrace:()V
21: goto 29
24: astore_2
25: aload_2
26: invokevirtual #11 // Method org/lyeung/thenecklace/DiamondNecklaceException.printStackTrace:()V
29: return
Exception table:
from to target type
0 5 8 Class org/lyeung/thenecklace/PearlNecklaceException
0 5 16 Class org/lyeung/thenecklace/VenetianCrossException
0 5 24 Class org/lyeung/thenecklace/DiamondNecklaceException
The exception table says, "For program counter offset 0 to 5, inclusive and exclusive respectively, the program counter should jump to target offset 8 if PearlNecklaceException is thrown, 16 if VenetianCrossException is thrown and 24 if DiamondNecklaceException is thrown."
All offsets 8, 16 and 24 invoke the caught exception's printStackTrace() method.
The disassembled code for the second variant:
public void findNecklace2(int);
Code:
0: aload_0
1: iload_1
2: invokespecial #8 // Method retrieveNecklace:(I)V
5: goto 32
8: astore_2
9: aload_0
10: aload_2
11: invokespecial #12 // Method handleException:(Ljava/lang/Exception;)V
14: goto 32
17: astore_2
18: aload_0
19: aload_2
20: invokespecial #12 // Method handleException:(Ljava/lang/Exception;)V
23: goto 32
26: astore_2
27: aload_0
28: aload_2
29: invokespecial #12 // Method handleException:(Ljava/lang/Exception;)V
32: return
Exception table:
from to target type
0 5 8 Class org/lyeung/thenecklace/PearlNecklaceException
0 5 17 Class org/lyeung/thenecklace/VenetianCrossException
0 5 26 Class org/lyeung/thenecklace/DiamondNecklaceException
The exception table is not much different from the first variant.
The disassembled code for the last variant:
public void findNecklace3(int);
Code:
0: aload_0
1: iload_1
2: invokespecial #8 // Method retrieveNecklace:(I)V
5: goto 13
8: astore_2
9: aload_2
10: invokevirtual #14 // Method java/lang/RuntimeException.printStackTrace:()V
13: return
Exception table:
from to target type
0 5 8 Class org/lyeung/thenecklace/VenetianCrossException
0 5 8 Class org/lyeung/thenecklace/PearlNecklaceException
0 5 8 Class org/lyeung/thenecklace/DiamondNecklaceException
}
Now, the exception table looks different. As you can see, the target values all point at 8, where offset 8 prints the stack trace!
The generated bytecodes are definitely smaller and more compact than the previous variants.
Below is the disassembled code for entire Mathilde class:
Compiled from "Mathilde.java"
public class org.lyeung.thenecklace.Mathilde {
public org.lyeung.thenecklace.Mathilde();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
private void retrieveNecklace(int) throws org.lyeung.thenecklace.PearlNecklaceException, org.lyeung.thenecklace.VenetianCrossException, org.lyeung.thenecklace.DiamondNecklaceException;
Code:
0: iload_1
1: ifne 12
4: new #2 // class org/lyeung/thenecklace/PearlNecklaceException
7: dup
8: invokespecial #3 // Method org/lyeung/thenecklace/PearlNecklaceException."":()V
11: athrow
12: iload_1
13: iconst_1
14: if_icmpne 25
17: new #4 // class org/lyeung/thenecklace/VenetianCrossException
20: dup
21: invokespecial #5 // Method org/lyeung/thenecklace/VenetianCrossException."":()V
24: athrow
25: new #6 // class org/lyeung/thenecklace/DiamondNecklaceException
28: dup
29: invokespecial #7 // Method org/lyeung/thenecklace/DiamondNecklaceException."":()V
32: athrow
public void findNecklace1(int);
Code:
0: aload_0
1: iload_1
2: invokespecial #8 // Method retrieveNecklace:(I)V
5: goto 29
8: astore_2
9: aload_2
10: invokevirtual #9 // Method org/lyeung/thenecklace/PearlNecklaceException.printStackTrace:()V
13: goto 29
16: astore_2
17: aload_2
18: invokevirtual #10 // Method org/lyeung/thenecklace/VenetianCrossException.printStackTrace:()V
21: goto 29
24: astore_2
25: aload_2
26: invokevirtual #11 // Method org/lyeung/thenecklace/DiamondNecklaceException.printStackTrace:()V
29: return
Exception table:
from to target type
0 5 8 Class org/lyeung/thenecklace/PearlNecklaceException
0 5 16 Class org/lyeung/thenecklace/VenetianCrossException
0 5 24 Class org/lyeung/thenecklace/DiamondNecklaceException
public void findNecklace2(int);
Code:
0: aload_0
1: iload_1
2: invokespecial #8 // Method retrieveNecklace:(I)V
5: goto 32
8: astore_2
9: aload_0
10: aload_2
11: invokespecial #12 // Method handleException:(Ljava/lang/Exception;)V
14: goto 32
17: astore_2
18: aload_0
19: aload_2
20: invokespecial #12 // Method handleException:(Ljava/lang/Exception;)V
23: goto 32
26: astore_2
27: aload_0
28: aload_2
29: invokespecial #12 // Method handleException:(Ljava/lang/Exception;)V
32: return
Exception table:
from to target type
0 5 8 Class org/lyeung/thenecklace/PearlNecklaceException
0 5 17 Class org/lyeung/thenecklace/VenetianCrossException
0 5 26 Class org/lyeung/thenecklace/DiamondNecklaceException
private void handleException(java.lang.Exception);
Code:
0: aload_1
1: invokevirtual #13 // Method java/lang/Exception.printStackTrace:()V
4: return
public void findNecklace3(int);
Code:
0: aload_0
1: iload_1
2: invokespecial #8 // Method retrieveNecklace:(I)V
5: goto 13
8: astore_2
9: aload_2
10: invokevirtual #14 // Method java/lang/RuntimeException.printStackTrace:()V
13: return
Exception table:
from to target type
0 5 8 Class org/lyeung/thenecklace/VenetianCrossException
0 5 8 Class org/lyeung/thenecklace/PearlNecklaceException
0 5 8 Class org/lyeung/thenecklace/DiamondNecklaceException
}
Friday, January 22, 2016
JUnit Test Categorisation With Maven
To group by test types, we annotate our test codes with JUnit's org.junit.experimental.categories.Category and customise Maven's test plugin to allow certain groups to run before others.
But just before we start, we need to provide group names to our test codes. As an example, we'll model these group types as Felicite's early and later life.
public class TestFelicity {
As you can see from above, we've categorised our test methods by annotating @Category with the desired group types.
To enable early life tests when Maven runs package goal, we need to update the pom file to customise Surefire plugin:
To enable later life tests when Maven runs integration test or after, we need another update to Failsafe plugin:
What this means is that when Maven runs package goal, only tests with early life are invoked. When Maven runs integration test goal or after, both early life and later life tests are invoked1.
Below is a snippet log for package goal:
mvn clean package
Below is a snippet log for package verify goal:
1http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
But just before we start, we need to provide group names to our test codes. As an example, we'll model these group types as Felicite's early and later life.
package org.lyeung.simpleheart; public interface EarlyLife {}
Below is a sample test class:package org.lyeung.simpleheart; public interface LaterLife {}
public class TestFelicity {
@Category(EarlyLife.class) @Test public void isKnowsTheodore() { System.out.println("isKnowsTheodore"); assertTrue(true); } @Category(EarlyLife.class) @Test public void isKnowsPaul() { System.out.println("isKnowsPaul"); assertTrue(true); } @Category(EarlyLife.class) @Test public void isKnowsVirginie() { System.out.println("isKnowsVirginie"); assertTrue(true); } @Category(LaterLife.class) @Test public void isKnowsFatherColmiche() { System.out.println("isKnowsFatherColmiche"); assertTrue(true); } @Category(LaterLife.class) @Test public void isParrotLouLou() { System.out.println("isParrotLouLou"); assertTrue(true); } }
As you can see from above, we've categorised our test methods by annotating @Category with the desired group types.
To enable early life tests when Maven runs package goal, we need to update the pom file to customise Surefire plugin:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.18.1</version> <dependencies> <dependency> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit47</artifactId> <version>2.18.1</version> </dependency> </dependencies> <configuration> <groups>org.lyeung.simpleheart.EarlyLife</groups> </configuration>
</plugin>
To enable later life tests when Maven runs integration test or after, we need another update to Failsafe plugin:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.18.1</version> <dependencies> <dependency> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit47</artifactId> <version>2.18.1</version> </dependency> </dependencies> <configuration> <groups>org.lyeung.simpleheart.LaterLife</groups> </configuration> <executions> <execution> <id>failsafe-integration-tests</id> <phase>integration-test</phase> <goals> <goal>integration-test</goal> </goals> <configuration> <includes> <include>**/*.class</include> </includes> </configuration> </execution> </executions> </plugin>
What this means is that when Maven runs package goal, only tests with early life are invoked. When Maven runs integration test goal or after, both early life and later life tests are invoked1.
"From the threshold of the room she saw Virginia, stretched on her back, her hands joined, her mouth open, and her head thrown back under a black cross bending towards her, between motionless curtains, less white than her face."
Below is a snippet log for package goal:
mvn clean package
------------------------------------------------------- T E S T S ------------------------------------------------------- Running org.lyeung.simpleheart.TestFelicity isKnowsVirginie isKnowsPaul isKnowsTheodore Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec - in org.lyeung.simpleheart.TestFelicity Results : Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
Below is a snippet log for package verify goal:
mvn clean verify ------------------------------------------------------- T E S T S ------------------------------------------------------- Running org.lyeung.simpleheart.TestFelicity isKnowsVirginie isKnowsPaul isKnowsTheodore Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec - in org.lyeung.simpleheart.TestFelicity Results : Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 ------------------------------------------------------- T E S T S ------------------------------------------------------- Running org.lyeung.simpleheart.TestFelicity isKnowsFatherColmiche isParrotLouLou Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec - in org.lyeung.simpleheart.TestFelicity Results : Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
1http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
Subscribe to:
Posts (Atom)

