blob: adf9f0bbf14acdfccb23567a442b294e4973cc3d [file] [log] [blame]
// Copyright 2021 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 cca provides utilities to interact with Chrome Camera App.
package cca
import (
"context"
"os"
"path/filepath"
"time"
"chromiumos/tast/common/android/ui"
"chromiumos/tast/common/camera/chart"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/fsutil"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/assistant"
"chromiumos/tast/local/camera/testutil"
"chromiumos/tast/local/chrome"
"chromiumos/tast/ssh"
"chromiumos/tast/testing"
)
const (
ccaSetUpTimeout = 25 * time.Second
ccaTearDownTimeout = 5 * time.Second
testBridgeSetUpTimeout = 20 * time.Second
setUpTimeout = chrome.LoginTimeout + testBridgeSetUpTimeout
tearDownTimeout = chrome.ResetTimeout
)
type feature string
const (
manualCrop feature = "CameraAppDocumentManualCrop"
)
func init() {
testing.AddFixture(&testing.Fixture{
Name: "ccaLaunched",
Desc: "Launched CCA",
Contacts: []string{"wtlee@chromium.org"},
Data: []string{"cca_ui.js"},
Impl: &fixture{launchCCA: true},
SetUpTimeout: setUpTimeout,
ResetTimeout: testBridgeSetUpTimeout,
PreTestTimeout: ccaSetUpTimeout,
PostTestTimeout: ccaTearDownTimeout,
TearDownTimeout: tearDownTimeout,
})
testing.AddFixture(&testing.Fixture{
Name: "ccaLaunchedGuest",
Desc: "Launched CCA",
Contacts: []string{"wtlee@chromium.org"},
Data: []string{"cca_ui.js"},
Impl: &fixture{guestMode: true, launchCCA: true},
SetUpTimeout: setUpTimeout,
ResetTimeout: testBridgeSetUpTimeout,
PreTestTimeout: ccaSetUpTimeout,
PostTestTimeout: ccaTearDownTimeout,
TearDownTimeout: tearDownTimeout,
})
testing.AddFixture(&testing.Fixture{
Name: "ccaLaunchedWithFakeCamera",
Desc: "Launched CCA with fake camera input",
Contacts: []string{"wtlee@chromium.org"},
Data: []string{"cca_ui.js"},
Impl: &fixture{fakeCamera: true, launchCCA: true},
SetUpTimeout: setUpTimeout,
ResetTimeout: testBridgeSetUpTimeout,
PreTestTimeout: ccaSetUpTimeout,
PostTestTimeout: ccaTearDownTimeout,
TearDownTimeout: tearDownTimeout,
})
testing.AddFixture(&testing.Fixture{
Name: "ccaTestBridgeReady",
Desc: "Set up test bridge for CCA",
Contacts: []string{"wtlee@chromium.org"},
Data: []string{"cca_ui.js"},
Impl: &fixture{},
SetUpTimeout: setUpTimeout,
ResetTimeout: testBridgeSetUpTimeout,
TearDownTimeout: tearDownTimeout,
})
testing.AddFixture(&testing.Fixture{
Name: "ccaTestBridgeReadyWithFakeCamera",
Desc: `Set up test bridge for CCA with fake camera. Any tests using this
fixture should switch the camera scene before opening camera`,
Contacts: []string{"wtlee@chromium.org"},
Data: []string{"cca_ui.js"},
Impl: &fixture{fakeCamera: true, fakeScene: true},
SetUpTimeout: setUpTimeout,
ResetTimeout: testBridgeSetUpTimeout,
TearDownTimeout: tearDownTimeout,
})
testing.AddFixture(&testing.Fixture{
Name: "ccaTestBridgeReadyBypassPermissionClamshell",
Desc: "Set up test bridge for CCA with bypassPermission on clamshell mode on",
Contacts: []string{"wtlee@chromium.org"},
Data: []string{"cca_ui.js"},
Impl: &fixture{bypassPermission: true, forceClamshell: true},
SetUpTimeout: setUpTimeout,
ResetTimeout: testBridgeSetUpTimeout,
TearDownTimeout: tearDownTimeout,
})
testing.AddFixture(&testing.Fixture{
Name: "ccaTestBridgeReadyWithArc",
Desc: "Set up test bridge for CCA with ARC enabled",
Contacts: []string{"wtlee@chromium.org"},
Data: []string{"cca_ui.js"},
Impl: &fixture{arcBooted: true},
SetUpTimeout: setUpTimeout + arc.BootTimeout + ui.StartTimeout,
ResetTimeout: testBridgeSetUpTimeout,
TearDownTimeout: tearDownTimeout,
})
testing.AddFixture(&testing.Fixture{
Name: "ccaTestBridgeReadyForDocumentManualCrop",
Desc: "Set up test bridge for CCA and chrome for testing document manual crop",
Contacts: []string{"inker@chromium.org"},
Data: []string{"cca_ui.js"},
Impl: &fixture{
fakeCamera: true,
fakeScene: true,
features: []feature{manualCrop},
},
SetUpTimeout: setUpTimeout,
ResetTimeout: testBridgeSetUpTimeout,
TearDownTimeout: tearDownTimeout,
})
}
// DebugParams defines some useful flags for debug CCA tests.
type DebugParams struct {
SaveScreenshotWhenFail bool
SaveCameraFolderWhenFail bool
}
// TestWithAppParams defines parameters to control behaviors of running test
// with app.
type TestWithAppParams struct {
StopAppOnlyIfExist bool
}
// ResetChromeFunc reset chrome used in this fixture.
type ResetChromeFunc func(context.Context) error
// StartAppFunc starts CCA.
type StartAppFunc func(context.Context) (*App, error)
// StopAppFunc stops CCA.
type StopAppFunc func(context.Context, bool) error
// ResetTestBridgeFunc resets the test bridge.
type ResetTestBridgeFunc func(context.Context) error
// TestWithAppFunc is the function to run with app.
type TestWithAppFunc func(context.Context, *App) error
// FixtureData is the struct exposed to tests.
type FixtureData struct {
Chrome *chrome.Chrome
ARC *arc.ARC
TestBridge func() *testutil.TestBridge
// App returns the CCA instance which lives through the test.
App func() *App
// ResetChrome resets chrome used by this fixture.
ResetChrome ResetChromeFunc
// StartApp starts CCA which can be used between subtests.
StartApp StartAppFunc
// StopApp stops CCA which can be used between subtests.
StopApp StopAppFunc
// ResetTestBridgeFunc resets the test bridge. Usually we don't need to call
// it explicitly unless the sub test launch/tear-down the app itself.
ResetTestBridge ResetTestBridgeFunc
// SwitchScene switches the camera scene to the given scene. This only works
// for fixtures using fake camera stream.
SwitchScene func(string) error
// RunTestWithApp runs the given function with the handling of the app
// start/stop.
RunTestWithApp func(context.Context, TestWithAppFunc, TestWithAppParams) error
// PrepareChart prepares chart by loading the given scene. It only works for
// CameraBox.
PrepareChart func(ctx context.Context, addr, keyFile, contentPath string) error
// SetDebugParams sets the debug parameters for current test.
SetDebugParams func(params DebugParams)
}
type fixture struct {
cr *chrome.Chrome
arc *arc.ARC
tb *testutil.TestBridge
app *App
outDir string
chart *chart.Chart
cameraScene string
scriptPaths []string
fakeCamera bool
fakeScene bool
arcBooted bool
launchCCA bool
bypassPermission bool
forceClamshell bool
guestMode bool
debugParams DebugParams
features []feature
}
func (f *fixture) cameraType() testutil.UseCameraType {
if f.fakeCamera {
return testutil.UseFakeCamera
}
return testutil.UseRealCamera
}
func (f *fixture) SetUp(ctx context.Context, s *testing.FixtState) interface{} {
success := false
var chromeOpts []chrome.Option
for _, f := range f.features {
chromeOpts = append(chromeOpts, chrome.EnableFeatures(string(f)))
}
if f.fakeCamera {
chromeOpts = append(chromeOpts, chrome.ExtraArgs(
// The default fps of fake device is 20, but CCA requires fps >= 24.
// Set the fps to 30 to avoid OverconstrainedError.
"--use-fake-device-for-media-stream=fps=30"))
if f.fakeScene {
dataDir := filepath.Dir(s.DataPath("cca_ui.js"))
f.cameraScene = filepath.Join(dataDir, "camera_scene.mjpeg")
chromeOpts = append(chromeOpts, chrome.ExtraArgs(
// Set the default camera scene as the input of the fake stream.
// The content of the scene can be dynamically changed during tests.
"--use-file-for-fake-video-capture="+f.cameraScene))
}
}
if f.guestMode {
chromeOpts = append(chromeOpts, chrome.GuestLogin())
}
if f.arcBooted {
chromeOpts = append(chromeOpts, chrome.ARCEnabled(), chrome.ExtraArgs("--disable-features=ArcResizeLock"))
}
if f.bypassPermission {
chromeOpts = append(chromeOpts, chrome.ExtraArgs("--use-fake-ui-for-media-stream"))
}
if f.forceClamshell {
chromeOpts = append(chromeOpts, chrome.ExtraArgs("--force-tablet-mode=clamshell"))
}
// Enable assistant verbose logging for the CCAUIAssistant test. Since
// assistant is disabled by default, this should not affect other tests.
chromeOpts = append(chromeOpts, assistant.VerboseLogging())
cr, err := chrome.New(ctx, chromeOpts...)
if err != nil {
s.Fatal("Failed to start Chrome: ", err)
}
f.cr = cr
defer func() {
if !success {
f.cr.Close(ctx)
f.cr = nil
}
}()
if f.arcBooted {
a, err := arc.New(ctx, s.OutDir())
if err != nil {
s.Fatal("Failed to start ARC: ", err)
}
f.arc = a
defer func() {
if !success {
f.arc.Close(ctx)
f.arc = nil
}
}()
}
tb, err := testutil.NewTestBridge(ctx, cr, f.cameraType())
if err != nil {
s.Fatal("Failed to construct test bridge: ", err)
}
f.tb = tb
f.scriptPaths = []string{s.DataPath("cca_ui.js")}
success = true
return FixtureData{Chrome: f.cr, ARC: f.arc,
TestBridge: f.testBridge,
App: f.cca,
ResetChrome: f.resetChrome,
StartApp: f.startApp,
StopApp: f.stopApp,
ResetTestBridge: f.resetTestBridge,
SwitchScene: f.switchScene,
RunTestWithApp: f.runTestWithApp,
PrepareChart: f.prepareChart,
SetDebugParams: f.setDebugParams}
}
func (f *fixture) TearDown(ctx context.Context, s *testing.FixtState) {
if err := f.tb.TearDown(ctx); err != nil {
s.Error("Failed to tear down test bridge: ", err)
}
f.tb = nil
if f.arcBooted {
if err := f.arc.Close(ctx); err != nil {
s.Error("Failed to tear down ARC: ", err)
}
f.arc = nil
}
if err := f.cr.Close(ctx); err != nil {
s.Error("Failed to tear down Chrome: ", err)
}
f.cr = nil
if f.cameraScene != "" {
if err := os.RemoveAll(f.cameraScene); err != nil {
s.Error("Failed to remove camera scene: ", err)
}
f.cameraScene = ""
}
}
func (f *fixture) Reset(ctx context.Context) error {
return f.resetTestBridge(ctx)
}
func (f *fixture) PreTest(ctx context.Context, s *testing.FixtTestState) {
f.outDir = s.OutDir()
if f.launchCCA {
app, err := f.startApp(ctx)
if err != nil {
s.Fatal("Failed to start app: ", err)
}
f.app = app
}
}
func (f *fixture) PostTest(ctx context.Context, s *testing.FixtTestState) {
defer func() {
f.debugParams = DebugParams{}
}()
if f.chart != nil {
if err := f.chart.Close(ctx, f.outDir); err != nil {
s.Error("Failed to close chart: ", err)
}
f.chart = nil
}
if f.launchCCA {
if err := f.stopApp(ctx, s.HasError()); err != nil {
s.Fatal("Failed to stop app: ", err)
}
}
}
func (f *fixture) resetTestBridge(ctx context.Context) error {
if err := f.tb.TearDown(ctx); err != nil {
return errors.Wrap(err, "failed to tear down test bridge")
}
f.tb = nil
cameraType := testutil.UseRealCamera
if f.fakeCamera {
cameraType = testutil.UseFakeCamera
}
tb, err := testutil.NewTestBridge(ctx, f.cr, cameraType)
if err != nil {
return errors.Wrap(err, "failed to construct test bridge")
}
f.tb = tb
return nil
}
func (f *fixture) resetChrome(ctx context.Context) error {
if err := f.cr.ResetState(ctx); err != nil {
return errors.Wrap(err, "failed to reset chrome in fixture")
}
tb, err := testutil.NewTestBridge(ctx, f.cr, f.cameraType())
if err != nil {
return errors.Wrap(err, "failed to construct test bridge after reset chrome state")
}
f.tb = tb
return nil
}
func (f *fixture) startApp(ctx context.Context) (*App, error) {
if err := ClearSavedDir(ctx, f.cr); err != nil {
return nil, errors.Wrap(err, "failed to clear camera folder")
}
app, err := New(ctx, f.cr, f.scriptPaths, f.outDir, f.tb)
if err != nil {
return nil, errors.Wrap(err, "failed to open CCA")
}
f.app = app
return f.app, nil
}
func (f *fixture) stopApp(ctx context.Context, hasError bool) (retErr error) {
if f.app == nil {
return
}
defer func(ctx context.Context) {
if err := f.app.CloseWithDebugParams(ctx, f.debugParams); err != nil {
retErr = errors.Wrap(retErr, err.Error())
}
f.app = nil
}(ctx)
if hasError && f.debugParams.SaveScreenshotWhenFail {
if err := f.app.SaveScreenshot(ctx); err != nil {
return errors.Wrap(err, "failed to save a screenshot")
}
}
return nil
}
func (f *fixture) stopAppIfExist(ctx context.Context, hasError bool) error {
if appExist, err := InstanceExists(ctx, f.cr); err != nil {
return errors.Wrap(err, "failed to check existence of CCA")
} else if appExist {
return f.stopApp(ctx, hasError)
} else {
// The app does not exist, might be proactively closed by the test flow.
f.app = nil
}
return nil
}
// switchScene switches the camera scene of fake camera to the given |scene|.
func (f *fixture) switchScene(scene string) error {
if f.cameraScene == "" {
return errors.New("failed to switch scene for non-fake camera stream")
}
if err := fsutil.CopyFile(scene, f.cameraScene); err != nil {
return errors.Wrapf(err, "failed to copy from the given scene: %v", scene)
}
return nil
}
func (f *fixture) runTestWithApp(ctx context.Context, testFunc TestWithAppFunc, params TestWithAppParams) (retErr error) {
app, err := f.startApp(ctx)
if err != nil {
return errors.Wrap(err, "failed to start app")
}
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 3*time.Second)
defer cancel()
defer f.resetTestBridge(cleanupCtx)
defer func(cleanupCtx context.Context) {
hasError := retErr != nil
stopFunc := f.stopApp
if params.StopAppOnlyIfExist {
stopFunc = f.stopAppIfExist
}
if err := stopFunc(cleanupCtx, hasError); err != nil {
retErr = errors.Wrap(retErr, err.Error())
}
}(cleanupCtx)
return testFunc(ctx, app)
}
func (f *fixture) prepareChart(ctx context.Context, addr, keyFile, contentPath string) (retErr error) {
var sopt ssh.Options
ssh.ParseTarget(addr, &sopt)
sopt.KeyFile = keyFile
sopt.ConnectTimeout = 10 * time.Second
conn, err := ssh.New(ctx, &sopt)
if err != nil {
return errors.Wrap(err, "failed to connect to chart tablet")
}
// No need to close ssh connection since chart will handle it when cleaning
// up.
c, namePaths, err := chart.SetUp(ctx, conn, f.outDir, []string{contentPath})
if err != nil {
return errors.Wrap(err, "failed to prepare chart tablet")
}
if err := c.Display(ctx, namePaths[0]); err != nil {
return errors.Wrap(err, "failed to display chart on chart tablet")
}
f.chart = c
return nil
}
func (f *fixture) testBridge() *testutil.TestBridge {
return f.tb
}
func (f *fixture) cca() *App {
return f.app
}
func (f *fixture) setDebugParams(params DebugParams) {
f.debugParams = params
}