Mode serveur

Le mode serveur MTP permet à un éditeur, un IDE, une interface de test personnalisée ou un autre outil de test de lancer une application de test et de piloter la découverte et l’exécution via JSON-RPC. Utilisez le mode serveur lorsque votre outil a besoin de métadonnées de test, d’exécution de tests sélectionnés, de mises à jour de progression ou d’artefacts de résultat sans analyser la sortie du terminal. Pour les exécutions de ligne de commande et ci normales, exécutez l’application de test directement ou utilisez dotnet test.

Pour une découverte ponctuelle sans connexion persistante, utilisez --list-tests json. Pour un client VSTest existant, consultez la découverte et l’exécution des tests par programme.

Démarrez le serveur JSON-RPC public avec --server ou --server jsonrpc.

Important

Le sdk .NET utilise un protocole binaire interne distinct via --server dotnettestcli. Ne transmettez pas cette valeur vous-même. dotnet test configure les arguments de transport et de protocole.

Utiliser le client source uniquement

MTP 2.4 fournit un client de JSON-RPC canonique. Ajoutez le package Microsoft.Testing.Platform.ServerMode.Client.Sources. Le package injecte la source C# dans votre projet au lieu d’ajouter un assembly runtime.

La conception basée uniquement sur le code source comprend :

  • Aucune DLL d’exécution ni dépendance de déploiement supplémentaire.
  • Sérialisation native, compatible AOT et sans réflexion.
  • Types de protocole et code de sérialisation à partir du même référentiel que le serveur MTP.

Les types injectés MtpServerClient, IMtpServerClient et de protocole peuvent initialiser une connexion, découvrir des tests, exécuter des tests, demander l’arrêt du serveur et signaler des mises à jour des nœuds de test. Tous les types injectés restent internal pour l’assembly qui les consomme. Le client gère le lancement du processus, le transport, l’encadrement des messages et la sérialisation ; vous n’avez pas besoin d’implémenter JSON-RPC vous-même.

Le package compile la source C#. Pour utiliser le client depuis Visual Basic ou un autre langage .NET, ajoutez une référence à une bibliothèque de classes C# qui encapsule les types injectés derrière votre propre API publique, comme dans l’exemple suivant.

Découvrir et exécuter des tests à partir de .NET

Cet exemple utilise le package client source uniquement version 2.5.2 et cible .NET 10. Créez d’abord une application de test compatible MTP, puis passez au client le chemin d’accès à son binaire compilé .dll ou à son exécutable.

L’utilitaire C# partagé détecte les tests, affiche les métadonnées sérialisables de chaque nœud et exécute les tests détectés par UID. Pour exécuter un sous-ensemble, sélectionnez les UID requis dans les mises à jour de découverte et transmettez uniquement ces UID à RunTestsAsync. N’utilisez pas de noms d’affichage comme identités de test.

using System.Text.Json;
using Microsoft.Testing.Platform.ServerMode.Client;

namespace TestClientSample;

