Grabar autotests de IU de video trabajando en Chrome sin cabeza

¡Hola a todos!

En este artículo quiero hablar sobre cómo se resolvió el problema de grabar pruebas automáticas de video que funcionan en Chrome sin cabeza (no debería haber problemas con la grabación en modo sin cabeza). Se considerarán varios enfoques diferentes y se informarán las dificultades y las formas de resolverlos.

Formulación del problema

  1. Las pruebas se ejecutan en Windows
  2. Las pruebas utilizan Selenium Web Driver + Chrome sin cabeza
  3. Las pruebas se ejecutan en múltiples hilos

Para las pruebas caídas, debe guardar el video, mientras

  1. El tiempo de ejecución no debe crecer más del 10%.
  2. Hacer con un mínimo de cambios en la implementación actual

Si está interesado en una solución, bienvenido a cat.

El enfoque ingenuo. Capturas de pantalla


Nuestro marco de prueba tiene una envoltura de bajo nivel sobre selenio. Por lo tanto, la primera implementación fue muy simple y extremadamente ingenua: se agregó código a todos los lugares que cambian la página (clic, establecer cuadro de texto, navegar, etc.), lo que guarda una captura de pantalla a través del controlador web Selenium

Driver.TakeScreenshot().SaveAsFile(screenshotPath);

El tiempo de ejecución de prueba con este enfoque ha crecido significativamente. Motivo: la operación de guardar la captura de pantalla no funciona del todo rápidamente, de 0,5 segundos a varios segundos.

Capturas de pantalla en una secuencia separada


En lugar de código que guarda capturas de pantalla en todos los lugares que cambian la página (clic, establecer cuadro de texto, navegar), se agregó un código que guarda constantemente capturas de pantalla en una secuencia separada

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

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


El tiempo de ejecución de la prueba aún era muy largo. No entendí la razón del retraso. Lo más probable es que Selenium se niegue a hacer algo mientras se guarda la captura de pantalla. Quizás otra instancia de Selenium hubiera ayudado, terminando en la misma sesión.

Capturas de pantalla en un hilo separado a través de Puppeteer


No fue muy interesante hacer dos instancias de Selenium, ya que durante mucho tiempo había querido probar el titiritero en la práctica, y aquí encontré una razón adecuada. Del lado de Selenium, se creó Puppeteer, que simplemente se conectó a Chrome, ya creado a través de Selenium

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

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


La prueba pasó por Selenium, y Puppeteer tomó capturas de pantalla en un hilo separado

Texto oculto
...
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();
    ...
}


Esta implementación dio resultados alentadores, el tiempo de ejecución aumentó en un 10%.

Menos

  1. El tiempo para guardar capturas de pantalla a través de Puppeteer no es instantáneo, algunos de los fotogramas se perderán y pueden resultar algo interesante de analizar.
  2. Si Selenium cambia las pestañas, debe notificar a Puppeteer, de lo contrario, solo capturará la primera pestaña del código anterior (tal vez haya una manera de encontrar la pestaña activa: debe buscar).

El primer menos resultó ser un bloqueo para mí, así que pasamos a la siguiente solución.

Screencast


Chrome tiene una característica interesante: Page.startScreencast . Según la descripción, ella solo hace lo que se necesita, lanza los marcos cambiados para que aquellos que lo deseen puedan interceptarlos y hacer algo interesante con ellos.

Tanto en Selenium como en Puppeteer, puede iniciar Page.startScreencast, pero no puede agregar controladores en uno u otro. El hotelero ya tiene voz: estamos esperando la implementación.

Intenté hacerme amigo de la biblioteca ChromeDevTools . Lamentablemente, no pude establecer rápidamente buenas relaciones con ella. Después de más búsquedas, se encontró una solución para ScreenCast en mafredri / cdp. La navegación innecesaria se eliminó del ejemplo original y se agregaron los parámetros de entrada necesarios:

Texto oculto
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
}


Además, este archivo fue compilado por el comando:

go build -o screencast.exe main.go

Y pude usarlo en una solución C # con pruebas:

Texto oculto
var startInfo = new ProcessStartInfo(screenCastPath)
{
    WindowStyle = ProcessWindowStyle.Minimized,

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

Process.Start(startInfo);


Un flujo separado para grabar capturas de pantalla fue descartado como innecesario. El algoritmo de trabajo fue el siguiente:

  1. Inicio de Chrome a través de Selenium
  2. Comenzamos Screencast a través del binario ensamblado: se conecta a Chrome y comienza a guardar la secuencia de marcos en la carpeta que especificamos
  3. Al final de la prueba, cierre Chrome: el binario se cierra automáticamente
  4. Si la prueba falla, cree un video
  5. Limpiamos la carpeta con marcos

Este enfoque dio el mejor resultado en términos de tiempo de ejecución (prácticamente no hay retrasos). Además, proporcionó la máxima información sobre la prueba (prácticamente no hay cuadros perdidos).

Contras

1. Baja resolución para screencast. Si ejecuta las pruebas en un par de secuencias y establece la resolución en 2560 * 1440 para Chrome, el búfer asignado para la transferencia de datos se desbordará .

2. Con un aumento en la resolución, aumenta la carga en la CPU.

Como resultado, en screencast, elegí la resolución de 1024 * 576: en esta resolución, las pruebas funcionaron bien en 6 hilos, el procesador funcionó en modo cómodo (6 núcleos i7-5820).

Recopilamos video


Queda por recopilar fotogramas en el video. Para esto, utilicé la biblioteca SharpAvi

Texto oculto
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
    }
}


Imágenes de lujo


Dado que la resolución del screencast es muy pequeña 1024 * 576, debe establecer una resolución pequeña para Chrome, de lo contrario habrá problemas con el texto pequeño.

Chrome 2560 * 1440 -> screencast en 1024 * 576 = el texto pequeño es prácticamente ilegible
Chrome 1920 * 1080 -> screencast en 1024 * 576 = el texto pequeño se lee con dificultad
Chrome 1408 * 792 -> screencast en 1024 * 576 = el texto pequeño se lee sin problemas

El video resultante 1024 * 576 se puede mejorar, si los cuadros se reducen a 1920 * 1080 utilizando la biblioteca PhotoSauce

Texto oculto
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);
		}
	}
}


Como resultado, se obtuvieron los siguientes parámetros: Chrome funciona en 1408 * 792, ScreenCast en 1024 * 576, el video final para ver escalas superiores a 1920 * 1080. Haga clic aquí para ver un ejemplo del resultado final.

gracias


Gracias a todos los que han leído: si hay una solución más simple para el problema original, escriba el comentario. También se acepta cualquier crítica, incluida la crítica maliciosa como se describió anteriormente.

¡Toda la salud y el rápido final a las codiciadas restricciones!

All Articles