blob: a9d5fb3e04db5b29ac7d49029a5560e2862566f7 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package jstest contains utilities to run device tests using the
// chrome JS controller API.
package jstest
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/uhid"
"chromiumos/tast/testing"
)
// KernelCommunicationDone is used to determine when the get report requests by
// the kernel are done.
var KernelCommunicationDone = false
// Gamepad runs a javascript test for the given device. It compares
// the buttons listed in expectedButtons with the ones produced by the
// recording.
func Gamepad(ctx context.Context, s *testing.State, d *uhid.Device, replayPath, buttonMappings string, expectedButtons []string) {
for !KernelCommunicationDone {
status, err := d.Dispatch(ctx)
if err != nil {
s.Fatal("Failed during kernel communication: ", err)
}
// If no event was caught the kernel does not require a
// GetReportReply, we can proceed with the test.
if status == uhid.StatusNoEvent {
s.Log("No get report requests made by kernel")
break
}
}
s.Log("Kernel information requests done")
server := httptest.NewServer(http.FileServer(s.DataFileSystem()))
defer server.Close()
conn, err := prepareScript(ctx, server, buttonMappings)
if err != nil {
s.Fatal("Failed creating connection: ", err)
}
s.Log("Script ready")
defer conn.Close()
defer conn.CloseTarget(ctx)
s.Log("Starting replay and js test")
js := make(chan error)
go runJavascriptTest(ctx, conn, expectedButtons, js)
if err := replayDevice(ctx, d, replayPath); err != nil {
s.Fatal("Replay failed: ", err)
}
if err := d.Close(); err != nil {
s.Fatal("Failed to destroy controller: ", err)
}
s.Log("Destroyed device")
if err, ok := <-js; ok {
s.Fatal("JavaScript test failed: ", err)
}
}
// CreateDevice creates the device in the given recording and
// initializes it.
func CreateDevice(ctx context.Context, path string) (*uhid.Device, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var d *uhid.Device
if d, err = uhid.NewDeviceFromRecording(ctx, file); err != nil {
return nil, err
}
if err = d.NewKernelDevice(ctx); err != nil {
return nil, err
}
return d, nil
}
// replayDevice replays on d the events in the file in path. Returns
// error if opening the file failed or if the replay failed.
func replayDevice(ctx context.Context, d *uhid.Device, path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
return d.Replay(ctx, file)
}
// prepareScript returns the connection created from the url for
// ds3_replay.html and sets the given controller mappings.
func prepareScript(ctx context.Context, server *httptest.Server, mappings string) (*chrome.Conn, error) {
cr, err := chrome.New(ctx)
if err != nil {
return nil, err
}
conn, err := cr.NewConn(ctx, path.Join(server.URL, "replay.html"))
if err != nil {
return nil, err
}
if err := conn.WaitForExpr(ctx, "scriptReady"); err != nil {
return nil, errors.Wrap(err, "failed waiting for script to be ready")
}
call := fmt.Sprintf("setMappings(%s)", mappings)
if err := conn.WaitForExpr(ctx, call); err != nil {
return nil, errors.Wrap(err, "failed setting button mappings")
}
return conn, nil
}
// runJavascriptTest waits for the gamepad disconnected event
// (triggered by a call to uhid.Device.Close). After this it compares
// the button sequence gathered by the test with the expected button
// sequence. If at any point there's an error it will be written to
// the channel and the function will return.
func runJavascriptTest(ctx context.Context, conn *chrome.Conn, expected []string, c chan error) {
if err := conn.WaitForExpr(ctx, "gamepadDisconnected"); err != nil {
c <- err
return
}
var actual []string
if err := conn.Eval(ctx, "getResults()", &actual); err != nil {
c <- err
return
}
if err := compareButtonSequence(expected, actual); err != nil {
c <- err
return
}
close(c)
}
// compareButtonSequence compares the expected button sequence with
// the actual one and returns and error if there's a mismatch.
func compareButtonSequence(expected, actual []string) error {
if len(expected) != len(actual) {
return errors.Errorf("expected button array length and actual button array lengths differ: got %d, want %d", len(actual), len(expected))
}
for i, v := range expected {
if v != actual[i] {
return errors.Errorf("button discrepancy at index %d: got %s, want %s", i, actual[i], v)
}
}
return nil
}