Nehmen Sie Video-UI-Autotests auf, die in kopflosem Chrome funktionieren

Hallo alle zusammen!

In diesem Artikel möchte ich darüber sprechen, wie die Aufgabe des Aufzeichnens von Video-Autotests in kopflosem Chrome gelöst wurde (es sollte keine Probleme mit der Aufzeichnung im nicht kopflosen Modus geben). Es werden verschiedene Ansätze in Betracht gezogen und Schwierigkeiten und Lösungswege aufgezeigt.

Formulierung des Problems

  1. Tests werden unter Windows ausgeführt
  2. Tests verwenden Selenium Web Driver + Headless Chrome
  3. Tests werden in mehreren Threads ausgeführt

Für gefallene Tests müssen Sie das Video währenddessen speichern

  1. Die Laufzeit sollte um nicht mehr als 10% steigen.
  2. Machen Sie mit einem Minimum an Änderungen in der aktuellen Implementierung

Wenn Sie an einer Lösung interessiert sind, sind Sie bei cat willkommen.

Der naive Ansatz. Screenshots


Unser Test-Framework verfügt über einen Low-Level-Wrapper über Selen. Daher war die erste Implementierung sehr einfach und äußerst naiv: Code wurde an allen Stellen hinzugefügt, die die Seite ändern (Klicken, Textfeld festlegen, Navigieren usw.), wodurch ein Screenshot über den Selenium-Webtreiber gespeichert wird

Driver.TakeScreenshot().SaveAsFile(screenshotPath);

Die Testausführungszeit mit diesem Ansatz ist erheblich gewachsen. Grund: Das Speichern des Screenshots funktioniert überhaupt nicht schnell - von 0,5 Sekunden bis zu mehreren Sekunden.

Screenshots in einem separaten Stream


Anstelle von Code, der Screenshots an allen Stellen speichert, die die Seite ändern (Klicken, Textfeld festlegen, Navigieren), wurde ein Code hinzugefügt, der Screenshots ständig in einem separaten Stream speichert

Versteckter Text
...
var timeTillNextFrame = TimeSpan.Zero;
while (!_stopThread.WaitOne(timeTillNextFrame))
{
    var screenShotDriver = Driver as ITakesScreenshot;
    if (screenShotDriver == null)
    {
        continue;
    }

    var screenShot = screenShotDriver.GetScreenshot();
    ...
}


Die Testausführungszeit war noch sehr lang. Ich habe den Grund für die Verzögerung nicht verstanden. Höchstwahrscheinlich weigert sich Selen, etwas zu tun, während der Screenshot gespeichert wird. Vielleicht hätte eine andere Instanz von Selen geholfen und in derselben Sitzung geendet.

Screenshots in einem separaten Thread durch Puppenspieler


Es war nicht sehr interessant, zwei Selen-Instanzen zu machen, da ich schon lange Puppenspieler-scharf in der Praxis ausprobieren wollte - und hier fand ich einen geeigneten Grund. Auf der Seite von Selenium wurde Puppeteer erstellt, das einfach mit Chrome verbunden ist und bereits über Selenium erstellt wurde

Versteckter Text
var options = new ConnectOptions()
{
    BrowserURL = $"http://127.0.0.1:{debugPort}"
};

_puppeteerBrowser = Puppeteer.ConnectAsync(options).GetAwaiter().GetResult();


Der Test ging seinen eigenen Weg durch Selen und Puppeteer machte Screenshots in einem separaten Thread

Versteckter Text
...
var timeTillNextFrame = TimeSpan.Zero;
while (!_stopThread.WaitOne(timeTillNextFrame))
{
    var pages = _puppeteerBrowser.PagesAsync().GetAwaiter().GetResult();
    if (pages.Length <= 0)
    {
        continue;
    }
    
    var page = pages[0];
    
    page.SetViewportAsync(new ViewPortOptions
    {
        Width = screenWidth,
        Height = screenHeight
    }).GetAwaiter().GetResult();
    
    var screen = page.ScreenshotStreamAsync().GetAwaiter().GetResult();
    ...
}


Diese Implementierung lieferte ermutigende Ergebnisse, die Ausführungszeit erhöhte sich um zulässige 10%.

Minuspunkte

  1. Die Zeit zum Speichern von Screenshots über Puppeteer ist nicht sofort, einige der Frames gehen verloren und es kann sich als interessant herausstellen, sie zu analysieren.
  2. Wenn Selenium die Registerkarten wechselt, müssen Sie Puppenspieler benachrichtigen. Andernfalls wird nur die erste Registerkarte im obigen Code angezeigt (möglicherweise gibt es eine Möglichkeit, die aktive Registerkarte zu finden - Sie müssen nachsehen).

Das erste Minus stellte sich für mich als blockierend heraus, also fahren wir mit der nächsten Lösung fort.

