tests: add basic integration tests

Introduce a new osbuild-tests command and ship it in the -tests
sub-package.

The intended usecase is to install the -tests subpackage into an
otherwise pristine VM, and call the osbuild-tests binary over ssh
from the outside of the booted VM. If the binary exits with a return
code of 0, the tests passed, otherwise they failed. The VM should
not be reused after running the tests.

Signed-off-by: Tom Gundersen <teg@jklm.no>
This commit is contained in:
Tom Gundersen 2019-12-21 15:55:03 +01:00
parent fb7fb0156d
commit 02a194f612
2 changed files with 266 additions and 1 deletions

249
cmd/osbuild-tests/main.go Normal file
View file

@ -0,0 +1,249 @@
package main
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"time"
"github.com/BurntSushi/toml"
"github.com/google/uuid"
"github.com/osbuild/osbuild-composer/internal/blueprint"
)
func main() {
// Smoke tests, until functionality tested fully
// that the calls succeed and return valid output
runComposerCLI(false, "compose", "types")
runComposerCLI(false, "compose", "status")
runComposerCLI(false, "compose", "list")
runComposerCLI(false, "compose", "list", "waiting")
runComposerCLI(false, "compose", "list", "running")
runComposerCLI(false, "compose", "list", "finished")
runComposerCLI(false, "compose", "list", "failed")
// runCommand(false, "compose", "log", UUID, [<SIZE>])
// runCommand(false, "compose", "cancel", UUID)
// runCommand(false, "compose", "delete", UUID)
// runCommand(false, "compose", "info", UUID)
// runCommand(false, "compose", "metadata", UUID)
// runCommand(false, "compose", "logs", UUID)
// runCommand(false, "compose", "results", UUID)
// runCommand(false, "compose", "image", UUID)
runComposerCLI(false, "blueprints", "list")
// runCommand(false, "blueprints", "show", BLUEPRINT,....)
// runCommand(false, "blueprints", "changes", BLUEPRINT,....)
// runCommand(false, "blueprints", "diff", BLUEPRINT, FROM/NEWEST, TO/NEWEST/WORKSPACE)
// runCommand(false, "blueprints", "save", BLUEPRINT,...)
// runCommand(false, "blueprints", "delete", BLUEPRINT)
// runCommand(false, "blueprints", "depsolve", BLUEPRINT,...)
// runCommand(false, "blueprints", "push", BLUEPRINT.TOML)
// runCommand(false, "blueprints", "freeze", BLUEPRINT,...)
// runCommand(false, "blueprints", "freeze", "show", BLUEPRINT,...)
// runCommand(false, "blueprints", "freeze", "save", BLUEPRINT,...)
// runCommand(false, "blueprints", "tag", BLUEPRINT)
// runCommand(false, "blueprints", "undo", BLUEPRINT, COMMIT)
// runCommand(false, "blueprints", "workspace", BLUEPRINT)
runComposerCLI(false, "modules", "list")
runComposerCLI(false, "projects", "list")
runComposerCLI(false, "projects", "info", "filesystem")
runComposerCLI(false, "projects", "info", "filesystem", "kernel")
runComposerCLI(false, "sources", "list")
// runCommand(false, "sources", "info", "fedora")
// runCommand(false, "sources", "info", "fedora", "fedora-updates")
// runCommand(false, "sources", "add" SOURCES.TOML)
// runCommand(false, "sources", "change" SOURCES.TOML)
// runCommand(false, "sources", "delete" SOURCES.TOML)
runComposerCLI(false, "status", "show")
// Full integration tests
testCompose("qcow2")
}
func testCompose(outputType string) {
bp := blueprint.Blueprint{
Name: "empty",
Description: "Test empty blueprint in toml format",
}
pushBlueprint(&bp)
defer deleteBlueprint(&bp)
uuid := startCompose("empty", outputType)
defer deleteCompose(uuid)
status := waitForCompose(uuid)
if status != "FINISHED" {
log.Fatalf("Unexpected compose result: %s", status)
}
}
func startCompose(name, outputType string) uuid.UUID {
var reply struct {
BuildID uuid.UUID `json:"build_id"`
Status bool `json:"status"`
}
rawReply := runComposerCLI(false, "compose", "start", name, outputType)
err := json.Unmarshal(rawReply, &reply)
if err != nil {
log.Fatalf("Unexpected reply: " + err.Error())
}
if reply.Status != true {
log.Fatalf("Unexpected status %v", reply.Status)
}
return reply.BuildID
}
func deleteCompose(id uuid.UUID) {
type deleteUUID struct {
ID uuid.UUID `json:"uuid"`
Status bool `json:"status"`
}
var reply struct {
IDs []deleteUUID `json:"uuids"`
Errors []interface{} `json:"errors"`
}
rawReply := runComposerCLI(false, "compose", "delete", id.String())
err := json.Unmarshal(rawReply, &reply)
if err != nil {
log.Fatalf("Unexpected reply: " + err.Error())
}
if len(reply.Errors) != 0 {
log.Fatalf("Unexpected errors")
}
if len(reply.IDs) != 1 {
log.Fatalf("Unexpected number of UUIDs returned: %d", len(reply.IDs))
}
if reply.IDs[0].Status != true {
log.Fatalf("Unexpected status %v", reply.IDs[0].Status)
}
}
func waitForCompose(uuid uuid.UUID) string {
for {
status := getComposeStatus(true, uuid)
if status == "FINISHED" || status == "FAILED" {
return status
}
time.Sleep(time.Second)
}
}
func getComposeStatus(quiet bool, uuid uuid.UUID) string {
var reply struct {
QueueStatus string `json:"queue_status"`
}
rawReply := runComposerCLI(quiet, "compose", "info", uuid.String())
err := json.Unmarshal(rawReply, &reply)
if err != nil {
log.Fatalf("Unexpected reply: " + err.Error())
}
return reply.QueueStatus
}
func pushBlueprint(bp *blueprint.Blueprint) {
tmpfile, err := ioutil.TempFile("", "osbuild-test-")
if err != nil {
log.Fatalf("Could not create temporary file: " + err.Error())
}
defer os.Remove(tmpfile.Name())
err = toml.NewEncoder(tmpfile).Encode(bp)
if err != nil {
log.Fatalf("Could not marshapl blueprint TOML: " + err.Error())
}
if err := tmpfile.Close(); err != nil {
log.Fatalf("Could not close toml file: " + err.Error())
}
var reply struct {
Status bool `json:"status"`
}
rawReply := runComposerCLI(false, "blueprints", "push", tmpfile.Name())
err = json.Unmarshal(rawReply, &reply)
if err != nil {
log.Fatalf("Unexpected reply: " + err.Error())
}
if reply.Status != true {
log.Fatalf("Unexpected status %v", reply.Status)
}
}
func deleteBlueprint(bp *blueprint.Blueprint) {
var reply struct {
Status bool `json:"status"`
}
rawReply := runComposerCLI(false, "blueprints", "delete", bp.Name)
err := json.Unmarshal(rawReply, &reply)
if err != nil {
log.Fatalf("Unexpected reply: " + err.Error())
}
if reply.Status != true {
log.Fatalf("Unexpected status %v", reply.Status)
}
}
func runComposerCLI(quiet bool, command ...string) json.RawMessage {
command = append([]string{"--json"}, command...)
cmd := exec.Command("composer-cli", command...)
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatalf("Colud not create command: " + err.Error())
}
if !quiet {
log.Printf("$ composer-cli %s\n", strings.Join(command, " "))
}
cmd.Start()
var result json.RawMessage
contents, err := ioutil.ReadAll(stdout)
if err != nil {
log.Fatalf("Could not read stdout from command: " + err.Error())
}
if len(contents) != 0 {
err = json.Unmarshal(contents, &result)
if err != nil {
// We did not get JSON, try interpreting it as TOML
var data interface{}
err = toml.Unmarshal(contents, &data)
if err != nil {
log.Fatalf("Could not parse output: %s", err.Error())
}
buffer := bytes.Buffer{}
err = json.NewEncoder(&buffer).Encode(data)
if err != nil {
log.Fatalf("Could not remarshal TOML to JSON: %s", err.Error())
}
err = json.NewDecoder(&buffer).Decode(&result)
if err != nil {
log.Fatalf("Could not decode the remarshalled JSON: %s", err.Error())
}
}
}
err = cmd.Wait()
if err != nil {
log.Fatalf("Command failed: " + err.Error())
}
buffer := bytes.Buffer{}
encoder := json.NewEncoder(&buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(result)
if err != nil {
log.Fatalf("Could not remarshal the recevied JSON: " + err.Error())
}
if !quiet {
log.Printf("Return:\n%s\n", buffer.String())
}
return result
}