public static class TestClient
{
    public static async Task<int> DiscoverAndRunAsync(
        string testApplicationPath,
        CancellationToken cancellationToken = default)
    {
        string path = Path.GetFullPath(testApplicationPath);
        var testUids = new HashSet<string>(StringComparer.Ordinal);

        using (IMtpServerClient client = await MtpServerClient.LaunchAsync(
            path, cancellationToken: cancellationToken))
        {
            try
            {
                MtpServerCapabilities capabilities =
                    await client.InitializeAsync(cancellationToken);
                if (capabilities.MultiConnectionProvider)
                {
                    throw new NotSupportedException("A passive controller connection can't discover or run tests.");
                }

                if (!capabilities.SupportsDiscovery)
                {
                    throw new NotSupportedException("The test application doesn't support discovery.");
                }

                client.LogReceived += (_, log) => Console.Error.WriteLine(log.Message);
                client.TestNodesUpdated += (_, update) =>
                {
                    foreach (MtpTestNodeUpdate change in update.Changes)
                    {
                        Console.WriteLine(JsonSerializer.Serialize(change.Node));
                        if (change.NodeType != "group"
                            && change.ExecutionState == "discovered"
                            && change.Uid is string uid)
                        {
                            testUids.Add(uid);
                        }
                    }
                };

                await client.DiscoverTestsAsync(cancellationToken);
                await client.ExitAsync(cancellationToken);
            }
            finally
            {
                await client.ShutdownAsync();
            }
        }

        if (testUids.Count == 0)
        {
            Console.Error.WriteLine("No tests were discovered.");
            return 8;
        }

        var completedUids = new HashSet<string>(StringComparer.Ordinal);
        bool hasUnsuccessfulResult = false;
        using IMtpServerClient runClient = await MtpServerClient.LaunchAsync(
            path, cancellationToken: cancellationToken);
        try
        {
            MtpServerCapabilities capabilities =
                await runClient.InitializeAsync(cancellationToken);
            if (capabilities.MultiConnectionProvider)
            {
                throw new NotSupportedException("A passive controller connection can't discover or run tests.");
            }

            runClient.LogReceived += (_, log) => Console.Error.WriteLine(log.Message);
            runClient.TestNodesUpdated += (_, update) =>
            {
                foreach (MtpTestNodeUpdate change in update.Changes)
                {
                    Console.WriteLine(JsonSerializer.Serialize(change.Node));
                    if (change.Uid is string uid
                        && testUids.Contains(uid)
                        && change.ExecutionState is string state
                        && state is not ("discovered" or "in-progress")
                        && (!change.Node.TryGetValue("retry.is-superseded", out object? superseded)
                            || superseded is not true))
                    {
                        completedUids.Add(uid);
                        hasUnsuccessfulResult |= state is not ("passed" or "skipped");
                    }
                }
            };

            MtpRunResult result = await runClient.RunTestsAsync(testUids, cancellationToken);
            foreach (MtpAttachment artifact in result.Artifacts)
            {
                Console.WriteLine($"Artifact: {artifact.Uri}");
            }

            await runClient.ExitAsync(cancellationToken);

            if (hasUnsuccessfulResult || !testUids.All(completedUids.Contains))
            {
                Console.Error.WriteLine("A selected test failed or didn't report a final result.");
                return 2;
            }

            return 0;
        }
        finally
        {
            await runClient.ShutdownAsync();
        }
    }
}

L’assistant démarre un processus de test distinct pour la découverte et l’exécution, de sorte qu’il ne nécessite pas MtpServerCapabilities.MultiRequestSupport. Pour réutiliser un processus, vérifiez cette capacité InitializeAsync avant d’émettre plusieurs demandes. Vérifiez SupportsDiscovery également avant de demander discovery.

Important

Les helpers de lancement du client source uniquement gèrent une connexion JSON-RPC. Dans la version 2.5.2, ils n’acceptent ni n’acheminent les connexions supplémentaires utilisées par les extensions adossées au contrôleur. Par exemple, la création de rapports TRX sur Windows peut démarrer un contrôleur dont la connexion passive arrive avant l’hôte de test actif. Une connexion avec MultiConnectionProvider défini sur true n’achemine que des mises à jour et des artefacts ; n’envoie pas de requêtes de découverte ou d’exécution à celui-ci. L’exemple rejette cette liaison.

Pour les rapports adossés à un contrôleur, exécutez l’application de test via l’interface CLI ou dotnet test, ou utilisez un client d’outil qui gère les connexions supplémentaires du protocole. Un client à connexion unique peut toujours recevoir des artefacts que l’hôte de test actif publie.

Les projets console C# et Visual Basic référencent tous deux le projet d’assistance C# :

using TestClientSample;

if (args.Length != 1)
{
    Console.Error.WriteLine("Usage: DiscoverAndRun <test-application-path>");
    return 1;
}

return await TestClient.DiscoverAndRunAsync(args[0]);

Exécutez l’un ou l’autre projet de console avec le chemin d’accès de l’application de test en tant qu’argument :