Screencast


Chrome hat eine interessante Funktion - Page.startScreencast . Gemäß der Beschreibung - sie tut nur das, was benötigt wird - wirft sie die geänderten Rahmen, damit diejenigen, die es wünschen, sie abfangen und etwas Interessantes mit ihnen machen können.

Sowohl in Selenium als auch in Puppeteer können Sie Page.startScreencast starten, aber Sie können weder in der einen noch in der anderen Handler hinzufügen. Der Hoteller ist bereits geäußert - wir warten auf die Umsetzung.

Ich habe versucht , mich mit der ChromeDevTools- Bibliothek anzufreunden . Leider konnte ich nicht schnell gute Beziehungen zu ihr aufbauen. Nach weiteren Suchen wurde eine Lösung für ScreenCast in mafredri / cdp gefunden. Aus dem ursprünglichen Beispiel wurde die unnötige Navigation entfernt und die erforderlichen Eingabeparameter hinzugefügt:

Versteckter Text
package main

import (
    "os"
    "context"
    "fmt"
    "io/ioutil"
    "log"
    "time"
    "flag"

    "github.com/mafredri/cdp"
    "github.com/mafredri/cdp/devtool"
    "github.com/mafredri/cdp/protocol/page"
    "github.com/mafredri/cdp/rpcc"
)

func main() {

    folderPtr := flag.String("folder", "", "folder path for screenshots: example c:\\temp\\screens\\")
    chromePtr := flag.String("chrome", "http://localhost:9222", "chrome connection - example: http://localhost:9222")
    
    widthPtr := flag.Int("width", 1280, "screencast width")
    heightPtr := flag.Int("height", 720, "screencast height")
    qualityPtr := flag.Int("quality", 100, "screencast quality")
    
    flag.Parse()

    if err := run(*folderPtr, *chromePtr, *widthPtr, *heightPtr, *qualityPtr); err != nil {
        panic(err)
    }
}

func run(folder string, chromeConnection string, width int, height int, quality int) error {
    ctx, cancel := context.WithCancel(context.TODO())
    defer cancel()
    
    chromePath := chromeConnection
    folderPath := folder

    devt := devtool.New(chromePath)

    pageTarget, err := devt.Get(ctx, devtool.Page)
    if err != nil {
        return err
    }

    conn, err := rpcc.DialContext(ctx, pageTarget.WebSocketDebuggerURL)
    if err != nil {
        return err
    }
    defer conn.Close()

    c := cdp.NewClient(conn)

    err = c.Page.Enable(ctx)
    if err != nil {
        return err
    }

    // Start listening to ScreencastFrame events.
    screencastFrame, err := c.Page.ScreencastFrame(ctx)
    if err != nil {
        return err
    }

    go func() {
        defer screencastFrame.Close()

        for {
            ev, err := screencastFrame.Recv()
            if err != nil {
                log.Printf("Failed to receive ScreencastFrame: %v", err)
                os.Exit(0)
            }
            log.Printf("Got frame with sessionID: %d: %+v", ev.SessionID, ev.Metadata)

            err = c.Page.ScreencastFrameAck(ctx, page.NewScreencastFrameAckArgs(ev.SessionID))
            if err != nil {
                log.Printf("Failed to ack ScreencastFrame: %v", err)
                os.Exit(0)
            }

            // Write to screencast_frame-[timestamp].png.
            name := fmt.Sprintf("screencast_frame-%d.png", ev.Metadata.Timestamp.Time().Unix())
            
            filePath := folderPath + name

            // Write the frame to file (without blocking).
            go func() {
                err = ioutil.WriteFile(filePath, ev.Data, 0644)
                if err != nil {
                    log.Printf("Failed to write ScreencastFrame to %q: %v", name, err)
                }
            }()
        }
    }()

    screencastArgs := page.NewStartScreencastArgs().
        SetQuality(quality).
        SetMaxWidth(width).
        SetMaxHeight(height).
        SetEveryNthFrame(1).
        SetFormat("png")
    err = c.Page.StartScreencast(ctx, screencastArgs)
    if err != nil {
        return err
    }

    // Random delay for our screencast.
    time.Sleep(600 * time.Second)

    err = c.Page.StopScreencast(ctx)
    if err != nil {
        return err
    }

    return nil
}


Ferner wurde diese Datei mit dem folgenden Befehl kompiliert:

go build -o screencast.exe main.go

Und ich konnte es in einer C # -Lösung mit Tests verwenden:

Versteckter Text
var startInfo = new ProcessStartInfo(screenCastPath)
{
    WindowStyle = ProcessWindowStyle.Minimized,

    Arguments = $"-folder={_framesFolderPath} " +
                $"-chrome=http://localhost:{_debugPort} " +
                "-width=1024 " +
                "-height=576 " +
                "-quality=0"
};

