blob: 3428837ec484993c6719b5dbe8be808c05bc9ac7 [file]
// 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 filemanager
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"chromiumos/tast/common/action"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/fsutil"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/uiauto"
"chromiumos/tast/local/chrome/uiauto/faillog"
"chromiumos/tast/local/chrome/uiauto/filesapp"
"chromiumos/tast/local/chrome/uiauto/nodewith"
"chromiumos/tast/local/chrome/uiauto/role"
"chromiumos/tast/local/input"
"chromiumos/tast/local/sysutil"
"chromiumos/tast/testing"
)
type deletionTargetType uint8
const (
emptyFolder deletionTargetType = 1 << iota
nonEmptyFolder
file
)
const testFile = "files_app_test.png"
func init() {
testing.AddTest(&testing.Test{
Func: Deletion,
LacrosStatus: testing.LacrosVariantUnknown,
Desc: "Ensure deletion of files & folders work fine",
Contacts: []string{
"cienet-development@googlegroups.com",
"chromeos-sw-engprod@google.com",
"ting.chen@cienet.com",
},
Attr: []string{"group:mainline", "informational"},
SoftwareDeps: []string{"chrome"},
Data: []string{testFile},
Fixture: "chromeLoggedIn",
})
}
// Deletion deletes files and folders from Downloads & My files.
func Deletion(ctx context.Context, s *testing.State) {
cr := s.FixtValue().(*chrome.Chrome)
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
kb, err := input.Keyboard(ctx)
if err != nil {
s.Fatal("Failed to find keyboard: ", err)
}
defer kb.Close()
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to create test API connection: ", err)
}
filesApp, err := filesapp.Launch(ctx, tconn)
if err != nil {
s.Fatal("Failed to launch the Files App: ", err)
}
defer filesApp.Close(cleanupCtx)
defer faillog.DumpUITreeWithScreenshotOnError(cleanupCtx, s.OutDir(), s.HasError, cr, "ui_dump")
targetsName := map[deletionTargetType]string{
emptyFolder: "Empty",
nonEmptyFolder: "NonEmpty",
file: testFile,
}
dirPath := map[string]string{
filesapp.Downloads: filesapp.DownloadPath,
filesapp.MyFiles: filesapp.MyFilesPath,
}
res := &deletionTestResource{
tconn: tconn,
kb: kb,
filesApp: filesApp,
dataPath: s.DataPath(testFile),
}
for workingDir, workingDirPath := range dirPath {
for _, test := range []struct {
name string
testTargets deletionTargetType
}{
{"nonEmptyFolder", nonEmptyFolder},
{"emptyFolder", emptyFolder},
{"oneFile", file},
{"multiple", emptyFolder | nonEmptyFolder | file},
} {
subTest := func(ctx context.Context, s *testing.State) {
if err := filesApp.OpenDir(workingDir, filesapp.FilesTitlePrefix+workingDir)(ctx); err != nil {
s.Fatalf("Failed to open directory %q: %v", workingDir, err)
}
cleanupSubtestCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 3*time.Second)
defer cancel()
targets := generateTestTargets(test.testTargets, targetsName, workingDirPath)
defer cleanUp(cleanupSubtestCtx, targets)
if err := setupFilesAndFolders(res, targets); err != nil {
s.Fatal("Failed to set up files and folders: ", err)
}
if err := processForDeletion(ctx, res, targets); err != nil {
s.Fatal("Failed to do deletion: ", err)
}
}
testName := strings.ReplaceAll(workingDir+"_"+test.name, " ", "")
if !s.Run(ctx, testName, subTest) {
// Stop if any sub-test failed.
s.Fatalf("Failed to run subtest %s", testName)
}
}
}
}
type deletionTargetDetail struct {
name string
path string
isFolder bool
skipDeletion bool
}
type deletionTestResource struct {
tconn *chrome.TestConn
kb *input.KeyboardEventWriter
filesApp *filesapp.FilesApp
dataPath string
}
// generateTestTargets generates the test targets, returns the map of deletionTargetType and its detail.
func generateTestTargets(testTargets deletionTargetType, targetsName map[deletionTargetType]string,
workingDirPath string) *map[deletionTargetType]*deletionTargetDetail {
targets := map[deletionTargetType]*deletionTargetDetail{}
needToDelete := func(targetType deletionTargetType) bool { return (testTargets & targetType) != 0 }
if needToDelete(emptyFolder) {
targets[emptyFolder] = &deletionTargetDetail{
name: targetsName[emptyFolder],
path: filepath.Join(workingDirPath, targetsName[emptyFolder]),
isFolder: true,
}
}
if needToDelete(nonEmptyFolder) {
targets[nonEmptyFolder] = &deletionTargetDetail{
name: targetsName[nonEmptyFolder],
path: filepath.Join(workingDirPath, targetsName[nonEmptyFolder]),
isFolder: true,
}
targets[nonEmptyFolder|file] = &deletionTargetDetail{
name: targetsName[file],
path: filepath.Join(workingDirPath, targetsName[nonEmptyFolder], targetsName[file]),
skipDeletion: true, // The content of the folder will be deleted as the folder is deleted.
}
}
if needToDelete(file) {
targets[file] = &deletionTargetDetail{
name: targetsName[file],
path: filepath.Join(workingDirPath, targetsName[file]),
}
}
return &targets
}
// waitUntilFileDeleted waits the target is deleted from system
// to prevents the deletion conflict at the cleanup step.
func waitUntilFileDeleted(path string) action.Action {
return func(ctx context.Context) error {
if err := testing.Poll(ctx, func(ctx context.Context) error {
if _, err := os.Stat(path); !os.IsNotExist(err) {
return errors.New("file still exists")
}
return nil
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
return err
}
return nil
}
}
func processForDeletion(ctx context.Context, res *deletionTestResource, targets *map[deletionTargetType]*deletionTargetDetail) error {
actions := make([]uiauto.Action, 0)
targetsName := make([]string, 0)
for _, detail := range *targets {
if detail.skipDeletion {
continue
}
targetsName = append(targetsName, detail.name)
actions = append(actions, res.filesApp.WaitUntilFileGone(detail.name))
actions = append(actions, waitUntilFileDeleted(detail.name))
}
verifyTargetsAreGone := uiauto.Combine("verify all targets are gone", actions...)
testing.ContextLog(ctx, "Trying to delete targets: ", targetsName)
deleteBtn := nodewith.Name("Delete").HasClass("cr-dialog-ok").Role(role.Button)
return uiauto.Combine(fmt.Sprintf("select and delete targets: %v", targetsName),
res.filesApp.SelectMultipleFiles(res.kb, targetsName...),
res.kb.AccelAction("Alt+Backspace"),
res.filesApp.LeftClick(deleteBtn),
res.filesApp.WaitUntilGone(deleteBtn),
verifyTargetsAreGone,
)(ctx)
}
func cleanUp(ctx context.Context, targets *map[deletionTargetType]*deletionTargetDetail) {
for _, target := range *targets {
if err := os.RemoveAll(target.path); err != nil {
testing.ContextLogf(ctx, "Failed to remove target %q: %v", target.path, err)
}
}
}
// setupFilesAndFolders prepares files and folders.
func setupFilesAndFolders(res *deletionTestResource, targets *map[deletionTargetType]*deletionTargetDetail) error {
// Setup folders.
for _, target := range *targets {
if target.isFolder {
if err := os.MkdirAll(target.path, 0755); err != nil {
return errors.Wrap(err, "failed to create folder")
}
// The folders must be owned by `chronous` to ensure it can be deleted by `filesapp` through UI control.
if err := os.Chown(target.path, int(sysutil.ChronosUID), int(sysutil.ChronosGID)); err != nil {
return errors.Wrapf(err, "failed to chown of folder: %q", target.path)
}
}
}
// Setup files.
for _, target := range *targets {
if !target.isFolder {
if err := fsutil.CopyFile(res.dataPath, target.path); err != nil {
return errors.Wrapf(err, "failed to copy file: %q to: %q", res.dataPath, target.path)
}
}
}
return nil
}