| // Copyright 2018 The Fuchsia 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 testexec |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| |
| "fuchsia.googlesource.com/infra/infra/fuchsia" |
| ) |
| |
| // TestSpec is the specification for a single test and the environments it |
| // should be executed in. |
| type TestSpec struct { |
| // Test is the test that this specuration is for. |
| Test `json:"test"` |
| |
| // Envs is a set of environments that the test should be executed in. |
| Envs []Environment `json:"environments"` |
| } |
| |
| // Validate performs basic schema validation on the TestSpec and returns |
| // an error if validation fails. |
| |
| // |
| // This should be performed after decoding and before usage to ensure the |
| // invariants of the structure are upheld. |
| // |
| // TODO(mknyszek): Consider making this an actual JSON schema and validate against that. |
| // TODO(mknyszek): Don't validate platform policy here once we have it implemented |
| // for the input to this tool. |
| func (c *TestSpec) Validate(testArch string, platforms TestDevicePlatforms) error { |
| for _, env := range c.Envs { |
| if err := env.Validate(testArch, platforms); err != nil { |
| return err |
| } |
| } |
| return c.Test.Validate() |
| } |
| |
| // Test encapsulates details about a particular test. |
| type Test struct { |
| // Location must be a reference to a test. One example of a reference to a test |
| // is a filesystem path. Another could be a Fuchsia URI. |
| // This is a required field. |
| Location string `json:"location"` |
| } |
| |
| // Validate performs basic schema validation on the Test. |
| // |
| // This should be performed after decoding and before usage to ensure the |
| // invariants of the structure are upheld. |
| func (t *Test) Validate() error { |
| if t.Location == "" { |
| return fmt.Errorf("Test must have non-empty location") |
| } |
| return nil |
| } |
| |
| // Environment describes the full environment a test requires. |
| // |
| // This type must contain only types that are allowed to be used as map keys (i.e. |
| // all types must be comparable, https://golang.org/ref/spec#Comparison_operators). |
| type Environment struct { |
| // Device represents properties of the device that are part of the test |
| // environment. |
| Device DeviceSpec `json:"device"` |
| } |
| |
| // Validate performs basic schema validation on the Environment. |
| func (e *Environment) Validate(testArch string, platforms TestDevicePlatforms) error { |
| return e.Device.Validate(testArch, platforms) |
| } |
| |
| // DeviceSpec describes the device environment a test requires. |
| // |
| // This type must contain only types that are allowed to be used as map keys (i.e. |
| // all types must be comparable, https://golang.org/ref/spec#Comparison_operators). |
| type DeviceSpec struct { |
| // Type represents the class of device the test should run on. |
| // This is a required field. |
| Type string `json:"type"` |
| } |
| |
| // Validate performs basic schema validation on the DeviceSpec. |
| // |
| // This should be performed after decoding and before usage to ensure the |
| // invariants of the structure are upheld. |
| func (ds *DeviceSpec) Validate(testArch string, platforms TestDevicePlatforms) error { |
| platform, ok := platforms.Get(ds.Type) |
| if !ok { |
| return fmt.Errorf("unknown device type %q", ds.Type) |
| } |
| if platform.Arch != "*" && testArch != platform.Arch { |
| return fmt.Errorf("invalid arch %q for device type %q found", testArch, ds.Type) |
| } |
| return nil |
| } |
| |
| // HasTestSpecExt returns true if the given path ends with .spec.json. |
| func HasTestSpecExt(path string) bool { |
| if first := filepath.Ext(path); first != ".json" { |
| return false |
| } |
| if second := filepath.Ext(path[:len(path)-len(".json")]); second != ".spec" { |
| return false |
| } |
| return true |
| } |
| |
| // TODO(IN-673): Add unit tests for the following function. |
| // LoadTestSpecs loads a set of test specifications from a manifest of Fuchsia packages |
| // produced by a Fuchsia build. |
| func LoadTestSpecs(fuchsiaBuildDir string) ([]TestSpec, error) { |
| manifestPath := filepath.Join(fuchsiaBuildDir, fuchsia.PackagesManifestFilename) |
| |
| // Read the package manifest produced by the build to find where packages |
| // live in the build directory. |
| manifestFile, err := os.Open(manifestPath) |
| if err != nil { |
| return nil, fmt.Errorf("failed to open packages manifest %s: %s", manifestPath, err.Error()) |
| } |
| defer manifestFile.Close() |
| var manifest fuchsia.PackagesManifest |
| if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil { |
| return nil, fmt.Errorf("failed to parse package manifest %s: %s", manifestPath, err.Error()) |
| } |
| |
| // Use the packages manifest to discover test specifications. |
| // |
| // It is guaranteed that a test spec will be written to the build directory of the |
| // corresponding package its test was defined in: specifically, it will be put in |
| // <target_out_dir of the test package>/<test package name>. |
| // This algorithm iterates over these directories for each package in the package |
| // manifest. |
| specs := make([]TestSpec, 0, len(manifest.Packages)) |
| for _, pkg := range manifest.Packages { |
| testSpecDir := filepath.Join(fuchsiaBuildDir, pkg.BuildDir, pkg.Name) |
| |
| // If the associated test spec directory does not exist, the package specified no |
| // tests. |
| if _, err := os.Stat(testSpecDir); os.IsNotExist(err) { |
| continue |
| } |
| |
| // Non-recursively enumerate the files in this directory; it's guaranteed that |
| // the test specs will be found here if generated. |
| entries, err := ioutil.ReadDir(testSpecDir) |
| if err != nil { |
| return nil, err |
| } |
| |
| for _, entry := range entries { |
| path := filepath.Join(testSpecDir, entry.Name()) |
| |
| // Skip any directories or files without a .spec.json extension. |
| // At this time, only test specs are generated in this subdirectory. |
| if entry.IsDir() || !HasTestSpecExt(path) { |
| continue |
| } |
| |
| // Open, read, and parse the test spec. If at any point we fail from here, |
| // fail the whole algorithm, since either the file is malformed which |
| // suggests larger problems (e.g. a file from the build is impersonating |
| // a test spec accidentally). |
| f, err := os.Open(path) |
| if err != nil { |
| return nil, fmt.Errorf("failed to open %s: %s", path, err.Error()) |
| } |
| defer f.Close() |
| |
| var spec TestSpec |
| if err := json.NewDecoder(f).Decode(&spec); err != nil { |
| return nil, fmt.Errorf("failed to decode %s: %s", path, err.Error()) |
| } |
| specs = append(specs, spec) |
| } |
| } |
| return specs, nil |
| } |