Process.Start(startInfo);


Ein separater Stream zum Aufzeichnen von Screenshots wurde als unnötig verworfen. Der Arbeitsalgorithmus war wie folgt:

  1. Starten von Chrome über Selen
  2. Wir starten Screencast über die zusammengestellte Binärdatei - sie stellt eine Verbindung zu Chrome her und speichert den Frame-Stream in dem von uns angegebenen Ordner
  3. Schließen Sie am Ende des Tests Chrome - die Binärdatei wird automatisch geschlossen
  4. Wenn der Test fehlschlägt, erstellen Sie ein Video
  5. Wir reinigen den Ordner mit Frames

Dieser Ansatz ergab das beste Ergebnis in Bezug auf die Ausführungszeit (es gibt praktisch keine Verzögerungen). Außerdem lieferte er maximale Informationen zum Test (es gibt praktisch keine verlorenen Frames).

Nachteile

1. Niedrige Auflösung für Screencast. Wenn Sie die Tests in mehreren Streams ausführen und die Auflösung für Chrome auf 2560 * 1440 festlegen, läuft der für die Datenübertragung zugewiesene Puffer über .

2. Mit zunehmender Auflösung steigt die Belastung der CPU.

Als Ergebnis habe ich unter Screencast die Auflösung 1024 * 576 gewählt - bei dieser Auflösung funktionierten die Tests in 6 Threads einwandfrei, der Prozessor im komfortablen Modus (6 Core i7-5820).

Wir sammeln Videos


Es bleibt, Bilder im Video zu sammeln. Dafür habe ich die SharpAvi- Bibliothek verwendet

Versteckter Text
private void GenerateVideoFromScreens(string videoPath)
{
    try
    {
        var videoWriter = new AviWriter(videoPath) { FramesPerSecond = 1, EmitIndex1 = true };

        var videoStream = videoWriter.AddMotionJpegVideoStream(1024, 576);

        var screens = new DirectoryInfo(_framesFolderPath).GetFiles().OrderBy(f => f.CreationTimeUtc.Ticks).ToList();
        foreach (var screen in screens)
        {
            try
            {
                using (var bmp = new Bitmap(screen.FullName))
                {
                    var bits = bmp.LockBits(new Rectangle(0, 0, videoStream.Width, videoStream.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb);
                    var videoFrame = new byte[videoStream.Width * videoStream.Height * 4];
                    Marshal.Copy(bits.Scan0, videoFrame, 0, videoFrame.Length);
                    bmp.UnlockBits(bits);

                    videoStream.WriteFrameAsync(
                        true,
                        videoFrame,
                        0,
                        videoFrame.Length).GetAwaiter().GetResult();
                }
            }
            catch(Exception ex)
            {
                // ignore all video related errors per frame
            }
        }

        videoWriter.Close();
    }
    catch
    {
        // ignore all video related errors per streams
    }
}


Gehobene Bilder


Da die Auflösung von Screencasts 1024 * 576 sehr gering ist, müssen Sie eine kleine Auflösung für Chrome selbst festlegen, da sonst Probleme mit kleinem Text auftreten.

Chrome 2560 * 1440 -> Screencast bei 1024 * 576 = kleiner Text ist praktisch unlesbar
Chrome 1920 * 1080 -> Screencast bei 1024 * 576 = kleiner Text wird nur schwer gelesen
Chrome 1408 * 792 -> Screencast bei 1024 * 576 = kleiner Text wird ohne Probleme gelesen

Das resultierende Video 1024 * 576 kann verbessert werden - wenn die Frames mithilfe der PhotoSauce- Bibliothek auf 1920 * 1080 verkleinert werden

Versteckter Text
public Bitmap ResizeImage(Bitmap bitmap, int width)
{
	using (var inStream = new MemoryStream())
	{
		bitmap.Save(inStream, ImageFormat.Png);
		inStream.Position = 0;
		using (MemoryStream outStream = new MemoryStream())
		{
			var settings = new ProcessImageSettings { Width = width };
			MagicImageProcessor.ProcessImage(inStream, outStream, settings);
			return new Bitmap(outStream);
		}
	}
}


Als Ergebnis wurden die folgenden Parameter erhalten: Chrome funktioniert in 1408 * 792, ScreenCast in 1024 * 576, das endgültige Video zum Anzeigen von Upscales auf 1920 * 1080. Klicken Sie hier, um ein Beispiel für das Endergebnis anzuzeigen.

Danke


Vielen Dank an alle, die gelesen haben - wenn es eine einfachere Lösung für das ursprüngliche Problem gibt, schreiben Sie bitte in den Kommentar. Jede Kritik wird ebenfalls akzeptiert, einschließlich böswilliger Kritik wie oben beschrieben.

Alle Gesundheit und schnelles Ende der begehrten Einschränkungen!

All Articles