diff --git a/docs/getstarted/builders-workshop/3-testing.md b/docs/getstarted/builders-workshop/3-testing.md index 59412a1b..e599d752 100644 --- a/docs/getstarted/builders-workshop/3-testing.md +++ b/docs/getstarted/builders-workshop/3-testing.md @@ -46,8 +46,8 @@ This command: * Generates the basic structure for writing tests :::note -The default testing language is KCL. You can specify `--langauage=python` in -the test command +The default testing language is KCL. You can also specify `--language=python` or +`--language=go` to generate tests in Python or Go. ::: ## Set up your test imports diff --git a/docs/manuals/cli/howtos/testing.md b/docs/manuals/cli/howtos/testing.md index 31db0d81..c096a1dd 100644 --- a/docs/manuals/cli/howtos/testing.md +++ b/docs/manuals/cli/howtos/testing.md @@ -17,6 +17,7 @@ To test your compositions and run end-to-end tests, make sure you have: * The `up` CLI `v0.38` or higher [installed][installed] * An Upbound account +* Authenticated with Upbound using `up login` ## Development control planes @@ -36,7 +37,7 @@ Preview how your composition creates resources with the `up composition render` command before you deploy to a development control plane. ```shell -up composition render apis/xbuckets/composition.yaml examples/bucket/example-xr.yaml +up composition render apis/xstoragebuckets/composition.yaml examples/xstoragebuckets/example-xr.yaml ``` This command requires a **Composite Resource** (XR) file that defines the @@ -79,44 +80,108 @@ generate` command. ### Generate a composition test Composition tests validate the logic of your compositions without requiring a -live environment. They simulate the composition controller's behavior, allowing +live environment. They simulate the composition function pipeline, allowing you to test resource creation, dependencies, and state transitions with mock data. You can generate tests with `up test generate` for composition tests. -You can write tests in KCL or Python. +You can write tests in KCL, Python, or Go. For example, to generate a composition test: + +```shell {copy-lines="all"} +up test generate my-test --language=go +``` + + -```ini {copy-lines="all"} -up test generate --language=python +```shell {copy-lines="all"} +up test generate my-test --language=python ``` -```ini {copy-lines="all"} -up test generate --language=kcl +```shell {copy-lines="all"} +up test generate my-test --language=kcl ``` #### Author a composition test -Composition tests use a declarative API in KCL or Python. Each test -models a single composition controller loop, making testing more streamlined for +Composition tests use a declarative API in KCL, Python, or Go. Each test +models a single function pipeline run, making testing more streamlined for reading and debugging. -This testing command simulates the Crossplane composition controller. The -controller evaluates the current state of resources, processes the composition, -and makes necessary changes. The command recreates this process locally to -verify composition logic. +The test runner invokes your composition functions with a given XR input and +compares the composed resource output against `assertResources`. It doesn't +exercise the Crossplane composition controller, which handles reconciliation and +external resource management. + + +```go +// Package main generates a CompositionTest +package main + +import ( + "fmt" + "os" + + "k8s.io/utils/ptr" + "sigs.k8s.io/yaml" + + metav1 "dev.upbound.io/models/io/k8s/meta/v1" + metav1alpha1 "dev.upbound.io/models/io/upbound/dev/meta/v1alpha1" +) + +func main() { + assertResources := resourcesToItems[metav1alpha1.CompositionTestSpecAssertResourcesItem]() + test := metav1alpha1.CompositionTest{ + APIVersion: ptr.To(metav1alpha1.CompositionTestAPIVersionmetaDevUpboundIoV1Alpha1), + Kind: ptr.To(metav1alpha1.CompositionTestKindCompositionTest), + Metadata: &metav1.ObjectMeta{ + Name: ptr.To("test-xstoragebucket-default-go"), + }, + Spec: &metav1alpha1.CompositionTestSpec{ + AssertResources: &assertResources, + CompositionPath: ptr.To("apis/xstoragebuckets/composition.yaml"), + XrPath: ptr.To("examples/xstoragebuckets/example.yaml"), + XrdPath: ptr.To("apis/xstoragebuckets/definition.yaml"), + TimeoutSeconds: ptr.To(120), + Validate: ptr.To(false), + }, + } + output := map[string]interface{}{ + "items": []interface{}{test}, + } + out, err := yaml.Marshal(output) + if err != nil { + fmt.Fprintf(os.Stderr, "Error encoding YAML: %v\n", err) + os.Exit(1) + } + fmt.Print(string(out)) +} +``` + +:::note +`up test generate` produces the complete `main.go`, including helper functions +`resourcesToItems`, `toItem`, and `convertViaJSON`. The snippet above shows only +`main()`. Do not copy it as a standalone file — run `up test generate` first, +then edit the generated file. +::: + +Import your provider resource types from `dev.upbound.io/models` and pass them +as arguments to `resourcesToItems` to populate `assertResources`. The test +runner calls `go run .` and captures the YAML printed to stdout. + + ```python @@ -140,7 +205,7 @@ test = compositiontest.CompositionTest( -```shell +```kcl import models.io.upbound.dev.meta.v1alpha1 as metav1alpha1 _items = [ @@ -172,16 +237,22 @@ everything in that directory. up test run tests/* ``` -To run a specific test, give the full path of that test: +To run a specific test, give the path to that test directory: + +```shell +up test run tests/my-test +``` + +For KCL tests you can also point to the specific file: ```shell -up test run tests/xstoragebucket-default/main.k +up test run tests/my-test/main.k ``` You can provide wildcards to run tests matching a pattern: ```shell -up test run tests/xstoragebucket/**/*.k +up test run tests/xstoragebucket/** ``` The command returns a summary of results: @@ -197,12 +268,13 @@ up test run tests/* SUCCESS Failed tests: 0 ``` -When you run Compositions tests, Upbound: +When you run composition tests, Upbound: -1. Detects the test language and converts to a unified format. -2. Builds and pushes the project to local daemon. -3. Sets the context to the new control plane. -4. Executes tests and validates results. +1. Detects the test language from the files present (`main.k`, `main.py`, or `go.mod`). +2. For Go tests, runs `go run .` locally and captures the YAML output. +3. Builds composition functions and pushes them to the local Docker daemon. +4. Sets the context to the local control plane. +5. Executes tests and validates results. ### Generate an end-to-end test @@ -210,22 +282,28 @@ End-to-end tests validate compositions in real environments, ensuring creation, deletion, and operations work as expected. You can generate test with `up test generate` for end-to-end tests. -You can write tests in KCL or Python. +You can write tests in KCL, Python, or Go. -For example, to generate a end-to-end test: +For example, to generate an end-to-end test: + +```shell {copy-lines="all"} +up test generate my-e2e-test --e2e --language=go +``` + + -```ini {copy-lines="all"} -up test generate --e2e --language=python +```shell {copy-lines="all"} +up test generate my-e2e-test --e2e --language=python ``` -```ini {copy-lines="all"} -up test generate --e2e --language=kcl +```shell {copy-lines="all"} +up test generate my-e2e-test --e2e --language=kcl ``` @@ -233,11 +311,74 @@ up test generate --e2e --language=kcl #### Author an end-to-end test -End-to-end tests use the `E2ETest` API, written in KCL or Python. +End-to-end tests use the `E2ETest` API, written in KCL, Python, or Go. + + +```go +// Package main generates an E2ETest +package main + +import ( + "fmt" + "os" + + "k8s.io/utils/ptr" + "sigs.k8s.io/yaml" + + metav1 "dev.upbound.io/models/io/k8s/meta/v1" + metav1alpha1 "dev.upbound.io/models/io/upbound/dev/meta/v1alpha1" +) + +func main() { + manifests := resourcesToItems[metav1alpha1.E2ETestSpecManifestsItem]() + extraResources := resourcesToItems[metav1alpha1.E2ETestSpecExtraResourcesItem]() + test := metav1alpha1.E2ETest{ + APIVersion: ptr.To(metav1alpha1.E2ETestAPIVersionmetaDevUpboundIoV1Alpha1), + Kind: ptr.To(metav1alpha1.E2ETestKindE2ETest), + Metadata: &metav1.ObjectMeta{ + Name: ptr.To("e2etest-xstoragebucket-go"), + }, + Spec: &metav1alpha1.E2ETestSpec{ + Crossplane: &metav1alpha1.E2ETestSpecCrossplane{ + AutoUpgrade: &metav1alpha1.E2ETestSpecCrossplaneAutoUpgrade{ + Channel: ptr.To(metav1alpha1.E2ETestSpecCrossplaneAutoUpgradeChannelRapid), + }, + }, + DefaultConditions: &[]string{"Ready"}, + Manifests: &manifests, + ExtraResources: &extraResources, + SkipDelete: ptr.To(false), + TimeoutSeconds: ptr.To(4500), + }, + } + output := map[string]interface{}{ + "items": []interface{}{test}, + } + out, err := yaml.Marshal(output) + if err != nil { + fmt.Fprintf(os.Stderr, "Error encoding YAML: %v\n", err) + os.Exit(1) + } + fmt.Print(string(out)) +} +``` + +:::note +`up test generate` produces the complete `main.go` including helper functions. +The snippet above shows only `main()` — run `up test generate` first, then edit +the generated file. +::: + +Populate `manifests` with your claim or XR resources and `extraResources` with +any prerequisites such as `ProviderConfig`. Import your resource types from +`dev.upbound.io/models` and pass them to `resourcesToItems`. + + + ```python @@ -298,7 +439,7 @@ test = e2etest.E2ETest( -```shell +```kcl import models.com.example.platform.v1alpha1 as platformv1alpha1 import models.io.upbound.aws.v1beta1 as awsv1beta1 import models.io.upbound.dev.meta.v1alpha1 as metav1alpha1 @@ -352,16 +493,16 @@ everything in that directory. up test run --e2e tests/* ``` -To run a specific test, give the full path of that test: +To run a specific test, give the path to that test directory: ```shell -up test run --e2e tests/e2etest-xstoragebucket-default/main.k +up test run --e2e tests/my-e2e-test ``` You can provide wildcards to run tests matching a pattern: ```shell -up test run --e2e tests/xstoragebucket/**/*.k +up test run --e2e tests/xstoragebucket/** ``` The command returns a summary of results: diff --git a/docusaurus.config.js b/docusaurus.config.js index 784dc8be..3a6333d5 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -12,6 +12,7 @@ const config = { organizationName: "upbound", projectName: "docs", onBrokenLinks: "warn", + onBrokenMarkdownLinks: "warn", scripts: [ { src: "https://cdn-cookieyes.com/client_data/401fea7900d8d7b84b9e7b40/script.js", @@ -43,9 +44,6 @@ const config = { }, markdown: { mermaid: true, - hooks: { - onBrokenMarkdownLinks: "warn", - }, }, themes: ["@docusaurus/theme-mermaid"], presets: [ diff --git a/package-lock.json b/package-lock.json index 883c2d30..dceaa927 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@upbound/elements": "0.0.1", "@upbound/utils": "0.0.1", "@upbound/ux": "0.0.2", + "clean": "^4.0.2", "clsx": "^2.1.1", "docusaurus-pushfeedback": "^1.0.5", "js-yaml": "^4.1.0", @@ -8991,6 +8992,12 @@ "astring": "bin/astring" } }, + "node_modules/async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.23", "funding": [ @@ -9671,6 +9678,18 @@ "version": "5.0.5", "license": "MIT" }, + "node_modules/clean": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/clean/-/clean-4.0.2.tgz", + "integrity": "sha512-2LGVh4dNtI16L4UzqDHO6Hbl74YjG1vWvEUU78dgLO4kuyqJZFMNMPBx+EGtYKTFb14e24p+gWXgkabqxc1EUw==", + "license": "MIT", + "dependencies": { + "async": "^0.9.0", + "minimist": "^1.1.0", + "mix2": "^1.0.0", + "skema": "^1.0.0" + } + }, "node_modules/clean-css": { "version": "5.3.3", "license": "MIT", @@ -13965,6 +13984,15 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/make-array": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/make-array/-/make-array-0.1.2.tgz", + "integrity": "sha512-bcFmxgZ+OTaMYJp/w6eifElKTcfum7Gi5H7vQ8KzAf9X6swdxkVuilCaG3ZjXr/qJsQT4JJ2Rq9SDYScWEdu9Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/markdown-extensions": { "version": "2.0.0", "license": "MIT", @@ -20033,6 +20061,17 @@ "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", "license": "MIT" }, + "node_modules/skema": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/skema/-/skema-1.0.2.tgz", + "integrity": "sha512-5LWfF2RSW2B3xfOaY6j49X8aNwsnj9cRVrM5QMF7it+cZvpv5ufiOUT13ps2U52sIbAzs11bdRP6mi5qyg75VQ==", + "license": "MIT", + "dependencies": { + "async": "^0.9.0", + "make-array": "^0.1.2", + "mix2": "^1.0.0" + } + }, "node_modules/skin-tone": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", diff --git a/package.json b/package.json index 6a2cde8b..9c5763c2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@upbound/elements": "0.0.1", "@upbound/utils": "0.0.1", "@upbound/ux": "0.0.2", + "clean": "^4.0.2", "clsx": "^2.1.1", "docusaurus-pushfeedback": "^1.0.5", "js-yaml": "^4.1.0",