Modalità server

La modalità server MTP consente a un editor, un IDE, un'interfaccia di test personalizzata o a un altro strumento di test di avviare un'applicazione di test e di gestire il rilevamento e l'esecuzione tramite JSON-RPC. Usare la modalità server quando lo strumento necessita di metadati di test, esecuzione di test selezionati, aggiornamenti dello stato o artefatti dei risultati senza analizzare l'output del terminale. Per le normali esecuzioni da riga di comando e CI, eseguire direttamente l'applicazione di test oppure usare dotnet test.

Per l'individuazione con un'unica operazione senza una connessione permanente, usare --list-tests json. Per un client VSTest esistente, vedere Individuazione ed esecuzione di test a livello di codice.

Avviare il server JSON-RPC pubblico con --server o --server jsonrpc.

Importante

L'SDK di .NET usa un protocollo binario interno separato tramite --server dotnettestcli. Non passare questo valore manualmente. dotnet test configura gli argomenti del trasporto e del protocollo.

Usare il client di sola origine

MTP 2.4 fornisce un client JSON-RPC canonico. Aggiungere il pacchetto Microsoft.Testing.Platform.ServerMode.Client.Sources. Il pacchetto inserisce il codice sorgente C# nel tuo progetto anziché aggiungere un assembly di runtime.

La progettazione basata esclusivamente sulla sorgente fornisce:

  • Nessuna DLL di runtime o dipendenza di distribuzione aggiuntiva.
  • Serializzazione nativa compatibile con AOT e priva di reflection.
  • Tipi di protocollo e codice di serializzazione dallo stesso repository del server MTP.

I tipi di protocollo inseriti MtpServerClient, IMtpServerCliente possono inizializzare una connessione, individuare test, eseguire test, richiedere l'uscita del server e segnalare gli aggiornamenti del nodo di test. Tutti i tipi inseriti rimangono internal nell'assembly che utilizza. Il client gestisce l'avvio del processo, il trasporto, la strutturazione dei messaggi e la serializzazione; non è necessario implementare JSON-RPC manualmente.

Il pacchetto compila il codice sorgente C#. Per usare il client da Visual Basic o da un altro linguaggio .NET, fare riferimento a una libreria di classi C# che espone la propria API pubblica intorno ai tipi iniettati, come nell'esempio seguente.

Individua ed esegui test da .NET

Questo esempio usa il pacchetto client source-only versione 2.5.2 ed è destinato a .NET 10. Compilare prima un'applicazione di test abilitata per MTP, quindi passare al client il percorso del relativo file compilato .dll o eseguibile.

L'helper C# condiviso individua i test, stampa i metadati serializzabili di ogni nodo ed esegue i test individuati tramite UID. Per eseguire un sottoinsieme, selezionare gli UID necessari dagli aggiornamenti di rilevamento e passare a RunTestsAsync solo tali UID. Non usare i nomi visualizzati come identificativi di 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'helper avvia un processo di test separato per l'individuazione e l'esecuzione, quindi non richiede MtpServerCapabilities.MultiRequestSupport. Per riutilizzare un processo, verificare tale funzionalità dopo InitializeAsync prima di inviare più richieste. Controllare SupportsDiscovery anche prima di richiedere il rilevamento.

Importante

I componenti di avvio del client solo sorgente gestiscono una connessione JSON-RPC. Nella versione 2.5.2 non accettano e instradano le connessioni aggiuntive usate dalle estensioni basate sul controller. Ad esempio, la generazione di report TRX in Windows può avviare un controller la cui connessione passiva arriva prima dell'host di test attivo. Una connessione con MultiConnectionProvider impostato su true esegue solo il push degli aggiornamenti e degli artefatti; non inviare a essa le richieste di rilevamento o di esecuzione. Il codice di esempio rifiuta tale connessione.

Per i report basati su controller, eseguire l'applicazione di test tramite l'interfaccia della riga di comando o dotnet testo usare un client di strumenti che gestisce le connessioni aggiuntive del protocollo. Un client a connessione singola può comunque ricevere artefatti pubblicati dall'host di test attivo.

