From 0dcd16aa36defc27425d26021e9091940177944f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Budai?= Date: Mon, 24 Feb 2020 12:43:47 +0100 Subject: [PATCH] tests: begin rewriting of ./test/run test suite to Go ./test/run test suite has served us well over the last months. However, there is currently a major effort to run the better defined integration test suite on a CI. Nonetheless, two very important parts are still missing from the integration test suite: inspecting the image with image-info and booting the image. This commit begins the work on this matter by porting a part of ./test/run suite to Go. Currently, only image-info tests work, the rest will come in the following commits. --- .../osbuild-image-tests.go | 265 ++++++++++++++++++ golang-github-osbuild-composer.spec | 11 +- 2 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 cmd/osbuild-image-tests/osbuild-image-tests.go diff --git a/cmd/osbuild-image-tests/osbuild-image-tests.go b/cmd/osbuild-image-tests/osbuild-image-tests.go new file mode 100644 index 000000000..e9b563e06 --- /dev/null +++ b/cmd/osbuild-image-tests/osbuild-image-tests.go @@ -0,0 +1,265 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "github.com/google/go-cmp/cmp" + "github.com/osbuild/osbuild-composer/internal/common" + "github.com/osbuild/osbuild-composer/internal/distro" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type testcaseStruct struct { + Compose struct { + Distro string + Arch string + Filename string + } + Pipeline json.RawMessage + ImageInfo json.RawMessage `json:"image-info"` +} + +// runOsbuild runs osbuild with the specified pipeline and store. +func runOsbuild(pipeline []byte, store string) (string, error) { + cmd := exec.Command( + "osbuild", + "--store", store, + "--json", + "-", + ) + + cmd.Stderr = os.Stderr + cmd.Stdin = bytes.NewReader(pipeline) + var outBuffer bytes.Buffer + cmd.Stdout = &outBuffer + + log.Print("[osbuild] running") + err := cmd.Run() + + if err != nil { + log.Print("[osbuild] failed") + if _, ok := err.(*exec.ExitError); ok { + return "", fmt.Errorf("running osbuild failed: %s", outBuffer.String()) + } + return "", fmt.Errorf("running osbuild failed from an unexpected reason: %v", err) + } + + log.Print("[osbuild] succeeded") + + var result struct { + OutputID string `json:"output_id"` + } + + err = json.NewDecoder(&outBuffer).Decode(&result) + if err != nil { + return "", fmt.Errorf("cannot decode osbuild output: %v", err) + } + + return result.OutputID, nil +} + +// extractXZ extracts an xz archive, it's just a simple wrapper around unxz(1). +func extractXZ(archivePath string) error { + cmd := exec.Command("unxz", archivePath) + if err := cmd.Run(); err != nil { + return fmt.Errorf("cannot extract xz archive: %v", err) + } + + return nil +} + +// splitExtension returns a file extension as the second return value and +// the rest as the first return value. +// The functionality should be the same as Python splitext's +func splitExtension(path string) (string, string) { + ex := filepath.Ext(path) + base := strings.TrimSuffix(path, ex) + + return base, ex +} + +// testImageInfo runs image-info on image specified by imageImage and +// compares the result with expected image info +func testImageInfo(imagePath string, rawImageInfoExpected []byte) error { + var imageInfoExpected interface{} + err := json.Unmarshal(rawImageInfoExpected, &imageInfoExpected) + if err != nil { + return fmt.Errorf("cannot decode expected image info: %v", err) + } + + cmd := exec.Command("/usr/libexec/osbuild-composer/image-info", imagePath) + cmd.Stderr = os.Stderr + reader, writer := io.Pipe() + cmd.Stdout = writer + + err = cmd.Start() + if err != nil { + return fmt.Errorf("image-info cannot start: %v", err) + } + + var imageInfoGot interface{} + err = json.NewDecoder(reader).Decode(&imageInfoGot) + if err != nil { + return fmt.Errorf("decoding image-info output failed: %v", err) + } + + err = cmd.Wait() + if err != nil { + return fmt.Errorf("running image-info failed: %v", err) + } + + if diff := cmp.Diff(imageInfoExpected, imageInfoGot); diff != "" { + return fmt.Errorf("image info differs:\n%s", diff) + } + + return nil +} + +// testImage performs a series of tests specified in the testcase +// on an image +func testImage(testcase testcaseStruct, imagePath string) error { + if testcase.ImageInfo != nil { + log.Print("[image info sub-test] running") + err := testImageInfo(imagePath, testcase.ImageInfo) + if err != nil { + log.Print("[image info sub-test] failed") + return err + } + log.Print("[image info sub-test] succeeded") + } else { + log.Print("[image info sub-test] not defined, skipping") + } + + return nil +} + +// runTestcase builds the pipeline specified in the testcase and then it +// tests the result +func runTestcase(testcase testcaseStruct) error { + store, err := ioutil.TempDir("/var/tmp", "osbuild-image-tests-") + if err != nil { + return fmt.Errorf("cannot create temporary store: %v", err) + } + defer func() { + err := os.RemoveAll(store) + if err != nil { + log.Printf("cannot remove temporary store: %v\n", err) + } + }() + + outputID, err := runOsbuild(testcase.Pipeline, store) + if err != nil { + return err + } + + imagePath := fmt.Sprintf("%s/refs/%s/%s", store, outputID, testcase.Compose.Filename) + + // if the result is xz archive, extract it + base, ex := splitExtension(imagePath) + if ex == ".xz" { + if err := extractXZ(imagePath); err != nil { + return err + } + imagePath = base + } + + return testImage(testcase, imagePath) +} + +// getAllCases returns paths to all testcases in the testcase directory +func getAllCases() ([]string, error) { + const casesDirectory = "/usr/share/tests/osbuild-composer/cases" + cases, err := ioutil.ReadDir(casesDirectory) + if err != nil { + return nil, fmt.Errorf("cannot list test cases: %v", err) + } + + casesPaths := []string{} + for _, c := range cases { + if c.IsDir() { + continue + } + + casePath := fmt.Sprintf("%s/%s", casesDirectory, c.Name()) + casesPaths = append(casesPaths, casePath) + } + + return casesPaths, nil +} + +// runTests opens, parses and runs all the specified testcases +func runTests(cases []string) error { + for _, path := range cases { + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("%s: cannot open test case: %v", path, err) + } + + var testcase testcaseStruct + err = json.NewDecoder(f).Decode(&testcase) + if err != nil { + return fmt.Errorf("%s: cannot decode test case: %v", path, err) + } + + currentArch := common.CurrentArch() + if testcase.Compose.Arch != currentArch { + log.Printf("%s: skipping, the required arch is %s, the current arch is %s", path, testcase.Compose.Arch, currentArch) + continue + } + + hostDistroName, err := distro.GetHostDistroName() + if err != nil { + return fmt.Errorf("cannot get host distro name: %v", err) + } + + // TODO: forge distro name for now + if strings.HasPrefix(hostDistroName, "fedora") { + hostDistroName = "fedora-30" + } + + if testcase.Compose.Distro != hostDistroName { + log.Printf("%s: skipping, the required distro is %s, the host distro is %s", path, testcase.Compose.Distro, hostDistroName) + continue + } + + log.Printf("%s: RUNNING", path) + + err = runTestcase(testcase) + if err != nil { + log.Printf("%s: FAILURE\nReason: %v", path, err) + } else { + log.Printf("%s: SUCCESS", path) + } + + } + + return nil +} + +func main() { + flag.Parse() + cases := flag.Args() + + // if no cases were specified, run the default set + if len(cases) == 0 { + var err error + cases, err = getAllCases() + if err != nil { + log.Fatalf("searching for testcases failed: %v", err) + } + } + + err := runTests(cases) + + if err != nil { + log.Fatalf("error occured while running tests: %v", err) + } +} diff --git a/golang-github-osbuild-composer.spec b/golang-github-osbuild-composer.spec index 505e30b4c..0a0dd2870 100644 --- a/golang-github-osbuild-composer.spec +++ b/golang-github-osbuild-composer.spec @@ -68,6 +68,7 @@ export GOFLAGS=-mod=vendor %gobuild -o _bin/osbuild-worker %{goipath}/cmd/osbuild-worker %gobuild -o _bin/osbuild-tests %{goipath}/cmd/osbuild-tests %gobuild -o _bin/osbuild-dnf-json-tests %{goipath}/cmd/osbuild-dnf-json-tests +%gobuild -o _bin/osbuild-image-tests %{goipath}/cmd/osbuild-image-tests %install install -m 0755 -vd %{buildroot}%{_libexecdir}/osbuild-composer @@ -78,10 +79,15 @@ install -m 0755 -vp dnf-json %{buildroot}%{_libex install -m 0755 -vd %{buildroot}%{_libexecdir}/tests/osbuild-composer install -m 0755 -vp _bin/osbuild-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ install -m 0755 -vp _bin/osbuild-dnf-json-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ +install -m 0755 -vp _bin/osbuild-image-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/ +install -m 0755 -vp tools/image-info %{buildroot}%{_libexecdir}/osbuild-composer/ install -m 0755 -vd %{buildroot}%{_datadir}/osbuild-composer/repositories install -m 0644 -vp repositories/* %{buildroot}%{_datadir}/osbuild-composer/repositories/ +install -m 0755 -vd %{buildroot}%{_datadir}/tests/osbuild-composer/cases +install -m 0644 -vp test/cases/* %{buildroot}%{_datadir}/tests/osbuild-composer/cases/ + install -m 0755 -vd %{buildroot}%{_unitdir} install -m 0644 -vp distribution/*.{service,socket} %{buildroot}%{_unitdir}/ @@ -141,8 +147,9 @@ Requires: createrepo_c Integration tests to be run on a pristine-dedicated system to test the osbuild-composer package. %files tests -%{_libexecdir}/tests/osbuild-composer/osbuild-tests -%{_libexecdir}/tests/osbuild-composer/osbuild-dnf-json-tests +%{_libexecdir}/tests/osbuild-composer/* +%{_datadir}/tests/osbuild-composer/* +%{_libexecdir}/osbuild-composer/image-info %package worker Summary: The worker for osbuild-composer