diff --git a/cnf-certification-test/suite_test.go b/cnf-certification-test/suite_test.go
index 22a900793..60b2325c4 100644
--- a/cnf-certification-test/suite_test.go
+++ b/cnf-certification-test/suite_test.go
@@ -17,10 +17,17 @@
package suite
import (
+ "bytes"
_ "embed"
+ "encoding/json"
"flag"
+ "fmt"
+ "io"
+ rlog "log"
+ "net/http"
"os"
"path/filepath"
+ "strings"
"testing"
"time"
@@ -45,6 +52,7 @@ import (
_ "github.com/test-network-function/cnf-certification-test/cnf-certification-test/performance"
_ "github.com/test-network-function/cnf-certification-test/cnf-certification-test/platform"
_ "github.com/test-network-function/cnf-certification-test/cnf-certification-test/preflight"
+ "github.com/test-network-function/cnf-certification-test/cnf-certification-test/webserver"
"github.com/test-network-function/cnf-certification-test/internal/clientsholder"
"github.com/test-network-function/cnf-certification-test/pkg/configuration"
"github.com/test-network-function/cnf-certification-test/pkg/diagnostics"
@@ -56,8 +64,10 @@ const (
defaultClaimPath = ".."
defaultCliArgValue = ""
junitFlagKey = "junit"
+ serverModeFlag = "serverMode"
TNFReportKey = "cnf-certification-test"
extraInfoKey = "testsExtraInfo"
+ defaultServerMode = false
)
var (
@@ -77,6 +87,7 @@ var (
// ClaimFormat is the current version for the claim file format to be produced by the TNF test suite.
// A client decoding this claim file must support decoding its specific version.
ClaimFormatVersion string
+ serverMode *bool
)
func init() {
@@ -84,6 +95,7 @@ func init() {
"the path where the claimfile will be output")
junitPath = flag.String(junitFlagKey, defaultCliArgValue,
"the path for the junit format report")
+ serverMode = flag.Bool("serverMode", defaultServerMode, "run test with webserver")
}
// setLogLevel sets the log level for logrus based on the "TNF_LOG_LEVEL" environment variable
@@ -126,8 +138,7 @@ func getGitVersion() string {
return gitDisplayRelease + " ( " + GitCommit + " )"
}
-// TestTest invokes the CNF Certification Test Suite.
-func TestTest(t *testing.T) {
+func PreRun(t *testing.T) (claimData *claim.Claim, claimRoot *claim.Root) {
// When running unit tests, skip the suite
if os.Getenv("UNIT_TEST") != "" {
t.Skip("Skipping test suite when running unit tests")
@@ -147,20 +158,13 @@ func TestTest(t *testing.T) {
log.Infof("Claim Format Version: %s", ClaimFormatVersion)
log.Infof("Ginkgo Version : %v", ginkgo.GINKGO_VERSION)
log.Infof("Labels filter : %v", ginkgoConfig.LabelFilter)
-
- // Diagnostic functions will run when no labels are provided.
- var diagnosticMode bool
- if ginkgoConfig.LabelFilter == "" {
- log.Infof("TNF will run in diagnostic mode so no test case will be launched.")
- diagnosticMode = true
- }
-
+ log.Infof("run test with webserver : %v", *serverMode)
// Set clientsholder singleton with the filenames from the env vars.
_ = clientsholder.GetClientsHolder(getK8sClientsConfigFileNames()...)
// Initialize the claim with the start time, tnf version, etc.
- claimRoot := claimhelper.CreateClaimRoot()
- claimData := claimRoot.Claim
+ claimRoot = claimhelper.CreateClaimRoot()
+ claimData = claimRoot.Claim
claimData.Configurations = make(map[string]interface{})
claimData.Nodes = make(map[string]interface{})
incorporateVersions(claimData)
@@ -170,21 +174,42 @@ func TestTest(t *testing.T) {
log.Errorf("Configuration node missing because of: %s", err)
t.FailNow()
}
-
claimData.Nodes = claimhelper.GenerateNodes()
claimhelper.UnmarshalConfigurations(configurations, claimData.Configurations)
+ return claimData, claimRoot
+}
+
+// TestTest invokes the CNF Certification Test Suite.
+func TestTest(t *testing.T) {
+ ginkgoConfig, _ := ginkgo.GinkgoConfiguration()
+ if !*serverMode {
+ claimData, claimRoot := PreRun(t)
+ var diagnosticMode bool
+ // Diagnostic functions will run when no labels are provided.
+
+ if ginkgoConfig.LabelFilter == "" {
+ log.Infof("TNF will run in diagnostic mode so no test case will be launched.")
+ diagnosticMode = true
+ }
- // initialize abort flag
- testhelper.AbortTrigger = ""
+ // initialize abort flag
+ testhelper.AbortTrigger = ""
- // Run tests specs only if not in diagnostic mode, otherwise all TSs would run.
- var env provider.TestEnvironment
- if !diagnosticMode {
- env.SetNeedsRefresh()
- env = provider.GetTestEnvironment()
- ginkgo.RunSpecs(t, CnfCertificationTestSuiteName)
+ // Run tests specs only if not in diagnostic mode, otherwise all TSs would run.
+ var env provider.TestEnvironment
+ if !diagnosticMode {
+ env.SetNeedsRefresh()
+ env = provider.GetTestEnvironment()
+ ginkgo.RunSpecs(t, CnfCertificationTestSuiteName)
+ }
+ ContinueRun(diagnosticMode, &env, claimData, claimRoot)
+ } else {
+ go StartServer()
+ select {}
}
+}
+func ContinueRun(diagnosticMode bool, env *provider.TestEnvironment, claimData *claim.Claim, claimRoot *claim.Root) {
endTime := time.Now()
claimData.Metadata.EndTime = endTime.UTC().Format(claimhelper.DateTimeFormatDirective)
@@ -211,7 +236,7 @@ func TestTest(t *testing.T) {
// Send claim file to the collector if specified by env var
if configuration.GetTestParameters().EnableDataCollection {
- err = collector.SendClaimFileToCollector(env.CollectorAppEndPoint, claimOutputFile, env.ExecutedBy, env.PartnerName, env.CollectorAppPassword)
+ err := collector.SendClaimFileToCollector(env.CollectorAppEndPoint, claimOutputFile, env.ExecutedBy, env.PartnerName, env.CollectorAppPassword)
if err != nil {
log.Errorf("Failed to send post request to the collector: %v", err)
}
@@ -264,3 +289,102 @@ func incorporateVersions(claimData *claim.Claim) {
ClaimFormat: ClaimFormatVersion,
}
}
+func StartServer() {
+ server := &http.Server{
+ Addr: ":8084", // Server address
+ ReadTimeout: 10 * time.Second, // Maximum duration for reading the entire request
+ WriteTimeout: 10 * time.Second, // Maximum duration for writing the entire response
+ IdleTimeout: 120 * time.Second, // Maximum idle duration before closing the connection
+ }
+ webserver.HandlereqFunc()
+
+ http.HandleFunc("/runFunction", RunHandler)
+
+ fmt.Println("Server is running on :8084...")
+ if err := server.ListenAndServe(); err != nil {
+ panic(err)
+ }
+}
+
+// Define an HTTP handler that triggers Ginkgo tests
+func RunHandler(w http.ResponseWriter, r *http.Request) {
+ webserver.Buf = bytes.NewBufferString("")
+ log.SetOutput(webserver.Buf)
+ rlog.SetOutput(webserver.Buf)
+
+ jsonData := r.FormValue("jsonData") // "jsonData" is the name of the JSON input field
+ log.Info(jsonData)
+ var data webserver.RequestedData
+ if err := json.Unmarshal([]byte(jsonData), &data); err != nil {
+ fmt.Println("Error:", err)
+ }
+ var flattenedOptions []string
+ flattenedOptions = webserver.FlattenData(data.SelectedOptions, flattenedOptions)
+
+ // Get the file from the request
+ file, handler, err := r.FormFile("kubeConfigPath") // "fileInput" is the name of the file input field
+ if err != nil {
+ http.Error(w, "Unable to retrieve file from form", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ // Create a new file on the server to store the uploaded content
+ uploadedFile, err := os.Create(handler.Filename)
+ if err != nil {
+ http.Error(w, "Unable to create file for writing", http.StatusInternalServerError)
+ return
+ }
+ defer uploadedFile.Close()
+
+ // Copy the uploaded file's content to the new file
+ _, err = io.Copy(uploadedFile, file)
+ if err != nil {
+ http.Error(w, "Unable to copy file", http.StatusInternalServerError)
+ return
+ }
+
+ // Copy the uploaded file to the server file
+
+ os.Setenv("KUBECONFIG", handler.Filename)
+ log.Infof("KUBECONFIG : %v", handler.Filename)
+
+ log.Infof("Labels filter : %v", flattenedOptions)
+ t := testing.T{}
+ claimData, claimRoot := PreRun(&t)
+ var env provider.TestEnvironment
+ env.SetNeedsRefresh()
+ env = provider.GetTestEnvironment()
+
+ // fetch the current config
+ suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration()
+ // adjust it
+ suiteConfig.SkipStrings = []string{"NEVER-RUN"}
+ reporterConfig.FullTrace = true
+ reporterConfig.JUnitReport = "cnf-certification-tests_junit.xml"
+ // pass it in to RunSpecs
+ suiteConfig.LabelFilter = strings.Join(flattenedOptions, "")
+ ginkgo.RunSpecs(&t, CnfCertificationTestSuiteName, suiteConfig, reporterConfig)
+
+ ContinueRun(false, &env, claimData, claimRoot)
+ // Return the result as JSON
+ response := struct {
+ Message string `json:"Message"`
+ }{
+ Message: fmt.Sprintf("Sucsses to run %s", strings.Join(flattenedOptions, "")),
+ }
+ // Serialize the response data to JSON
+ jsonResponse, err := json.Marshal(response)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ // Set the Content-Type header to specify that the response is JSON
+ w.Header().Set("Content-Type", "application/json")
+ // Write the JSON response to the client
+ _, err = w.Write(jsonResponse)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/cnf-certification-test/webserver/index.html b/cnf-certification-test/webserver/index.html
new file mode 100644
index 000000000..07611612a
--- /dev/null
+++ b/cnf-certification-test/webserver/index.html
@@ -0,0 +1,238 @@
+
+
+
+
+
+ CNF Certification Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CNF Certification Test
+
+
+ Show Log
+
+
+
+ Logs
+ No Logs Found
+ Close
+
+
+
+
+ Red Hat legal and privacy links
+
+ © 2022 Red Hat, Inc.
+ Red Hat legal and privacy links
+
+
+
+ ({ ...acc,
+ [key]: key in acc ? [acc[key], val] : val
+ }), {});
+
+ delete fields.submit;
+ console.log(fields);
+ formdata.append("jsonData", JSON.stringify(fields));
+
+ // Send an HTTP request to the server to run the function
+ let heading;
+ let message;
+ let state = 'success';
+
+ try {
+ const data = await fetch('/runFunction', {
+ method: 'POST',
+ body: formdata,
+ }).then(response => {
+ if (response.ok) {
+ return response.json();
+ } else {
+ throw new Error(response.statusText);
+ }
+ });
+
+ heading = 'Success';
+ message = data.Message;
+
+ console.log(data);
+ } catch (error) {
+ console.error(error);
+ heading = 'Error'
+ message = error.message;
+ state = 'danger';
+ } finally {
+ form.elements.submit.disabled = false;
+ for (const el of form.elements) if (el instanceof HTMLFieldSetElement) el.disabled = false
+ }
+
+ return { heading, message, state };
+ }
\ No newline at end of file
diff --git a/cnf-certification-test/webserver/toast.js b/cnf-certification-test/webserver/toast.js
new file mode 100644
index 000000000..1516826de
--- /dev/null
+++ b/cnf-certification-test/webserver/toast.js
@@ -0,0 +1,53 @@
+import '@rhds/elements/rh-alert/rh-alert.js';
+
+export async function toast({
+ heading,
+ message,
+ state = 'info',
+ timeout = 8_000,
+}) {
+ await import('@rhds/elements/rh-alert/rh-alert.js');
+ const h2 = document.createElement('h2');
+ h2.textContent = heading;
+ h2.slot = 'header';
+ const alert = document.createElement('rh-alert');
+ alert.setAttribute('aria-live', 'polite');
+ alert.dismissable = true;
+ alert.state = state;
+ alert.classList.add('toast');
+ alert.style.position = 'fixed';
+ alert.style.margin = '0';
+ alert.style.setProperty('z-index', '1000');
+ alert.style.setProperty('inset-inline-end', 'var(--rh-space-xl, 24px)');
+ alert.style.setProperty('inset-block-start', 'var(--rh-space-xl, 24px)');
+ alert.append(h2);
+ if (message) {
+ const p = document.createElement('p');
+ p.textContent = message;
+ alert.append(message);
+ }
+
+ alert.animate({ translate: ['100% 0', '0 0'] }, { duration: 200 });
+
+ await Promise.all(Array.from(document.querySelectorAll('rh-alert.toast'), toast =>
+ // TODO: handle more than 2 toasts
+ toast.animate({
+ translate: [
+ '0 auto',
+ '0 calc(100% + 20px)',
+ ],
+ }, {
+ duration: 200,
+ composite: 'accumulate',
+ rangeEnd: '100%',
+ fill: 'forwards',
+ }).finished));
+
+ setTimeout(() => {
+ if (alert.isConnected) {
+ alert.remove();
+ }
+ }, timeout);
+
+ document.body.append(alert);
+}
\ No newline at end of file
diff --git a/cnf-certification-test/webserver/webserver_function.go b/cnf-certification-test/webserver/webserver_function.go
new file mode 100644
index 000000000..1069432f6
--- /dev/null
+++ b/cnf-certification-test/webserver/webserver_function.go
@@ -0,0 +1,132 @@
+package webserver
+
+import (
+ "bufio"
+ "bytes"
+ _ "embed"
+ "fmt"
+ "net/http"
+
+ "github.com/gorilla/websocket"
+ "github.com/robert-nix/ansihtml"
+ "github.com/sirupsen/logrus"
+)
+
+//go:embed index.html
+var indexHTML []byte
+
+//go:embed submit.js
+var submit []byte
+
+//go:embed logs.js
+var logs []byte
+
+//go:embed toast.js
+var toast []byte
+var Buf *bytes.Buffer
+
+var upgrader = websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool {
+ return true
+ },
+}
+
+func logStreamHandler(w http.ResponseWriter, r *http.Request) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ logrus.Printf("WebSocket upgrade error: %v", err)
+ return
+ }
+ defer conn.Close()
+ // Create a scanner to read the log file line by line
+ for {
+ scanner := bufio.NewScanner(Buf)
+ for scanner.Scan() {
+ line := scanner.Bytes()
+ line = append(ansihtml.ConvertToHTML(line), []byte("
")...)
+
+ // Send each log line to the client
+ if err := conn.WriteMessage(websocket.TextMessage, line); err != nil {
+ fmt.Println(err)
+ return
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ logrus.Printf("Error reading log file: %v", err)
+ return
+ }
+ }
+}
+
+type RequestedData struct {
+ SelectedOptions interface{} `json:"selectedOptions"`
+}
+type ResponseData struct {
+ Message string `json:"message"`
+}
+
+func FlattenData(data interface{}, result []string) []string {
+ switch v := data.(type) {
+ case string:
+ result = append(result, v)
+ case []interface{}:
+ for _, item := range v {
+ result = FlattenData(item, result)
+ }
+ case map[string]interface{}:
+ for key, item := range v {
+ if key == "selectedOptions" {
+ result = FlattenData(item, result)
+ }
+ result = FlattenData(item, result)
+ }
+ }
+ return result
+}
+func HandlereqFunc() {
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ // Set the content type to "text/html".
+ w.Header().Set("Content-Type", "text/html")
+ // Write the embedded HTML content to the response.
+ _, err := w.Write(indexHTML)
+ if err != nil {
+ http.Error(w, "Failed to write response", http.StatusInternalServerError)
+ return
+ }
+ })
+
+ http.HandleFunc("/submit.js", func(w http.ResponseWriter, r *http.Request) {
+ // Set the content type to "application/javascript".
+ w.Header().Set("Content-Type", "application/javascript")
+ // Write the embedded JavaScript content to the response.
+ _, err := w.Write(submit)
+ if err != nil {
+ http.Error(w, "Failed to write response", http.StatusInternalServerError)
+ return
+ }
+ })
+
+ http.HandleFunc("/logs.js", func(w http.ResponseWriter, r *http.Request) {
+ // Set the content type to "application/javascript".
+ w.Header().Set("Content-Type", "application/javascript")
+ // Write the embedded JavaScript content to the response.
+ _, err := w.Write(logs)
+ if err != nil {
+ http.Error(w, "Failed to write response", http.StatusInternalServerError)
+ return
+ }
+ })
+
+ http.HandleFunc("/toast.js", func(w http.ResponseWriter, r *http.Request) {
+ // Set the content type to "application/javascript".
+ w.Header().Set("Content-Type", "application/javascript")
+ // Write the embedded JavaScript content to the response.
+ _, err := w.Write(toast)
+ if err != nil {
+ http.Error(w, "Failed to write response", http.StatusInternalServerError)
+ return
+ }
+ })
+ // Serve the static HTML file
+ http.HandleFunc("/logstream", logStreamHandler)
+}
diff --git a/go.mod b/go.mod
index 32d9c88f8..a3d408080 100644
--- a/go.mod
+++ b/go.mod
@@ -211,12 +211,15 @@ require (
github.com/fatih/color v1.16.0
github.com/go-logr/logr v1.3.0
github.com/go-logr/stdr v1.2.2
+ github.com/gorilla/websocket v1.4.2
github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0
github.com/manifoldco/promptui v0.9.0
github.com/openshift/machine-config-operator v0.0.1-0.20230515070935-49f32d46538e
github.com/redhat-openshift-ecosystem/openshift-preflight v0.0.0-20231018165107-f04b78186455
+ github.com/robert-nix/ansihtml v1.0.1
github.com/test-network-function/oct v0.0.3
github.com/test-network-function/privileged-daemonset v1.0.14
+ github.com/robert-nix/ansihtml v1.0.1
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1
k8s.io/kubectl v0.28.3
diff --git a/go.sum b/go.sum
index f4b2c7deb..43965e69d 100644
--- a/go.sum
+++ b/go.sum
@@ -314,6 +314,7 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
@@ -517,6 +518,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
+github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
diff --git a/run-cnf-suites.sh b/run-cnf-suites.sh
index 7e7a0465d..625be8052 100755
--- a/run-cnf-suites.sh
+++ b/run-cnf-suites.sh
@@ -8,16 +8,17 @@ set -x
export OUTPUT_LOC="$PWD/cnf-certification-test"
usage() {
- echo "$0 [-o OUTPUT_LOC] [-l LABEL...]"
+ echo "$0 [-o OUTPUT_LOC] [-l LABEL...] [-s run from webpage]"
echo "Call the script and list the test suites to run"
echo " e.g."
echo " $0 [ARGS] -l \"access-control,lifecycle\""
echo " will run the access-control and lifecycle suites"
echo " $0 [ARGS] -l all will run all the tests"
+ echo " $0 [ARGS] -s true will run the test from server"
echo ""
echo "Allowed suites are listed in the README."
echo ""
- echo "The specs can be listed with $0 -L|--list [-l LABEL...]"
+ echo "The specs can be listed with $0 -L|--list [-l LABEL...] [-s run from webpage]"
}
usage_error() {
@@ -28,6 +29,7 @@ usage_error() {
TIMEOUT=24h0m0s
LABEL=''
LIST=false
+SERVER_RUN=false
BASEDIR=$(dirname "$(realpath "$0")")
# Parse args beginning with "-".
@@ -49,6 +51,9 @@ while [[ $1 == -* ]]; do
exit 1
fi
;;
+ -s)
+ SERVER_RUN=true
+ ;;
-l | --label)
while (("$#" >= 2)) && ! [[ $2 = --* ]] && ! [[ $2 = -* ]]; do
LABEL="$LABEL $2"
@@ -91,6 +96,10 @@ GINKGO_ARGS="\
-test.v\
"
+if [ "$SERVER_RUN" = "true" ]; then
+ GINKGO_ARGS="$GINKGO_ARGS -serverMode"
+fi
+
if [[ $LABEL == "all" ]]; then
LABEL='common,extended,faredge,telco'
fi
@@ -100,7 +109,7 @@ echo "Report will be output to '$OUTPUT_LOC'"
echo "ginkgo arguments '${GINKGO_ARGS}'"
LABEL_STRING=''
-if [ -z "$LABEL" ]; then
+if [ -z "$LABEL" ] && { [ -z "$SERVER_RUN" ] || [ "$SERVER_RUN" == "false" ]; }; then
echo "No test label (-l) was set, so only diagnostic functions will run."
else
LABEL_STRING="-ginkgo.label-filter=${LABEL}"