Aller au contenu

Atelier 09

Dans cette séance de laboratoire, vous allez vous entraîner aux concepts avancés des systèmes de construction, tels que les phases du cycle de vie de la construction, le sideloading des artefacts, et l'accès aux dépôts d'artefacts tiers.
Vous travaillerez avec le code de XoxInternals, qui est une implémentation complète du modèle et du contrôleur du jeu TicTacToe (reformulé sous le nom de Xox, pour les X et les O).
Pour cloner les sources, utilisez : git clone https://github.com/m5c/XoxInternals.git

Cycle de vie Maven

Maven possède trois cycles de vie intégrés, chacun composé de phases. Nous nous intéressons principalement au cycle de vie default, qui permet de construire des logiciels à partir des sources.

Construction en appelant des phases du cycle de vie

  • La commande maven attend toujours une phase du cycle de vie comme argument.
  • Pour chaque phase fournie, le cycle complet jusqu'à cette phase est exécuté.
  • Par exemple, la commande mvn package exécutera toutes les phases du cycle de vie default jusqu'à (et y compris) la phase package.

À vous de jouer

  • Vous allez travailler avec un nouveau projet XoxFrontend, qui appellera certaines des fonctionnalités fournies par le projet cloné XoxInternals.
    • Il est cependant important de noter que nous voulons connecter ces deux projets à l'aide de Maven, et NON en copiant-collant du code d'un projet à l'autre.
  • Pour commencer, initialisez un nouveau dépôt, appelé XoxFrontend, en utilisant la commande d'archétype Maven :
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:
    XoxFrontend/
    ├── pom.xml
    └── src
        ├── main
           └── java
               └── ca
                   └── uqam
                       └── info
                           └── App.java
        └── test
            └── java
                └── ca
                    └── uqam
                        └── info
                            └── AppTest.java
    
    12 directories, 3 files
    
  • Ensuite, construisez le projet. Faites-le en appelant la phase du cycle de vie default de Maven qui ne traduit que les fichiers Java en fichiers de classe, sans produire de fichier JAR.
    • Inspectez le dossier target. Vérifiez qu'il y a des fichiers class, mais pas de fichier jar.
  • Construisez le projet en appelant la phase du cycle de vie default de Maven qui fait la même chose que précédemment, mais produit également un fichier jar.
    • Inspectez le dossier target. Vérifiez qu'il y a des fichiers class, ainsi qu'un fichier jar.

Scans de vulnérabilité

  • Chaque dépendance d'un projet est une responsabilité potentielle. Toute vulnérabilité incluse dans l'une de vos dépendances constitue également un risque pour la sécurité de votre propre projet.
  • Heureusement, les fichiers pom.xml sont une forme explicite de déclaration des dépendances, ce qui permet de détecter facilement les vulnérabilités potentielles en inspectant le fichier pom.xml.
  • La recherche (et la correction) des dépendances vulnérables est appelée scan de vulnérabilité.

Plugin de vulnérabilité IntelliJ

  • IntelliJ dispose d'un plugin activé par défaut appelé : Package Checker
  • En classe, vous avez vu une courte démo sur l'utilisation de l'outil.
    • Lorsque le fichier pom.xml est ouvert, les dépendances vulnérables sont surlignées avec un fond différent.
    • En survolant les zones surlignées, des informations supplémentaires apparaissent sur la cause de la vulnérabilité (et sur la façon de la corriger).

À vous de jouer

  • Ouvrez votre projet XoxFrontend dans IntelliJ.
    • Double-cliquez sur le fichier pom.xml.
    • Inspectez attentivement la couleur de fond de toutes les lignes. Vous verrez certaines lignes mises en surbrillance par le plugin Package Checker.
    • Survolez une ligne surlignée. Attendez une seconde, une fenêtre contextuelle apparaîtra.
  • Lisez attentivement le message, il vous expliquera comment corriger la vulnérabilité présente dans votre projet.
  • Corrigez la vulnérabilité et assurez-vous que votre projet peut toujours être construit.

Sideloading

  • Le sideloading fait référence à "contourner" l'algorithme de résolution des dépendances de Maven en injectant artificiellement un artefact directement dans le dépôt local .m2.
  • Cette technique permet d'utiliser des dépendances qui n'existent pas dans le dépôt officiel Maven en ligne, et qui ne pourraient donc pas être résolues automatiquement par Maven.
  • L'astuce repose sur le fait que Maven ne cherchera jamais un artefact en ligne une fois qu'il a été trouvé dans le dépôt local.