dotnet run --project <client-project> -- <test-application-path>

L’exemple retourne 0 quand chaque UID sélectionné a un résultat final et que chaque résultat final non remplacé indique passed ou skipped, 2 pour les résultats finaux non réussis ou manquants, et 8 lorsque la découverte ne retourne aucun test. Ces décisions appartiennent au client exemple. Une tâche achevée RunTestsAsync signifie que la requête du protocole a abouti, et non que les tests ont réussi. MtpRunResult.Artifacts contient des artefacts d’exécution, et non un verdict de réussite/échec. Utilisez les test-node updates pour déterminer les résultats des tests.

Plusieurs résultats peuvent partager un UID. L’exemple ignore une tentative de réessai antérieure uniquement lorsque retry.is-superseded est true ; l’égalité UID seule n’établit pas de nouvelle tentative. Un résultat réussi ou ignoré ultérieur ne peut pas effacer un échec antérieur non remplacé.

Consommer des mises à jour et des métadonnées

Abonnez-vous à TestNodesUpdated avant la découverte ou l’exécution. Lisez Changes à partir de chaque mise à jour et identifiez les nœuds par Uid. Le client fournit des propriétés pratiques telles que DisplayName, ExecutionState, ErrorMessage et DurationInMilliseconds. Lisez le Node dictionnaire pour d’autres champs sérialisés, y compris traits et des propriétés spécifiques au framework.

MSTest publie des catégories de test et des valeurs personnalisées TestProperty en tant que traits. La disponibilité des métadonnées dépend du framework de test et de ce qu’il publie pendant la phase de découverte. Les attributs arbitraires et les implémentations en mémoire IProperty ne traversent pas automatiquement la frontière entre processus. Pour publier des métadonnées clé-valeur portables, utilisez le type public TestMetadataProperty . Les couches de plateforme et de compatibilité peuvent fournir d’autres champs sérialisables ; leurs types de propriétés internes ne sont pas des API d’extension. Pour plus d’informations, consultez les propriétés de nœud de test.

Ne vous attendez pas à ce que le dictionnaire des nœuds JSON-RPC corresponde au document --list-tests json. Les transports utilisent différentes dispositions de champ. En outre, ne supposez pas que chaque framework émet les champs spécifiques à VSTest vstest.*.

Le client traite les mises à jour dans l’ordre et appelle les gestionnaires correspondants avant que la découverte ou la tâche d’exécution ne se termine. Vous n’avez pas besoin d’attendre la mise à jour finale. Les gestionnaires s’exécutent de manière synchrone sur la boucle de lecture du client. Ne les bloquez pas et n’attendez pas de manière synchrone une autre opération cliente. Distribuez les opérations coûteuses ou les mises à jour de l’interface utilisateur vers le thread approprié.

Transmettez un jeton d’annulation aux méthodes de découverte ou d’exécution pour demander l’annulation via le protocole. Si l’infrastructure de test prend en charge les filtres d’arborescence, utilisez DiscoverTestsWithFilterAsync ou RunTestsWithFilterAsync avec une expression de filtre d’arborescence MTP pour sélectionner des tests sans créer d’abord une collection UID. Ces méthodes n’acceptent pas les expressions VSTest --filter .

Répondre aux exigences du client

Utilisez C# 12 ou version ultérieure. Le package logiciel respecte le paramètre de version du langage de votre projet. Si votre framework cible est défini par défaut sur une version C# antérieure, définissez LangVersion sur 12.0 ou une version ultérieure. Le package fournit également des types de compatibilité internes pour .NET Framework 4.6.2 et netstandard2.0.

Supprimez toute copie écrite manuellement du client MTP avant d’ajouter le paquet afin d’éviter les doublons de types internes.

Choisir un modèle de lancement

Pour démarrer l’application de test en tant que processus externe, utilisez MtpServerClient.LaunchAsync. Le client est responsable de ce processus enfant et le termine pendant la phase de nettoyage, si nécessaire.

