blob: 02517a0424f9c42cd09db2179490985195178330 [file] [log] [blame]
// 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
}