I progetti console C# e Visual Basic fanno entrambi riferimento al progetto helper C#:

using TestClientSample;

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

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

Eseguire uno dei due progetti console con il percorso dell'applicazione di test come argomento:

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

L'esempio restituisce 0 quando ogni UID selezionato ha un risultato finale e ogni risultato finale non sostituito riporta passed o skipped, 2 per risultati finali non riusciti o mancanti e 8 quando l'individuazione non restituisce alcun test. Queste decisioni appartengono al cliente di esempio. Un'attività completata RunTestsAsync indica che la richiesta di protocollo è stata completata, non che i test siano stati superati. MtpRunResult.Artifacts contiene artefatti dell'esecuzione, non un verdetto di superamento/esito negativo. Usare gli aggiornamenti del nodo di test per determinare i risultati dei test.

Più risultati possono condividere un UID. Nell'esempio viene ignorato un precedente nuovo tentativo solo quando retry.is-superseded è true; l'uguaglianza UID da sola non determina un nuovo tentativo. Un risultato passato o ignorato in un secondo momento non può cancellare un precedente esito negativo non sostituito.

Elaborare gli aggiornamenti e i metadati

Sottoscrivi a TestNodesUpdated prima dell'individuazione o dell'esecuzione. Leggere Changes da ogni aggiornamento e identificare i nodi in base a Uid. Il client fornisce proprietà di utilità come DisplayName, ExecutionState, ErrorMessage e DurationInMilliseconds. Leggere il Node dizionario per altri campi serializzati, incluse traits e le proprietà specifiche del framework.

MSTest pubblica categorie di test e valori personalizzati TestProperty come tratti. La disponibilità dei metadati dipende dal framework di test e da ciò che pubblica durante l'individuazione. Gli attributi arbitrari e le implementazioni IProperty in memoria non attraversano automaticamente il confine tra processi. Per pubblicare i metadati portabile chiave-valore, usare il tipo pubblico TestMetadataProperty . La piattaforma e i layer di compatibilità possono fornire altri campi serializzabili; i tipi di proprietà interni non sono API di estensione. Per informazioni dettagliate, vedere Proprietà del nodo di test.

Non aspettarsi che il dizionario dei nodi JSON-RPC corrisponda al --list-tests json documento. I transport usano layout di campo diversi. Inoltre, non dare per scontato che ogni framework emetta i campi specifici vstest.* di VSTest.

Il client elabora gli aggiornamenti in ordine e richiama i gestori corrispondenti prima del completamento dell'individuazione o dell'attività di esecuzione. Non è necessario attendere l'aggiornamento finale. Gli handler vengono eseguiti in modo sincrono all'interno del ciclo di lettura del client, quindi non bloccarli e non attendere in modo sincrono il completamento di un'altra operazione del client. Inviare aggiornamenti costosi del lavoro o dell'interfaccia utente al thread appropriato.

Passare un token di annullamento all'individuazione o eseguire metodi per richiedere l'annullamento tramite il protocollo. Se il framework di test supporta filtri ad albero, usare DiscoverTestsWithFilterAsync o RunTestsWithFilterAsync con un'espressione di filtro ad albero MTP per selezionare i test senza prima creare una raccolta UID. Questi metodi non accettano espressioni VSTest --filter .

Soddisfare i requisiti del client

Usare C# 12 o versione successiva. Il pacchetto rispetta l'impostazione della versione del language del progetto. Se per impostazione predefinita il framework di destinazione è una versione C# precedente, impostare LangVersion su 12.0 o versione successiva. Il pacchetto fornisce anche tipi di compatibilità interni per .NET Framework 4.6.2 e netstandard2.0.

Rimuovere qualsiasi copia scritta a mano del client MTP prima di aggiungere il pacchetto per evitare tipi interni duplicati.

Scegliere un modello di avvio

Per avviare l'applicazione di test come processo esterno, usare MtpServerClient.LaunchAsync. Il client è proprietario del processo figlio e lo termina durante l'disinstallazione quando necessario.

