Merge pull request #42 from OpenCHAMI/tests

Added initial tests for API and compatibility coverage
This commit is contained in:
David Allen 2024-09-26 11:19:44 -06:00 committed by GitHub
commit 86e9ce03ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 379 additions and 65 deletions

2
.gitignore vendored
View file

@ -4,3 +4,5 @@ emulator/rf-emulator
**.tar.gz
**.tar.zst
**.part
dist/*
**coverage.out**

View file

@ -52,10 +52,10 @@ inst: ## go install tools
$(call print-target)
go install github.com/client9/misspell/cmd/misspell@v0.3.4
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2
go install github.com/goreleaser/goreleaser@v1.18.2
go install github.com/goreleaser/goreleaser/v2@v2.3.2
go install github.com/cpuguy83/go-md2man/v2@latest
.PHONY: goreleaser
.PHONY: release
release: ## goreleaser build
$(call print-target)
$(GOPATH)/bin/goreleaser build --clean --single-target --snapshot
@ -83,7 +83,9 @@ lint: ## golangci-lint
.PHONY: test
test: ## go test
$(call print-target)
go test -race -covermode=atomic -coverprofile=coverage.out -coverpkg=./... ./...
./emulator/setup.sh &
sleep 10
go test -race -covermode=atomic -coverprofile=coverage.out -coverpkg=./... tests/api_test.go tests/compatibility_test.go
go tool cover -html=coverage.out -o coverage.html
.PHONY: diff

1
go.mod
View file

@ -22,7 +22,6 @@ require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
)
require (

27
internal/util/util.go Normal file
View file

@ -0,0 +1,27 @@
package util
import (
"fmt"
"time"
)
// CheckUntil regularly check a predicate until it's true or time out is reached.
func CheckUntil(interval time.Duration, timeout time.Duration, predicate func() (bool, error)) error {
timeoutCh := time.After(timeout)
for {
select {
case <-time.After(interval):
predTrue, err := predicate()
if predTrue {
return nil
}
if err != nil {
return err
}
case <-timeoutCh:
return fmt.Errorf("timeout of %ds reached", int64(timeout/time.Second))
}
}
}

View file

@ -8,59 +8,189 @@
package tests
import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"flag"
magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/util"
"github.com/OpenCHAMI/magellan/pkg/client"
"github.com/rs/zerolog/log"
)
var (
scanParams = &magellan.ScanParams{
TargetHosts: [][]string{
[]string{
"http://127.0.0.1:443",
"http://127.0.0.1:5000",
},
},
Scheme: "https",
Protocol: "tcp",
Concurrency: 1,
Timeout: 30,
DisableProbing: false,
Verbose: false,
}
exePath = flag.String("exe", "../magellan", "path to 'magellan' binary executable")
emuPath = flag.String("emu", "./emulator/setup.sh", "path to emulator 'setup.sh' script")
)
func TestScanAndCollect(t *testing.T) {
// do a scan on the emulator cluster with probing disabled and check results
results := magellan.ScanForAssets(scanParams)
if len(results) <= 0 {
t.Fatal("expected to find at least one BMC node, but found none")
}
// do a scan on the emulator cluster with probing enabled
results = magellan.ScanForAssets(scanParams)
if len(results) <= 0 {
t.Fatal("expected to find at least one BMC node, but found none")
var (
err error
// tempDir = t.TempDir()
path string
command []string
cwd string
cmd *exec.Cmd
bufout bytes.Buffer
buferr bytes.Buffer
)
// set up the emulator to run before test
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
// do a collect on the emulator cluster to collect Redfish info
err := magellan.CollectInventory(&results, &magellan.CollectParams{})
// get the current working directory and print
cwd, err = os.Getwd()
if err != nil {
log.Error().Err(err).Msg("failed to collect inventory")
t.Fatalf("failed to get working directory: %v", err)
}
fmt.Printf("cwd: %s\n", cwd)
// path, err := exec.LookPath("dexdump")
// if err != nil {
// log.Fatal(err)
// }
// try and run a "scan" with the emulator
// set up the emulator to run before test
path, err = filepath.Abs(*exePath)
if err != nil {
t.Fatalf("failed to get absolute path: %v", err)
}
command = strings.Split("scan https://127.0.0.1 --port 5000 --verbose", " ")
cmd = exec.Command(path, command...)
cmd.Stdout = &bufout
cmd.Stderr = &buferr
err = cmd.Run()
fmt.Printf("out:\n%s\nerr:\n%s\n", bufout.String(), buferr.String())
if err != nil {
t.Fatalf("failed to run 'scan' command: %v", err)
}
// make sure that the expected output is not empty
if len(buferr.Bytes()) <= 0 {
t.Fatalf("expected the 'scan' output to not be empty")
}
// try and run a "collect" with the emulator
command = strings.Split("collect --username root --password root_password --verbose", " ")
cmd = exec.Command(path, command...)
cmd.Stdout = &bufout
cmd.Stderr = &buferr
err = cmd.Run()
fmt.Printf("out:\n%s\nerr:\n%s\n", bufout.String(), buferr.String())
if err != nil {
t.Fatalf("failed to run 'collect' command: %v", err)
}
// make sure that the output is not empty
if len(bufout.Bytes()) <= 0 {
t.Fatalf("expected the 'collect' output to not be empty")
}
// TODO: check for at least one System/EthernetInterface that we know should exist
}
func TestCrawlCommand(t *testing.T) {
// TODO: add test to check the crawl command's behavior
var (
err error
command []string
cmd *exec.Cmd
bufout bytes.Buffer
buferr bytes.Buffer
path string
)
// set up the emulator to run before test
path, err = filepath.Abs(*exePath)
if err != nil {
t.Fatalf("failed to get absolute path: %v", err)
}
fmt.Printf("path: %s\n", path)
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
// try and run a "collect" with the emulator
command = strings.Split("crawl --username root --password root_password -i https://127.0.0.1:5000", " ")
cmd = exec.Command(path, command...)
cmd.Stdout = &bufout
cmd.Stderr = &buferr
err = cmd.Run()
fmt.Printf("out:\n%s\nerr:\n%s\n", bufout.String(), buferr.String())
if err != nil {
t.Fatalf("failed to run 'crawl' command: %v", err)
}
// err = cmd.Wait()
// if err != nil {
// t.Fatalf("failed to call 'wait' for crawl: %v", err)
// }
// make sure that the output is not empty
if len(bufout.Bytes()) <= 0 {
t.Fatalf("expected the 'crawl' output to not be empty")
}
}
func TestListCommand(t *testing.T) {
// TODO: add test to check the list command's output
var (
err error
cmd *exec.Cmd
)
// set up the emulator to run before test
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
// set up temporary directory
cmd = exec.Command("bash", "-c", fmt.Sprintf("%s list", *exePath))
err = cmd.Start()
if err != nil {
t.Fatalf("failed to run 'list' command: %v", err)
}
// NOTE: the output of `list` can be empty if no scan has been performed
}
func TestUpdateCommand(t *testing.T) {
// TODO: add test that does a Redfish simple update checking it success and
// failure points
var (
cmd *exec.Cmd
err error
)
// set up the emulator to run before test
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
// set up temporary directory
cmd = exec.Command("bash", "-c", fmt.Sprintf("%s update", *exePath))
err = cmd.Start()
if err != nil {
t.Fatalf("failed to run 'update' command: %v", err)
}
}
func TestGofishFunctions(t *testing.T) {
@ -68,6 +198,115 @@ func TestGofishFunctions(t *testing.T) {
// gofish's output isn't changing spontaneously and remains predictable
}
// TestGenerateHosts() tests creating a collection of hosts by changing arguments
// and calling GenerateHostsWithSubnet().
func TestGenerateHosts(t *testing.T) {
// TODO: add test to generate hosts using a collection of subnets/masks
var (
subnet = "127.0.0.1"
subnetMask = &net.IPMask{255, 255, 255, 0}
ports = []int{443}
scheme = "https"
hosts = [][]string{}
)
t.Run("generate-hosts", func(t *testing.T) {
hosts = magellan.GenerateHostsWithSubnet(subnet, subnetMask, ports, scheme)
// check for at least one host to be generated
if len(hosts) <= 0 {
t.Fatalf("expected at least one host to be generated for subnet %s", subnet)
}
})
t.Run("generate-hosts-with-multiple-ports", func(t *testing.T) {
ports = []int{443, 5000}
hosts = magellan.GenerateHostsWithSubnet(subnet, subnetMask, ports, scheme)
// check for at least one host to be generated
if len(hosts) <= 0 {
t.Fatalf("expected at least one host to be generated for subnet %s", subnet)
}
})
t.Run("generate-hosts-with-subnet-mask", func(t *testing.T) {
subnetMask = &net.IPMask{255, 255, 0, 0}
hosts = magellan.GenerateHostsWithSubnet(subnet, subnetMask, ports, scheme)
// check for at least one host to be generated
if len(hosts) <= 0 {
t.Fatalf("expected at least one host to be generated for subnet %s", subnet)
}
})
}
func startEmulatorInBackground(path string) (int, error) {
// try and start the emulator in the background if arg passed
var (
cmd *exec.Cmd
err error
)
if path != "" {
cmd = exec.Command("bash", "-c", path)
err = cmd.Start()
if err != nil {
return -1, fmt.Errorf("failed while executing emulator startup script: %v", err)
}
} else {
return -1, fmt.Errorf("path to emulator start up script is required")
}
return cmd.Process.Pid, nil
}
// waitUntilEmulatorIsReady() polls with
func waitUntilEmulatorIsReady() error {
var (
interval = time.Second * 2
timeout = time.Second * 6
testClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
body client.HTTPBody
header client.HTTPHeader
err error
)
err = util.CheckUntil(interval, timeout, func() (bool, error) {
// send request to host until we get expected response
res, _, err := client.MakeRequest(testClient, "https://127.0.0.1:5000/redfish/v1/", http.MethodGet, body, header)
if err != nil {
return false, fmt.Errorf("failed to make request to emulator: %w", err)
}
if res == nil {
return false, fmt.Errorf("invalid response from emulator (response is nil)")
}
if res.StatusCode == http.StatusOK {
return true, nil
} else {
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
}
})
return err
}
func init() {
var (
cwd string
err error
)
// get the current working directory
cwd, err = os.Getwd()
if err != nil {
log.Error().Err(err).Msg("failed to get working directory")
}
fmt.Printf("cwd: %s\n", cwd)
// start emulator in the background before running tests
pid, err := startEmulatorInBackground(*emuPath)
if err != nil {
log.Error().Err(err).Msg("failed to start emulator in background")
os.Exit(1)
}
_ = pid
}

View file

@ -7,6 +7,7 @@
package tests
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
@ -15,56 +16,97 @@ import (
"github.com/OpenCHAMI/magellan/pkg/client"
"github.com/OpenCHAMI/magellan/pkg/crawler"
"github.com/rs/zerolog/log"
)
var (
host = flag.String("host", "localhost", "set the BMC host")
username = flag.String("username", "", "set the BMC username used for the tests")
password = flag.String("password", "", "set the BMC password used for the tests")
host = flag.String("host", "https://127.0.0.1:5000", "set the BMC host")
username = flag.String("username", "root", "set the BMC username used for the tests")
password = flag.String("password", "root_password", "set the BMC password used for the tests")
)
// Simple test to fetch the base Redfish URL and assert a 200 OK response.
func TestRedfishV1Availability(t *testing.T) {
var (
url = fmt.Sprintf("%s/redfish/v1", *host)
body = []byte{}
headers = map[string]string{}
)
res, b, err := client.MakeRequest(nil, url, http.MethodGet, body, headers)
if err != nil {
t.Fatalf("failed to make request to BMC: %v", err)
}
func checkResponse(res *http.Response, b []byte) error {
// test for a 200 response code here
if res.StatusCode != http.StatusOK {
t.Fatalf("expected response code to return status code 200")
return fmt.Errorf("expected response code to return status code 200")
}
// make sure the response body is not empty
if len(b) <= 0 {
t.Fatalf("expected response body to not be empty")
return fmt.Errorf("expected response body to not be empty")
}
// make sure the response body is in a JSON format
if json.Valid(b) {
t.Fatalf("expected response body to be valid JSON")
// make sure the response body is in a valid JSON format
if !json.Valid(b) {
return fmt.Errorf("expected response body to be valid JSON")
}
return nil
}
// Simple test to fetch the base Redfish URL and assert a 200 OK response.
func TestRedfishV1ServiceRootAvailability(t *testing.T) {
var (
url = fmt.Sprintf("%s/redfish/v1/", *host)
body = []byte{}
headers = map[string]string{}
testClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
err error
)
// set up the emulator to run before test
err = waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
res, b, err := client.MakeRequest(testClient, url, http.MethodGet, body, headers)
if err != nil {
t.Fatalf("failed to make request to BMC node: %v", err)
}
err = checkResponse(res, b)
if err != nil {
t.Fatalf("failed to check response for redfish service root: %v", err)
}
}
// Simple test to ensure an expected Redfish version minimum requirement.
func TestRedfishVersion(t *testing.T) {
func TestRedfishV1Version(t *testing.T) {
var (
url string = fmt.Sprintf("%s/redfish/v1", *host)
body client.HTTPBody = []byte{}
headers client.HTTPHeader = map[string]string{}
err error
url string = fmt.Sprintf("%s/redfish/v1/", *host)
body client.HTTPBody = []byte{}
headers client.HTTPHeader = map[string]string{}
testClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
root map[string]any
err error
)
_, _, err = client.MakeRequest(nil, url, http.MethodGet, body, headers)
res, b, err := client.MakeRequest(testClient, url, http.MethodGet, body, headers)
if err != nil {
log.Error().Err(err).Msg("failed to make request")
t.Fatalf("failed to make request to BMC node: %v", err)
}
err = checkResponse(res, b)
if err != nil {
t.Fatalf("failed to check response for redfish version: %v", err)
}
// check the "RedfishVersion" from service root
err = json.Unmarshal(b, &root)
if err != nil {
t.Fatalf("failed to unmarshal redfish response: %v", err)
}
_, ok := root["RedfishVersion"]
if !ok {
t.Fatalf("failed to get 'RedfishVersion' from service root")
}
}
@ -72,12 +114,18 @@ func TestRedfishVersion(t *testing.T) {
// that we need for Magellan to run correctly. This test differs from the
// `TestCrawlCommand` testing function as it is not checking specifically
// for functionality.
func TestExpectedProperties(t *testing.T) {
func TestExpectedOutput(t *testing.T) {
// make sure what have a valid host
if host == nil {
t.Fatal("invalid host (host is nil)")
}
// set up the emulator to run before test
err := waitUntilEmulatorIsReady()
if err != nil {
t.Fatalf("failed while waiting for emulator: %v", err)
}
systems, err := crawler.CrawlBMC(
crawler.CrawlerConfig{
URI: *host,
@ -106,8 +154,5 @@ func TestExpectedProperties(t *testing.T) {
if len(system.EthernetInterfaces) <= 0 {
t.Errorf("no ethernet interfaces found for system '%s'", system.Name)
}
if len(system.NetworkInterfaces) <= 0 {
t.Errorf("no network interfaces found for system '%s'", system.Name)
}
}
}