Gravar autotestes de interface do usuário de vídeo trabalhando no Chrome sem cabeça

Olá a todos!

Neste artigo, quero falar sobre como foi resolvida a tarefa de gravar auto-testes de vídeo trabalhando no Chrome sem cabeça (não deve haver problemas com a gravação no modo sem cabeça). Várias abordagens diferentes serão consideradas, e dificuldades e formas de resolvê-las serão contadas.

Formulação do problema

  1. Testes executados no Windows
  2. Os testes usam o Selenium Web Driver + Chrome sem cabeça
  3. Testes executados em vários threads

Para testes reprovados, você precisa salvar o vídeo, enquanto

  1. O tempo de execução não deve crescer mais que 10%.
  2. Faça com um mínimo de alterações na implementação atual

Se você estiver interessado em uma solução, seja bem-vindo ao gato.

A abordagem ingênua. Screenshots


Nossa estrutura de teste possui um invólucro de baixo nível sobre o Selenium. Portanto, a primeira implementação foi muito simples e extremamente ingênua: foi adicionado código a todos os locais que alteram a página (Clique, Definir caixa de texto, Navegar, etc.), que salva uma captura de tela através do Selenium Web Driver

Driver.TakeScreenshot().SaveAsFile(screenshotPath);

O tempo de execução do teste com essa abordagem aumentou significativamente. Motivo: a operação de salvar a captura de tela não funciona rapidamente - de 0,5 a vários segundos.

Capturas de tela em um fluxo separado


Em vez de um código que salva capturas de tela em todos os lugares que alteram a página (Clique, Definir caixa de texto, Navegar), foi adicionado um código que salva constantemente capturas de tela em um fluxo separado

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

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


O tempo de execução do teste ainda era muito longo. Não entendi o motivo do atraso. Provavelmente, o Selenium se recusa a fazer alguma coisa enquanto a captura de tela está sendo salva. Talvez outra instância do Selenium ajudasse, terminando na mesma sessão.

Capturas de tela em um tópico separado através do Puppeteer


Não foi muito interessante fazer duas instâncias de selênio, já que há muito tempo eu queria tentar afinar os marionetes na prática - e aqui encontrei um motivo adequado. No lado do Selenium, foi criado o Puppeteer, que simplesmente se conectava ao Chrome, já criado pelo Selenium

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

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


O teste passou pelo Selenium, e o Puppeteer tirou screenshots em um tópico 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 implementação deu resultados encorajadores, o tempo de execução aumentou 10%.

Minuses

  1. O tempo para salvar as capturas de tela através do Puppeteer não é instantâneo, alguns dos quadros serão perdidos e podem ser algo interessante de analisar.
  2. Se o Selenium alternar entre guias, é necessário notificar o Puppeteer, caso contrário, apenas capturará a primeira guia no código acima (talvez exista uma maneira de encontrar a guia ativa - você precisa procurar).

O primeiro sinal de menos acabou bloqueando para mim, então passamos para a próxima solução.

Screencast


O Chrome tem um recurso interessante - Page.startScreencast . De acordo com a descrição - ela apenas faz o necessário - ela lança os quadros alterados para que aqueles que desejem possam interceptá-los e fazer algo interessante com eles.

No Selenium e no Puppeteer, é possível iniciar o Page.startScreencast, mas não é possível adicionar manipuladores em um ou no outro. O hoteleiro já foi dublado - estamos aguardando a implementação.

Tentei fazer amizade com a biblioteca ChromeDevTools . Infelizmente, não consegui estabelecer rapidamente boas relações com ela. Após pesquisas adicionais, foi encontrada uma solução para o ScreenCast em mafredri / cdp.A navegação desnecessária foi removida do exemplo original e os parâmetros de entrada necessários foram adicionados:

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
}


Além disso, esse arquivo foi compilado pelo comando:

go build -o screencast.exe main.go

E eu pude usá-lo em uma solução C # com testes:

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);


Um fluxo separado para gravar capturas de tela foi jogado fora como desnecessário. O algoritmo de trabalho foi o seguinte:

  1. Iniciando o Chrome via Selenium
  2. Iniciamos o Screencast através do binário montado - ele se conecta ao Chrome e começa a salvar o fluxo de quadros na pasta especificada
  3. No final do teste, feche o Chrome - o binário fecha automaticamente
  4. Se o teste falhar - crie um vídeo
  5. Limpamos a pasta com molduras

Essa abordagem deu o melhor resultado em termos de tempo de execução (praticamente não há atrasos). Além disso, ele forneceu o máximo de informações sobre o teste (praticamente não há quadros perdidos).

Contras

1. Baixa resolução para screencast. Se você executar os testes em alguns fluxos e definir a resolução para 2560 * 1440 para Chrome, o buffer alocado para transferência de dados será excedido .

2. Com um aumento na resolução, a carga na CPU aumenta.

Como resultado, no screencast, escolhi a resolução de 1024 * 576 - nessa resolução, os testes funcionaram bem em 6 threads, o processador funcionou no modo confortável (6 core i7-5820).

Nós coletamos vídeo


Resta coletar quadros no vídeo. Para isso, usei a 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
    }
}


Imagens de gama alta


Como a resolução do screencast é muito pequena, 1024 * 576, é necessário definir uma resolução pequena para o próprio Chrome, caso contrário, haverá problemas com o texto pequeno.

Chrome 2560 * 1440 -> screencast em 1024 * 576 = texto pequeno é praticamente ilegível
Chrome 1920 * 1080 -> screencast em 1024 * 576 = texto pequeno é lido com dificuldade
Chrome 1408 * 792 -> screencast em 1024 * 576 = texto pequeno é lido sem problemas

O vídeo resultante 1024 * 576 pode ser aprimorado - se os quadros forem reduzidos para 1920 * 1080 usando a 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, foram obtidos os seguintes parâmetros: O Chrome funciona em 1408 * 792, o ScreenCast em 1024 * 576, o vídeo final para visualização de upscales para 1920 * 1080. Clique aqui para ver um exemplo do resultado final.

obrigado


Obrigado a todos que leram - se houver uma solução mais simples para o problema original, escreva no comentário. Qualquer crítica também é aceita, incluindo críticas maliciosas, conforme descrito acima.

Toda a saúde e rápido fim de restrições cobiçadas!

All Articles