Per configurare un'applicazione di test esterna per un determinato ambiente, impostare MtpServerClientOptions.EnvironmentVariables prima dell'avvio. Il processo figlio eredita l'ambiente del processo padre, con i valori forniti applicati al processo figlio. Queste opzioni non modificano l'ambiente per un host in-process.

Ogni client controlla un'applicazione di test. Per più applicazioni di test, creare un client per ogni applicazione e aggregare i risultati nello strumento.

Per un host incorporato o un ambiente thread dell'interfaccia utente che non può usare Process.Start, usare MtpServerClient.LaunchInProcessAsync. Il metodo esegue l'applicazione di test tramite un callback asincrono nel processo corrente. Non bloccare il thread di avvio perché il callback e il client condividono il processo.

Entrambi i modelli di avvio usano un trasporto TCP loopback. Browser WebAssembly non è in grado di creare il listener necessario, quindi l'avvio nel processo genera PlatformNotSupportedException. L'avvio nel processo non fornisce un trasporto WebAssembly alternativo.

Arresta il client

Per rimuovere il client e avviare l'applicazione senza bloccare il thread chiamante, chiamare IMtpServerClient.ShutdownAsync(). Il metodo asincrono evita la velocità di risposta del thread dell'interfaccia utente e i problemi di deadlock negli host incorporati.

Per un host in-process, MtpServerClientOptions.ServerShutdownTimeout controlla il tempo di attesa dell'disinstallazione dopo che il client chiude il trasporto. Il valore predefinito è 30 secondi. Leggi ServerExitCode dopo lo spegnimento per ottenere il codice di uscita del callback. Il valore rimane null durante l'esecuzione dell'applicazione o quando l'applicazione non riesce anziché restituire un codice di uscita.

Dichiarare la gestione dello stato del client

A partire da MTP 2.4.1, impostare l'opzione nullable MtpServerClientOptions.IsStateful per descrivere come il client utilizza gli aggiornamenti di test-node:

Value Dichiarazione
true Il client mantiene un insieme di nodi di test indirizzabili per la sessione e mantiene l'ultimo stato noto di ogni nodo fino a quando non viene modificato da un altro aggiornamento, come potrebbe fare Esplora test dell'IDE.
false Il client elabora gli aggiornamenti senza mantenere lo stato del nodo di test.
null (impostazione predefinita) Il client non dichiara la capacità. L'handshake di inizializzazione omette il campo.

Il client invia un valore booleano esplicito tramite la funzionalità del protocollo sperimentale capabilities.testing.isStateful . La persistenza dello stato è indipendente dalla durata della connessione e dalla funzionalità del MultiRequestSupport server. Un client non diventa stateful solo perché invia diverse richieste sulla stessa connessione.

MTP 2.4.0 usa un'opzione booleana non nullable con valore predefinito pari a false. Per preservare una funzionalità non dichiarata, usare MTP 2.4.1 o versione successiva.

All'interno di un framework o di un'estensione, usare il servizio sperimentale IClientInfo service per esaminare la dichiarazione. Leggere clientInfo.Capabilities.IsStateful per ottenere un risultato booleano conservativo, ovvero false quando la funzionalità non è dichiarata. In MTP 2.4.1 o versione successiva, ClientCapabilitiesExtensions.GetIsStateful() mantiene la distinzione tra client non dichiarati, esplicitamente senza stato e con stato.

Negoziare la compatibilità del protocollo

A partire da MTP 2.4, la negoziazione del protocollo in modalità server è indipendente dalla versione del pacchetto NuGet MTP. Un client dichiara le versioni del protocollo supportate e il server seleziona una versione comune. Quando nessuna versione supportata si sovrappone, l'applicazione di test viene chiusa con il ProtocolError codice di uscita (12) anziché continuare con un contratto di collegamento incompatibile.

Usare lo schema JSON in modalità server con versione 1.0 per convalidare i messaggi. I serializzatori System.Text.Json e Jsonite seguono lo stesso schema e la stessa busta di errore.

Vedere anche