diff --git a/.github/workflows/go-ci/action.yml b/.github/workflows/go-ci/action.yml new file mode 100644 index 00000000000..6dc66224fe0 --- /dev/null +++ b/.github/workflows/go-ci/action.yml @@ -0,0 +1,22 @@ +name: 'Run CI for Go SDK' +runs: + using: 'composite' + steps: + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22.2' + + - name: Lint for Go SDK + uses: golangci/golangci-lint-action@v4 + with: + version: v1.57.2 + working-directory: ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src + args: --config=${{ github.workspace }}/.golangci.yml + + - name: Run CI for Go SDK + shell: bash + run: | + cd ./ocaml/sdk-gen/component-test/ + cp -r ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src jsonrpc-client/go/goSDK + bash run-tests.sh \ No newline at end of file diff --git a/.github/workflows/go-lint/action.yml b/.github/workflows/go-lint/action.yml deleted file mode 100644 index d041aa4627a..00000000000 --- a/.github/workflows/go-lint/action.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Go Lint -runs: - using: composite - steps: - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: 1.22.2 - - name: golangci-lint - uses: golangci/golangci-lint-action@v4 - with: - version: v1.57.2 - working-directory: ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src - args: --config=${{ github.workspace }}/.golangci.yml diff --git a/.github/workflows/sdk-ci/action.yml b/.github/workflows/sdk-ci/action.yml index 3113b1b91b4..f20b59ee8d6 100644 --- a/.github/workflows/sdk-ci/action.yml +++ b/.github/workflows/sdk-ci/action.yml @@ -1,6 +1,20 @@ -name: Continuous Integration for SDKs +name: 'Run CI for Go SDK' runs: - using: composite + using: 'composite' steps: - - name: Lint for Go SDK - uses: ./.github/workflows/go-lint + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '^3.12' + + - name: Install dependencies for JsonRPC Server + shell: bash + run: | + python -m pip install --upgrade pip + cd ./ocaml/sdk-gen/component-test/jsonrpc-server + pip install -r requirements.txt + + - name: Run CI for Go SDK + uses: ./.github/workflows/go-ci + + # Run other tests here \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/README.md b/ocaml/sdk-gen/component-test/README.md new file mode 100644 index 00000000000..d60b597c25d --- /dev/null +++ b/ocaml/sdk-gen/component-test/README.md @@ -0,0 +1,67 @@ +## How Component Testing Works + +#### jsonrpc-server +The jsonrpc-server is a mock HTTP server which aligns with [jsonrpc 2.0 specification](https://www.jsonrpc.org/specification) and to emulate the xen-api server. It parses the test data from JSON files located in the spec/ directory, executes the method and parameters specified in spec files with incoming requests and then sends back a jsonrpc response that includes the expect_result. + +For the purpose of backwards and forwards compatibility, the following structure is recommended: + +- Base Directory: All test cases should be housed within a dedicated directory, conventionally named spec/. + +- Sub-directories: As the number of test cases grows or when differentiating between API versions, it becomes advantageous to categorize them into sub-directories. These sub-directories can represent different versions or modules of the API. +``` +spec/ +├── v1/ +│ ├── test_id_1.json +│ ├── test_id_2.json +│ └── ... +├── v2/ +│ ├── test_id_3.json +│ ├── test_id_4.json +│ └── ... +└── ... +``` + +- Test Data Specification: Within each JSON file, the test data should be structured to include essential fields such as test_id, method, params, and expect_result. This structure allows for clear definition and expectation of each test case. +```json +{ + "test_id": "test_id_1", + "method": "methodName", + "params": { + // ... parameters required for the method + + }, + "expect_result": { + // ... expected result of the method execution + + } + +} +``` + +#### jsonrpc-client +jsonrpc-client is a client that imports the SDK and runs the functions, following these important details: + +1. Add test_id as a customize request header. + +2. Ensure that the function and params are aligned with the data defined in spec/ directory. + +3. In order to support test reports, practitioners should use the specific test framework to test SDK, eg: pytest, gotest, junit, xUnit and so on. + +4. To support the SDK component test, it recommended to move the SDK generated to a sub directory as a local module for import purposes, eg: +``` +cp -r ${{ github.workspace }}/_build/install/default/xapi/sdk/go/src jsonrpc-client/go/goSDK +``` +then, import the local module. +``` +module github.com/xapi-project/xen-api/sdk-gen/component-test/jsonrpc-client/go + +go 1.22.2 + +replace xenapi => ./goSDK +``` + +#### github actions +For a CI step in the generate sdk sources job, it should involve performing lint and component testing after sdk generation. + +## Run test locally +Install python 3.11+ with requirements and go 1.22+ and go to ocaml/sdk-gen/component-test and run `bash run-tests.sh` diff --git a/ocaml/sdk-gen/component-test/jsonrpc-client/go/go.mod b/ocaml/sdk-gen/component-test/jsonrpc-client/go/go.mod new file mode 100644 index 00000000000..f1ee4c8ee52 --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-client/go/go.mod @@ -0,0 +1,5 @@ +module github.com/xapi-project/xen-api/sdk-gen/component-test/jsonrpc-client/go + +go 1.22.2 + +replace xenapi => ./goSDK \ No newline at end of file diff --git a/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go b/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go new file mode 100644 index 00000000000..466c037deed --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-client/go/main_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "flag" + "fmt" + "testing" + "xenapi" +) + +const ServerURL = "http://localhost:5000" + +var session *xenapi.Session + +var USERNAME_FLAG = flag.String("root", "", "the username of the host (e.g. root)") +var PASSWORD_FLAG = flag.String("secret", "", "the password of the host") + +func TestLoginSuccess(t *testing.T) { + session = xenapi.NewSession(&xenapi.ClientOpts{ + URL: ServerURL, + Headers: map[string]string{ + "Test-ID": "test_id1", + }, + }) + if session == nil { + fmt.Printf("Failed to get the session") + return + } + _, err := session.LoginWithPassword(*USERNAME_FLAG, *PASSWORD_FLAG, "1.0", "Go sdk component test") + if err != nil { + t.Log(err) + t.Fail() + return + } + + expectedXapiVersion := "1.20" + getXapiVersion := session.XAPIVersion + if expectedXapiVersion != getXapiVersion { + t.Errorf("Unexpected result. Expected: %s, Got: %s", expectedXapiVersion, getXapiVersion) + } + var expectedAPIVersion xenapi.APIVersion = xenapi.APIVersion2_21 + getAPIVersion := session.APIVersion + if expectedAPIVersion != getAPIVersion { + t.Errorf("Unexpected result. Expected: %s, Got: %s", expectedAPIVersion, getAPIVersion) + } + + err = session.Logout() + if err != nil { + t.Log(err) + t.Fail() + return + } +} diff --git a/ocaml/sdk-gen/component-test/jsonrpc-server/requirements.txt b/ocaml/sdk-gen/component-test/jsonrpc-server/requirements.txt new file mode 100644 index 00000000000..e9feb68da3d --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-server/requirements.txt @@ -0,0 +1 @@ + aiohttp diff --git a/ocaml/sdk-gen/component-test/jsonrpc-server/server.py b/ocaml/sdk-gen/component-test/jsonrpc-server/server.py new file mode 100644 index 00000000000..8fff806eac1 --- /dev/null +++ b/ocaml/sdk-gen/component-test/jsonrpc-server/server.py @@ -0,0 +1,72 @@ +""" +Module Name: + jsonrpc_server +Description: + This module provides a simple JSON-RPC server implementation using aiohttp. +""" + +import json +import os + +from aiohttp import web # pytype: disable=import-error + + +def load_json_files(): + """ + Load all JSON files from the 'spec' directory and merge their contents + into a dictionary. + + Returns: + dict: A dictionary containing the merged contents of all JSON files. + """ + data = {} + for filename in os.listdir("spec/"): + if filename.endswith(".json"): + with open(f"spec/{filename}", "r", encoding="utf-8") as f: + data.update(json.load(f)) + return data + + +async def handle(request): + """ + Handle incoming requests and execute methods based on the test ID provided + in the request headers. + + Args: + request (aiohttp.web.Request): The incoming HTTP request. + + Returns: + aiohttp.web.Response: The HTTP response containing the JSON-RPC result. + """ + spec = load_json_files() + test_id = request.headers.get("Test-ID") + test_data = spec.get(test_id, {}) + data = await request.json() + + try: + assert data.get("method") in test_data.get("method") + assert test_data.get("params")[data.get("method")] == data.get("params") + except Exception: + response = { + "jsonrpc": "2.0", + "id": data.get("id"), + "error": { + "code": 500, + "message": "Rpc server failed to handle the client request!", + "data": str(data), + } + } + else: + response = { + "jsonrpc": "2.0", + "id": data.get("id"), + **test_data.get("expected_result")[data.get("method")], + } + return web.json_response(response) + + +app = web.Application() +app.router.add_post("/jsonrpc", handle) + +if __name__ == "__main__": + web.run_app(app, port=5000) diff --git a/ocaml/sdk-gen/component-test/run-tests.sh b/ocaml/sdk-gen/component-test/run-tests.sh new file mode 100644 index 00000000000..0eed34f0e0c --- /dev/null +++ b/ocaml/sdk-gen/component-test/run-tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -ex + +SCRIPT_PATH=$(cd "$(dirname "$0")" && pwd) + +start_jsonrpc_server() { + echo "Starting JSONRPC server" + python3 jsonrpc-server/server.py & + JSONRPC_SERVER_PID=$! + sleep 1 +} + +start_jsonrpc_go_client() { + echo "Starting JSONRPC Go client" + + cd jsonrpc-client/go + # ensure that all dependencies are satisfied + go mod tidy + # build client.go and run it + go test main_test.go -v & + JSONRPC_GO_CLIENT_PID=$! +} + +trap 'kill $JSONRPC_SERVER_PID $JSONRPC_GO_CLIENT_PID 2>/dev/null' EXIT + +main() { + cd "$SCRIPT_PATH" + start_jsonrpc_server + start_jsonrpc_go_client + + # Wait for the component test to finish + wait $JSONRPC_GO_CLIENT_PID + + # Shut down the server to reduce future problems when testing other clients. + kill $JSONRPC_SERVER_PID +} + +main diff --git a/ocaml/sdk-gen/component-test/spec/session.json b/ocaml/sdk-gen/component-test/spec/session.json new file mode 100644 index 00000000000..4916448f3d9 --- /dev/null +++ b/ocaml/sdk-gen/component-test/spec/session.json @@ -0,0 +1,40 @@ +{ + "test_id1": { + "method": [ + "session.login_with_password", + "pool.get_all", + "pool.get_record", + "host.get_record", + "session.logout" + ], + "params": { + "session.login_with_password": ["", "", "1.0", "Go sdk component test"], + "pool.get_all": ["login successfully"], + "pool.get_record": ["login successfully", "poolref0"], + "host.get_record": ["login successfully", ""], + "session.logout": ["login successfully"] + }, + "expected_result": { + "session.login_with_password": { + "result": "login successfully" + }, + "pool.get_all": { + "result": ["poolref0"] + }, + "pool.get_record": { + "result": {"name_label": "pool0"} + }, + "host.get_record": { + "result": { + "name_label": "host0", + "software_version": {"xapi": "1.20"}, + "API_version_major": "2", + "API_version_minor": "21" + } + }, + "session.logout": { + "result": "logout successfully" + } + } + } +} \ No newline at end of file