Exemple :

  • XoxFrontend aura besoin de fonctionnalités provenant du projet cloné XoxInternals.
    • Dans ce cas, XoxFrontend peut déclarer une <dependency> dans son fichier pom.xml :
      <dependency>
          <groupId>ca.uqam</groupId>
          <artifactId>xoxinternals</artifactId>
          <version>1.7</version>
      </dependency>
      
    • Si nous sideloadons XoxInternals dans le cache local .m2, la dépendance sera directement :
    • Maven n'a pas besoin de chercher l'artefact sur le serveur en ligne (nous l'avons injecté nous-mêmes).
    • XoxFrontend peut utiliser le code de la bibliothèque sans duplication de code.

Il existe différentes manières d'ajouter des artefacts au cache local .m2.

Ne copiez pas le code !

Ne copiez pas de code depuis le projet cloné XoxInternals. L'objectif est d'utiliser le code de XoxInternals en tant que dépendance Maven, et non par duplication de code.

Sideloading depuis des projets Maven

  • La forme la plus simple de sideloading se produit lorsque la dépendance en question est elle-même un projet Maven.
  • Heureusement, c'est le cas. XoxInternals possède un fichier pom.xml, et c'est un projet Maven.
  • Dans ce cas, la phase install peut être utilisée non seulement pour construire un artefact, mais aussi pour l'ajouter au cache local.

À vous de jouer

  • Ajoutez le bloc dependency ci-dessus dans le fichier pom.xml de XoxFrontend.
    • Il apparaîtra en rouge. La dépendance ne peut pas être résolue, car aucun artefact de ce type n'est disponible sur les serveurs Maven officiels.
  • Ensuite : Inspectez votre répertoire local .m2. Il peut être caché sur votre système. S'il n'est pas visible dans le gestionnaire de fichiers, ouvrez un terminal et accédez au répertoire caché .m2 dans votre dossier personnel.
    • Inspectez le contenu et vérifiez qu'il n'y a pas d'artefact ca/uqam/info/xoxinternals.
  • Construisez le projet XoxInternals cloné précédemment en utilisant la phase install.
  • Inspectez à nouveau votre cache local .m2. Vous devriez maintenant voir un nouvel artefact dans ca/uqam/info/xoxinternals.
  • Revenez au fichier pom.xml de XoxFrontend, la dépendance ne devrait plus apparaître en rouge, mais être correctement résolue.

Ensuite, nous voulons vérifier si nous pouvons réellement utiliser le code de la dépendance correctement résolue :

  • Ajoutez un nouveau lanceur au projet XoxFrontend qui utilise des fonctionnalités du projet XoxInternals.
  • Utilisez le code ci-dessous pour initialiser une nouvelle instance de jeu entre Alice et Bob :
    public class XoxAliceAndBob {
      public static void main(String[] args) {
        long gameId = XoxGameInitiator.createNewGame("Alice", "Bob");
        // Prints actions for alice.
        XoxBoardAndActionPrinter.printBoardAndActionsForPlayer(gameId, "Alice");
      }
    }
    

Pour les deux appels manquants, vous pouvez utiliser les classes d'aide ci-dessous :

  • 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]);
        }
      }
    }
    

Configurez le plugin maven exec dans votre fichier pom.xml et assurez-vous de pouvoir compiler et exécuter votre XoxFrontend avec :
mvn clean compile exec:java

Sideloading depuis des fichiers JAR génériques

  • Il arrive souvent que la bibliothèque qui vous manque ne soit pas fournie sous forme de code source, mais que vous n'ayez qu'un fichier JAR.
    • Il est possible que la bibliothèque ne soit même pas un projet Maven !
  • Il est néanmoins possible de sideloader directement à partir d'un fichier JAR, directement dans le cache du dépôt local .m2.
  • Dans le dernier cours, vous avez vu une commande Maven pour faire cela.

À vous de jouer

  1. Construisez le projet XoxInternals avec mvn clean package.
  2. Copiez le fichier jar depuis le répertoire target vers votre bureau.
  3. Supprimez le projet XoxInternals de votre disque dur, mais conservez le fichier jar.
  4. Supprimez l'artefact XoxInternals de votre cache local .m2.

