Lab 09
In this lab session you will be training advanced build system concepts, such as build lifcycle phases, artifact
sideloading, and accessing third party artifact repositories.
You will be working with the XoxInternals code, which is a full working Model & Controller implementation the TicTacToe
game (paraphrased as Xox, for the X's and the O's).
To clone the sources, use: git clone https://github.com/m5c/XoxInternals.git
Maven lifecycle
Maven has three built-in lifecycles, each consisting of phases. We're mostly interested in the default
lifecycle, which allows building software from sources.
Build by calling lifecycle phases
- The maven command always expects a lifecycle phase as argument.
- For every phase provided, the entire lifecycle until that phase is executed.
- E.g.
mvn package
command will execute all default lifecycle phases until (and including) thepackage
phase.
Your turn
- You will be working with a new project
XoxFrontend
, which will call some of the functionality provided by the clonedXoxInternals
project.- However it is important that we want to connect these two projects, using maven, NOT by copy-pasting code from one project to the other.
-
For a start, initialize a new repo, called
XoxFrontend
, by using the maven archetype command:mvn archetype:generate \ -DgroupId=ca.uqam.info \ -DartifactId=XoxFrontend \ -DarchetypeArtifactId=maven-archetype-quickstart \ -DinteractiveMode=false
If you're on windows, type the command in a single line, and omit the antislashes.
-
The command will produce a new maven project of the following structure:
- Next build the project. Do so by calling the maven default lifecycle phase that only translates all java files into class files,
without producing a jar.
- Inspect the
target
folder. Verify there areclass
files, but nojar
file.
- Inspect the
- Build the project, by calling the maven default lifecycle phase that does the same as before, but also produces
a
jar
file.- Inspect the
target
folder. Verify there areclass
files, and ajar
file.
- Inspect the
Vulnerability scans
- Every project dependency is a potential liability. Whatever vulnerability is included in one of your dependencies, is potentially also a security risk for your own project.
- Luckily
pom.xml
files are an explicit form of declaring dependencies, that is, by inspecting thepom.xml
potential vulnerabilities are easily detectable. - Searching (and fixing) vulnerable dependencies known as vulnerability scans.
IntelliJs vulnerability plugin
- Intellij has by-default-activated plugin named: Package Checker
- In class, you've seen a short demo of how to use the tool.
- When a
pom.xml
file is opened, vulnerable dependencies are hihglightes with a different background - Hovering over the highlighted zones shows additional information on the vulnerability cause (and how to fix it)
- When a
Your turn
- Open your XoxFrontend project in IntelliJ.
- Double-click the
pom.xml
file. - Carefully inspect the background colour of all lines. You'll see some lines highlighted by the Package Checker plugin.
- Hover your mouse over a highlighted line. Wait a second, a popup will appear.
- Double-click the
- Carefully read the message, it will tell you how to fix the vulnerability that you currently have in your project.
- Fix the vulnerability, and make sure your project can still be built.
Sideloading
- Sideloading refers to "outsmarting" maven's dependency resolve algorithm by artificially injecting an artifact
directly into the local
.m2
respository. - This technique allows usage of dependencies that do not exist on the official online maven repository, and hence could not be automatically resolved by maven.
- The trick relies on the fact that maven will never search for an artifact online, once it has been found in the local repo.
Example:
XoxFrontend
will be needing functionality from the clonedXoxInternals
project.- In this case
XoxFrontend
can declare a<dependency>
in it'spom.xml
file: - If we sideload
XoxInternals
into the local.m2
cache, the dependency is directly:- Maven does not need to search on online server for the artifact (we injected it ourselves)
XoxFrontend
can use the library code without code duplication.
- In this case
There are different ways to sideload artifacts into the local .m2
repository cache.
Do not copy code!
Do not copy past code from the cloned XoxInternals project. The goal is to use code from XoxInternals as maven dependency, not by code duplication.
Sideloading from maven projects
- The easiest form of sideloading occurs when the dependency in question is itself a maven project.
- Luckily this is the case.
XoxInternals
has apom.xml
, and is itself a maven project. - In this case the
install
phase can be used, to not only build an artifact, but also sideload it into the local cache.
Your turn
- Add the above
dependency
block to theXoxFrontend
'spom.xml
file.- It will show up red. The dependency cannot be resolved, because no such artifact is on the official maven servers.
- Next: Inspect your local
.m2
directory. It may be hidden on your system. If it is not visible in the file manager, open a terminal and access the hidden.m2
directory on your home folder.- Inspect the content, verify there is no artifact
ca/uqam/info/xoxinternals
- Inspect the content, verify there is no artifact
- Build the previously cloned
XoxInternals
project using theinstall
phase. - Inspect your local
.m2
cache again. You should not see a new artifact inca/uqam/info/xoxinternals
. - Switch back to the
XoxFrontend
'spom.xml
, the dependency should now no longer show red, but be correctly resolved.
Next we want to see if we can actually use the code from the correctly resolved dependency:
- Add a new launcher to the XoxFrontend project that uses functionality from the XoxInternals project.
-
Use the below code, to initialize a new game instance between
For the two missing calls, you can use the below helper classes:Alice
andBob
: -
XoxGameInitiator.java
:public class XoxGameInitiator { /** * Creates a new game instance for the given player names. * * @param playerName1 name of first player. * @param playerName2 name of second player. * @return long value of a unique game ID, that can be used to interact with game instance via * manager. */ public static long createNewGame(String playerName1, String playerName2) { Player player1 = new Player(playerName1, "#0000FF"); Player player2 = new Player(playerName2, "#00FF00"); List<Player> playerList = new LinkedList<Player>(); playerList.add(player1); playerList.add(player2); XoxInitSettings xoxSettings = new XoxInitSettings(playerList, playerName1); return XoxManagerImpl.getInstance().addGame(xoxSettings); } }
XoxBoardAndActionPrinter.java
:public class XoxBoardAndActionPrinter { public static void printBoardAndActionsForPlayer(long gameId, String player) { System.out.println(XoxManagerImpl.getInstance().getBoard(gameId)); // Print all possible actions for Alice XoxClaimFieldAction[] actions = XoxManagerImpl.getInstance().getActions(gameId, player); for (int i = 0; i < actions.length; i++) { System.out.println(actions[i]); } } }
Setup the maven exec plugin in your pom.xml
and make sure you can compile and run your XoxFrontend
with:
mvn clean compile exec:java
Sideloading from generic JAR files
- Often the library you're missing is itself not provided as source code, but you have only a jar file.
- Possibly the library is not even a maven project !
- It is still possible to sideload just from a jar file, directly into the local
.m2
repository cache. - In the last course, you've seen a maven command to do just that.
Your turn
- Build the
XoxInternals
project withmvn clean package
- Copy the
jar
file from thetarget
directory to your Desktop. - Delete the
XoxInternals
project from your hard drive, but keep thejar
file. - Delete the
XoxInternals
artifact from your local.m2
repository cache.
Clear you local repo!
The above steps are important. From the previous exercise you still have the XoxInternals
artifact in your local repo.
At this point the
XoxFrontend
project will no longer work, because we've manually removed theXoxInternals
projects it depends on.`
- Just using the saved
jar
file, use the mvn command seen in class to directly install an artifact from ajar
file. - Make sure to use the correct
groupId
,artifactId
andversion
, so the dependency can be correctly resolved. - Verify your local
.m2
content and verify that you can now once more build and run theXoxFrontend
application.
Third party repositories
Third party repositories are online folders providing unofficial maven artifacts. There are several reasons for placing artifacts in third party repos, notably access restriction and the ability to revoke.
Getting a dependency from a third party repo
- Instead of sideloading the
XoxInternals
artifact manually, it is preferrably to retrieve the artifact from a non-official third-party repo. - Reminder, maven's resolving algorithm is:
- First search to local cache for a match (local
.m2
directory) - Only as fallback: search the third party repos (if defined)
- Only as last resort: searches the official maven servers.
- Sideloading targets the first step. Third party repos the second step.
In the next exercise you will be using a third party repository, instead of manually sideloading an artifact.
Your turn
- Delete the sideloaded Xox artifact from your local
~/.m2/repository/
directory.
Clear you local repo!
The above steps are important. From the previous exercise you still have the XoxInternals
artifact in your local repo. If you do not delete it, maven will stop after step 1
and never search for the artifact online.
- Try to compile your
XoxFrontend
project. It will fail, becauseXoxInternals
can no longer be resolved. - Access your
XoxFrontend
project'spom.xml
and add a reference theinf2050
repo (which is a non-officiel 3rd party maven repo, created specifically for this course): - Compile your project again with:
mvn clean test
- Carefully inspect the output.
- Find the output line that indicates the missing artifact has been downloaded from the third party server.
- Verify the build was once more successful.
Why is there suddenly again a copy of XoxInternals
in your local cache ?
Maven always caches artifacts locally, once they were successfully retrieved. Otherwise, you would never be able to work offline.
Build profiles
- Build profiles allow you to develop different version of an application, by simply specifying a profile name along the standard maven command.
- The syntax is
-P
(for profile), directly followed by the profile name.- Note there is no space between the
-P
switch and the profile name. - Example: mvn clean package
-Ppremium-software-version
- Note there is no space between the
Alternative launchers
- One use case of build profiles is the specification of alternative launchers.
- This way different functionality of a software can be started, or different hard coded parameters can be used.
Your turn
- Revise the build profile syntax presented in the last course and modify your XoxFrontend to contain two launcher classes:
- For the first launcher, you can reuse the one you coded for Alice and Bob, earlier in this lab session.
- A second launcher, to initialize a game between players
Eve
andBob
.
- Modify the
XoxFrontend
project'spom.xml
, so two new build profiles are defined:alice-and-bob
as default profile.eve-and-bob
as alternative profile.
- For each build profile, add a configuration of the
exec plugin
, make sure that:- The
alice-and-bob
profile launches the originalXoxAliceAndBob
java launcher class. - The
eve-and-bob
profile launches the newly createdXoxEveAndBob
java launcher class.
- The
- Build and launch both variants using either of:
mvn clean compile exec:java
mvn clean compile exec:java -Palice-and-bob
mvn clean compile exec:java -Peve-and-bob
- Verify all commands print an empty board of the initialized game, but only the
alice-and-bob
/default
profile additionally prints a list of player actions.
Integration tests
- In the last course lecture you've learned that the maven default lifecycle defines two different testing phases:
test
for executing unit tests.verify
for executing integration tests.
Unit tests vs Integration tests
Unit tests verify the behaviour of individual classes of methods. Integration tests verify the behaviour of component interplay, ensuring the application in its integrity serves its purpose.
Mad Max
One way to integration test game implementations, e.g. the XoxInternals
project, is to write robot players (or if you want to use hype language you can call them "AIs").
- Where unit tests verify individual functions of the game, competing robot players attempt to play the game and this way verify if it works reliably.
- The simplest way of such an integration test is the "Mad Max" robot player.
- Mad Max uses a random generator to pick a random action, whenever it is their turn.
- Two competing Mad Max instances must eventually maneuver the game into a game over state.
- The integration test is successful if the game ends without errors within 5 seconds of CPU time.
- A seeded random number generator can be used to ensure deterministic execution paths of the robot players.
- For advanced testing,
1000
game instances can be tested, each with an individual seed (e.g. simple incrementing seed value1
,2
,3
,...
)
Integration tests are also tests, and belong into the src/test/java
folder structure.
- To distinguish between standard unit tests (which must be executed at
test
phase) and integration tests (which must be executed in theverify
phase), a special file name syntax can be used. - Integration tests should end with
...IT.java
. - An additional
pom.xml
configuration for the test plugin then splits execution by file name:<project> ... <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.5.2</version> <configuration> <excludes> <exclude>**/*IT.java</exclude> </excludes> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>test</goal> </goals> <phase>integration-test</phase> <configuration> <excludes> <exclude>none</exclude> </excludes> <includes> <include>**/*IT.java</include> </includes> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
Your Turn
- Add the above
maven-surfire-plugin
to yourXoxFrontend
project. - Create a new test class
MadMaxIT.java
. - In MadMaxIT, create an
@Test
decorated function that runs 1000 different Xox games with two competing MadMax robot players. - Verify the integration tests passes through the 1000 game instances without crashing.
mvn clean test
must not run the integration tests.mvn clean verify
must run the integration tests.
You can use the below code snippets to implement the requested functionality:
/**
* Provides a random number between 0 and the provided value (exclusive).
*
* @param upperBoundExcluded as the maximum value that is guaranteed not to be reached.
*/
public int pickRandomAction(int upperBoundExcluded) {
return Math.abs(randomNumberGenerator.nextInt()) % upperBoundExcluded;
}
/**
* Initialize seeded random number generator. Each game test-run of two competing MadMax players
* should use a different seed.
*/
public void initializeRandomNumberGenerator(int seed) {
// For seed 42, will always generate:
// -1170105035 234785527 -1360544799 205897768 ...
randomNumberGenerator = new Random(seed);
}
/**
* The integration test creates 1000 test games and verifies that each of them is successfully
* finished by two competing MadMax robot players. Every game uses a different random generator
* seed.
*/
@Test
public void madMax() {
// Run 1000 random games. All must conclude in valid game ending
for (int i = 0; i < 1000; i++) {
runSeededRandomGame(i);
}
}
private void runSeededRandomGame(int seed) {
// TODO: implement missing functionality to finish seeded game
long gameId = XoxGameInitiator.createNewGame(playerNames[0], playerNames[1]);
while (!gameOver) {
// select a random move for the current player
XoxClaimFieldAction[] allPossibleActions =
gameManager.getActions(gameId, playerNames[currentPlayer]);
// pick and play the random move
gameManager.performAction(gameId, playerNames[currentPlayer], randomAction);
// advance current player and update game over status
gameOver = gameManager.getRanking(gameId).isGameOver();
}
}