Pour configurer une application de test externe pour un environnement particulier, définissez la valeur de MtpServerClientOptions.EnvironmentVariables avant le lancement. Le processus enfant hérite de l’environnement du parent, avec les valeurs fournies appliquées à l’enfant. Ces options ne modifient pas l’environnement d’un hôte in-process.

Chaque client contrôle une application de test. Pour plusieurs applications de test, créez un client pour chaque application et agrègez leurs résultats dans votre outil.

Pour un hôte incorporé ou un environnement de thread d’interface utilisateur qui ne peut pas utiliser Process.Start, utilisez MtpServerClient.LaunchInProcessAsync. La méthode exécute l’application de test via un rappel asynchrone dans le processus actuel. Ne bloquez pas le thread de lancement, car le rappel et le client partagent le processus.

Les deux modèles au lancement utilisent un transport TCP en boucle locale. WebAssembly dans le navigateur ne peut pas créer le listener requis, donc le lancement dans le processus génère PlatformNotSupportedException. Le lancement in-process ne fournit pas de transport WebAssembly alternatif.

Arrêter le client

Pour arrêter le client et l’application lancée sans bloquer le thread appelant, appelez IMtpServerClient.ShutdownAsync(). La méthode asynchrone évite la réactivité des threads d’interface utilisateur et les problèmes de blocage dans les hôtes incorporés.

Pour un hôte dans le processus, MtpServerClientOptions.ServerShutdownTimeout contrôle le temps d’attente de la phase de démontage une fois que le client a fermé le transport. La valeur par défaut est 30 secondes. Lisez ServerExitCode après l’arrêt pour obtenir le code de retour du callback. La valeur reste null pendant l’exécution de l’application ou lorsque l’application échoue au lieu de retourner un code de sortie.

Déclarer le caractère avec état du client

À compter de MTP 2.4.1, définissez l’option nullable MtpServerClientOptions.IsStateful pour décrire la façon dont le client consomme les mises à jour de nœud de test :

Value Déclaration
true Le client conserve un ensemble adressable de nœuds de test pour la session et conserve l’état connu de chaque nœud jusqu’à ce qu’une autre mise à jour le modifie, comme pourrait le faire un explorateur de tests d’IDE.
false Le client consomme des mises à jour sans conserver cet état de nœud de test.
null (valeur par défaut) Le client ne déclare pas la capacité. La négociation d’initialisation omet le champ.

Le client envoie une valeur booléenne explicite via la capacité du protocole expérimental capabilities.testing.isStateful. Le maintien de l’état est indépendant de la durée de vie de la connexion et de la capacité de MultiRequestSupport du serveur. Un client n’acquiert pas d’état simplement parce qu’il envoie plusieurs requêtes sur la même connexion.

MTP 2.4.0 utilise une option booléenne non nullable avec une valeur par défaut false. Pour conserver une fonctionnalité non déclarée, utilisez MTP 2.4.1 ou version ultérieure.

À l’intérieur d’un framework ou d’une extension, utilisez le service expérimental IClientInfoservice pour inspecter la déclaration. Lisez clientInfo.Capabilities.IsStateful pour un résultat booléen conservateur, c’est-à-dire false lorsque la capacité n’est pas déclarée. Dans MTP 2.4.1 ou version ultérieure, ClientCapabilitiesExtensions.GetIsStateful() conserve la distinction entre les clients non déclarés, explicitement sans état et avec état.

Négocier la compatibilité des protocoles

À compter de MTP 2.4, la négociation de protocole en mode serveur est indépendante de la version du package NuGet MTP. Un client déclare les versions de protocole qu’il prend en charge, et le serveur sélectionne une version commune. Lorsqu’aucune version prise en charge commune n’existe, l’application de test s’arrête avec le code de sortie ProtocolError (12) au lieu de continuer avec un contrat de communication incompatible.

Utilisez le schéma JSON server-mode 1.0 versionné pour valider les messages. Les sérialiseurs System.Text.Json et Jsonite suivent le même schéma et l’enveloppe d’erreur.

Voir aussi