Videz votre dépôt local !

Les étapes ci-dessus sont importantes. À partir de l'exercice précédent, vous avez encore l'artefact XoxInternals dans votre dépôt local.

À ce stade, le projet XoxFrontend ne fonctionnera plus, car nous avons supprimé manuellement les dépendances du projet XoxInternals.

  1. En utilisant uniquement le fichier jar sauvegardé, utilisez la commande Maven vue en classe pour installer directement un artefact depuis un fichier jar.

  2. Assurez-vous d'utiliser le bon groupId, artifactId et version, afin que la dépendance puisse être correctement résolue.

  3. Vérifiez le contenu de votre répertoire .m2 local et assurez-vous que vous pouvez désormais reconstruire et exécuter l'application XoxFrontend.

Dépôts tiers

Les dépôts tiers sont des dossiers en ligne fournissant des artefacts Maven non officiels. Il existe plusieurs raisons de placer des artefacts dans des dépôts tiers, notamment les restrictions d'accès et la possibilité de révoquer l'accès.

Obtenir une dépendance depuis un dépôt tiers

  • Au lieu de sideloader manuellement l'artefact XoxInternals, il est préférable de récupérer l'artefact depuis un dépôt tiers non officiel.
  • Rappel : l'algorithme de résolution de Maven est :
    1. D'abord, chercher dans le cache local pour une correspondance (répertoire local .m2).
    2. En dernier recours : chercher dans les dépôts tiers (s'ils sont définis).
    3. En dernier recours : chercher sur les serveurs Maven officiels.
  • Le sideloading cible la première étape. Les dépôts tiers ciblent la deuxième étape.

Dans l'exercice suivant, vous allez utiliser un dépôt tiers, au lieu de sideloader manuellement un artefact.

À vous de jouer

  • Supprimez l'artefact Xox sideloadé de votre répertoire local ~/.m2/repository/.

Videz votre dépôt local !

Les étapes ci-dessus sont importantes. À partir de l'exercice précédent, vous avez encore l'artefact XoxInternals dans votre dépôt local. Si vous ne le supprimez pas, Maven s'arrêtera à l'étape 1 et ne cherchera jamais l'artefact en ligne.

  • Essayez de compiler votre projet XoxFrontend. Cela échouera, car XoxInternals ne pourra plus être résolu.
  • Accédez au fichier pom.xml de votre projet XoxFrontend et ajoutez une référence au dépôt inf2050 (qui est un dépôt Maven non officiel créé spécifiquement pour ce cours) :
    <repositories>
        <repository>
            <id>Max's third party repo on a UQAM GitLab file server</id>
            <url>https://max.pages.info.uqam.ca/inf2050repo/</url>
        </repository>
    </repositories>
    
  • Compilez à nouveau votre projet avec : mvn clean test.
  • Inspectez attentivement la sortie.
    • Trouvez la ligne de sortie indiquant que l'artefact manquant a été téléchargé depuis le serveur tiers.
    • Vérifiez que la construction a à nouveau réussi.
Pourquoi y a-t-il soudainement à nouveau une copie de XoxInternals dans votre cache local ?

Maven met toujours en cache les artefacts localement, une fois qu'ils ont été récupérés avec succès. Sinon, vous ne pourriez jamais travailler hors ligne.

Profils de construction

  • Les profils de construction permettent de développer différentes versions d'une application, en spécifiant simplement un nom de profil avec la commande Maven standard.
  • La syntaxe est -P (pour profile), directement suivie du nom du profil.
    • Notez qu'il n'y a pas d'espace entre le switch -P et le nom du profil.
    • Exemple : mvn clean package -Ppremium-software-version

Lanceurs alternatifs

  • Un cas d'utilisation des profils de construction est la spécification de lanceurs alternatifs.
  • De cette façon, différentes fonctionnalités d'un logiciel peuvent être lancées, ou différents paramètres codés en dur peuvent être utilisés.

À vous de jouer

  • Revisez la syntaxe des profils de construction présentée lors du dernier cours et modifiez votre projet XoxFrontend pour contenir deux classes de lancement :
    • Pour le premier lanceur, vous pouvez réutiliser celui que vous avez codé pour Alice et Bob, plus tôt dans cette session de laboratoire.
    • Un deuxième lanceur, pour initialiser une partie entre les joueurs Eve et Bob.
      public class XoxEveAndBob {
        public static void main(String[] args) {
          long gameId = XoxGameInitiator.createNewGame("Eve", "Bob");
          // prints nothing! Alice is not participating in match.
          XoxBoardAndActionPrinter.printBoardAndActionsForPlayer(gameId, "Alice");
        }
      }
      
  • Modifiez le fichier pom.xml du projet XoxFrontend afin que deux nouveaux profils de construction soient définis :
    • alice-and-bob comme profil par défaut.
    • eve-and-bob comme profil alternatif.
  • Pour chaque profil de construction, ajoutez une configuration du plugin exec, en vous assurant que :
    • Le profil alice-and-bob lance la classe Java originale XoxAliceAndBob.
    • Le profil eve-and-bob lance la nouvelle classe Java XoxEveAndBob créée.
  • Construisez et lancez les deux variantes en utilisant l'une des commandes suivantes :
    • mvn clean compile exec:java
    • mvn clean compile exec:java -Palice-and-bob
    • mvn clean compile exec:java -Peve-and-bob
  • Vérifiez que toutes les commandes affichent un tableau vide du jeu initialisé, mais que seul le profil alice-and-bob / par défaut affiche également une liste des actions des joueurs.

Tests d'intégration

  • Lors du dernier cours, vous avez appris que le cycle de vie par défaut de Maven définit deux phases de tests différentes :
    • test pour l'exécution des tests unitaires.
    • verify pour l'exécution des tests d'intégration.

Tests unitaires vs Tests d'intégration

Les tests unitaires vérifient le comportement des classes ou des méthodes individuelles. Les tests d'intégration vérifient le comportement de l'interaction entre les composants, s'assurant que l'application dans son ensemble remplit correctement son objectif.

Mad Max

Une façon de tester l'intégration des implémentations de jeu, par exemple le projet XoxInternals, est d'écrire des joueurs robots (ou, si vous voulez utiliser un langage à la mode, des "IA").

  • Alors que les tests unitaires vérifient les fonctions individuelles du jeu, les joueurs robots tentent de jouer au jeu et vérifient ainsi s'il fonctionne de manière fiable.
  • La façon la plus simple de tester cela est avec le joueur robot "Mad Max".
    • Mad Max utilise un générateur aléatoire pour choisir une action au hasard, chaque fois que c'est à son tour de jouer.
    • Deux instances concurrentes de Mad Max doivent finir par manœuvrer le jeu jusqu'à un état de fin de partie.
    • Le test d'intégration est réussi si la partie se termine sans erreur dans les 5 secondes du processeur.
  • Un générateur de nombres aléatoires avec une graine fixée peut être utilisé pour garantir des chemins d'exécution déterministes des joueurs robots.
  • Pour des tests avancés, 1000 instances de jeu peuvent être testées, chacune avec une graine individuelle (par exemple, une valeur de graine incrémentée comme 1, 2, 3, ...).

Les tests d'intégration sont également des tests, et doivent donc être placés dans la structure de dossiers src/test/java.

  • Pour distinguer les tests unitaires standards (qui doivent être exécutés lors de la phase test) des tests d'intégration (qui doivent être exécutés lors de la phase verify), une syntaxe de nom de fichier spéciale peut être utilisée.
  • Les tests d'intégration doivent se terminer par ...IT.java.
  • Une configuration supplémentaire dans le fichier pom.xml pour le plugin de test permet de séparer l'exécution en fonction du nom du fichier :
    <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>
    

À vous de jouer

  1. Ajoutez le plugin maven-surefire-plugin ci-dessus à votre projet XoxFrontend.
  2. Créez une nouvelle classe de test MadMaxIT.java.
  3. Dans MadMaxIT, créez une fonction décorée avec @Test qui exécute 1000 parties différentes de Xox avec deux joueurs robots MadMax en compétition.
  4. Vérifiez que les tests d'intégration passent à travers les 1000 instances de jeu sans planter.

  5. mvn clean test ne doit pas exécuter les tests d'intégration.

  6. mvn clean verify doit exécuter les tests d'intégration.

Vous pouvez utiliser les morceaux de code ci-dessous pour implémenter la fonctionnalité demandée :

  /**
 * 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();
  }
}