View file

@ -64,12 +64,17 @@ export GOFLAGS=-mod=vendor
%endif
%gobuild -o _bin/osbuild-composer %{goipath}/cmd/osbuild-composer
%gobuild -o _bin/osbuild-worker %{goipath}/cmd/osbuild-worker
%gobuild -o _bin/osbuild-tests %{goipath}/cmd/osbuild-tests
%install
install -m 0755 -vd %{buildroot}%{_libexecdir}/osbuild-composer
install -m 0755 -vp _bin/* %{buildroot}%{_libexecdir}/osbuild-composer/
install -m 0755 -vp _bin/osbuild-composer %{buildroot}%{_libexecdir}/osbuild-composer/
install -m 0755 -vp _bin/osbuild-worker %{buildroot}%{_libexecdir}/osbuild-composer/
install -m 0755 -vp dnf-json %{buildroot}%{_libexecdir}/osbuild-composer/
install -m 0755 -vd %{buildroot}%{_libexecdir}/tests/osbuild-composer
install -m 0755 -vp _bin/osbuild-tests %{buildroot}%{_libexecdir}/tests/osbuild-composer/
install -m 0755 -vd %{buildroot}%{_datadir}/osbuild-composer/repositories
install -m 0644 -vp repositories/* %{buildroot}%{_datadir}/osbuild-composer/repositories/
@ -107,6 +112,17 @@ export GOPATH=$PWD/_build:%{gopath}
%{_unitdir}/*.{service,socket}
%{_sysusersdir}/osbuild-composer.conf
%package tests
Summary: Integration tests
Requires: osbuild-composer
Requires: composer-cli
%description tests
Integration tests to be run on a pristine-dedicated system to test the osbuild-composer package.
%files tests
%{_libexecdir}/tests/osbuild-composer/osbuild-tests
%changelog
* Sun Dec 1 11:00:00 CEST 2019 Ondrej Budai <obudai@redhat.com> - 4-1
- New upstream release.