Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
eac5fc3
Add project scenario: unified framework for project-based tests
THardy98 Mar 23, 2026
6fc4c97
Add .NET project harness + HelloWorld and Nexus test
THardy98 Mar 27, 2026
f535d48
Add Docker support for project tests
THardy98 Mar 27, 2026
cb1aab7
Remove BaseDir from project.Build
THardy98 Mar 28, 2026
3c4fefc
cleanup README project tests section
THardy98 Mar 28, 2026
a1666dd
correct prog typing
THardy98 Mar 28, 2026
624431d
exclude more build artifacts in .dockerignore
THardy98 Mar 28, 2026
b52842b
specify binary path explicitly in project dockerfiles
THardy98 Mar 28, 2026
a6f0f24
remove redundant worker started log (already logged in harness)
THardy98 Mar 29, 2026
e3f9ab5
add comment for not great transitive dep on projects/api
THardy98 Mar 29, 2026
8a1d7e3
add search attributes to the .net project tests
THardy98 Mar 29, 2026
8fdd2b2
fix proto package name
THardy98 Mar 29, 2026
ae8242e
add docs to api.proto and improve description of project CLI command
THardy98 Mar 29, 2026
0eaf403
README update
THardy98 Mar 29, 2026
6b4e703
update harness proto dep, add transitive project/api dep to the go wo…
THardy98 Mar 29, 2026
77367ba
rename .net helloworld project -> HelloWorld
THardy98 Mar 29, 2026
3e039c6
Simplify .NET project build - no symlinking hackiness. Builds project…
THardy98 Mar 31, 2026
b674887
go mod tidy helloworld test
THardy98 Mar 31, 2026
3303d06
make project .NET build same pattern as Go project build
THardy98 Mar 31, 2026
a61a7c6
remove comment
THardy98 Mar 31, 2026
ff426b0
build dotnet program in release mode
THardy98 Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
workers/dotnet/**/obj/
workers/dotnet/**/bin/
workers/dotnet/**/build/
workers/dotnet/**/program.csproj
workers/*/projects/tests/project-build-*/
56 changes: 56 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,62 @@ jobs:
annotate_only: true
skip_annotations: true

build-project-worker:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- dockerfile: go-project.Dockerfile
project-dir: workers/go/projects/tests/helloworld
name: go-helloworld
- dockerfile: dotnet-project.Dockerfile
project-dir: workers/dotnet/projects/tests/HelloWorld
name: dotnet-helloworld
- dockerfile: dotnet-project.Dockerfile
project-dir: workers/dotnet/projects/tests/NexusSimpleWorkflow
name: dotnet-nexus
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
- name: Build ${{ matrix.name }} project image
run: |
docker build \
-f dockerfiles/${{ matrix.dockerfile }} \
--build-arg PROJECT_DIR=${{ matrix.project-dir }} \
-t omes-project:${{ matrix.name }} .
- name: Install Temporal CLI
uses: temporalio/setup-temporal@v0
- name: Start dev server
run: |
temporal server start-dev --port 7233 --headless --log-level error &
timeout 30 bash -c 'until nc -z 127.0.0.1 7233; do sleep 1; done'
- name: Start ${{ matrix.name }} worker
run: |
docker run --rm -d --network host \
--name omes-project-worker \
omes-project:${{ matrix.name }} worker \
--task-queue omes-project-${{ matrix.name }} \
--server-address 127.0.0.1:7233 \
--namespace default
- name: Run project scenario
run: |
docker run --rm --network host \
omes-project:${{ matrix.name }} run-scenario \
--scenario project \
--run-id project-${{ matrix.name }} \
--iterations 1 \
--server-address 127.0.0.1:7233 \
--namespace default
- name: Stop worker
if: always()
run: docker stop omes-project-worker || true

build-ks-gen-and-ensure-protos-up-to-date:
runs-on: ubuntu-latest
steps:
Expand Down
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ workers/java/build
/loadgen/kitchen-sink-gen/target/

omes.sln
/workers/dotnet/obj/
/workers/dotnet/bin/
workers/dotnet/**/obj/
workers/dotnet/**/bin/
workers/dotnet/**/build/
workers/dotnet/**/program.csproj
/last_fuzz_run.proto
workers/dotnet/Temporalio.Omes.temp.csproj

workers/python/**/__pycache__/
workers/*/omes-temp-*/
workers/*/prepared/
workers/*/projects/tests/project-build-*/

tsconfig.tsbuildinfo

temporal-omes
.gocache/
149 changes: 149 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,155 @@ If version is not specified, the SDK version specified in `versions.env` will be
Publishing images is done via CI, using the `build-push-worker-image` command.
See the GHA workflows for more information.

## Project Tests

Project tests are self-contained test programs that exercise specific SDK features (e.g., Nexus operations)
independently of the standard scenario/executor framework. Each project brings its own SDK version and
test logic, while a shared harness handles client creation, worker lifecycle, and gRPC orchestration.

### Structure

```
workers/<lang>/projects/
harness/ # Shared harness (gRPC server, client factory, worker management)
tests/
HelloWorld/ # Simple example project
NexusSimpleWorkflow/ # Nexus operation test
```

Each project binary supports two subcommands:
- `worker` — starts a Temporal worker
- `project-server` — starts a gRPC server that accepts `Init` and `Execute` RPCs

The `Init` RPC initializing the load test, providing client connection parameters, load test metadata
(i.e. RunID), and optional project-specific data.

The `Execute` RPC is called for each iteration of the executor running against your project test. This is
principally what drives load.

### Docker

Project Dockerfiles (`<lang>-project.Dockerfile` in `dockerfiles/`) produce images that can run both the project worker and the scenario runner.
The entrypoint script routes commands automatically: `worker` and `project-server` run the project
binary, while `run-scenario` and other commands run the omes CLI.

```sh
# Build
docker build -f dockerfiles/go-project.Dockerfile \
--build-arg PROJECT_DIR=workers/go/projects/tests/helloworld \
-t omes-go-project-helloworld .

# Run worker (one container)
docker run --rm -d omes-go-project-helloworld worker \
--task-queue omes-my-run --server-address host.docker.internal:7233 --namespace default

# Run scenario (another container, same image)
docker run --rm omes-go-project-helloworld run-scenario \
--scenario project \
--run-id my-run --iterations 1 \
--server-address host.docker.internal:7233 --namespace default
```

### Running locally

The easiest way to run a project test locally is `run-scenario-with-worker`, which starts an embedded
Temporal server, builds the project, starts the worker, and runs the scenario:

```sh
# Go
go run ./cmd run-scenario-with-worker \
--scenario project --project-dir workers/go/projects/tests/helloworld \
--option language=go --option project-dir=workers/go/projects/tests/helloworld \
--language go --embedded-server --iterations 1
```

This can be done with any other implemented language by swapping out the `language` value and
pointing to the respective `project-dir`.

**Note**:
There's a peculiarity in this usage. You have to specify `--project-dir ...` and also `--option project-dir=...`.
This is because we are building both the worker and the runner. The worker expects the top-level
`--project-dir` while the runner expects the `--option`. This allows separate usage of `run-worker` and `run-scenario`.

You can also run the worker and scenario separately against an existing server:

```sh
# Terminal 1: start worker
go run ./cmd run-worker --language go --run-id my-run \
--project-dir workers/go/projects/tests/helloworld

# Terminal 2: run scenario
go run ./cmd run-scenario --scenario project --run-id my-run --iterations 1 \
--option language=go --option project-dir=workers/go/projects/tests/helloworld
```

To build against a local SDK repo, add `--version ../sdk-go` (or `../sdk-dotnet`).

### Writing a new project test

The best starting point is the `HelloWorld` sample in your language (`workers/<lang>/projects/tests/HelloWorld/`).

1. Create a directory under `workers/<lang>/projects/tests/<TestName>/`
2. Register callbacks with the harness and call `Run()`:

```go
package main

import (
harness "github.com/temporalio/omes/workers/go/projects/harness"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
)

func main() {
h := harness.New()

// RegisterClient creates a Temporal client. Called by both the worker and project-server.
// Can customize the client options as you like, in addition to the provided options.
h.RegisterClient(func(opts client.Options, _ harness.ClientConfig) (client.Client, error) {
return client.Dial(opts)
})

// OnInit runs project-specific setup during the Init RPC (i.e. creating Nexus endpoints)
// Optional.
h.OnInit(func(ctx context.Context, c client.Client, config harness.InitConfig) error {
return nil
})

// RegisterWorker starts the Temporal worker with your registered workflows/activities etc.
// Can customize worker options as you like, in addition to the config provided.
h.RegisterWorker(func(c client.Client, config harness.WorkerConfig) error {
w := worker.New(c, config.TaskQueue, worker.Options{})
w.RegisterWorkflow(MyWorkflow)
return w.Run(worker.InterruptCh())
})

// OnExecute runs once per iteration - create your load (per-iteration) here (i.e. start a workflow)
h.OnExecute(func(ctx context.Context, c client.Client, info harness.ExecuteInfo) error {
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
TaskQueue: info.TaskQueue,
}, MyWorkflow, "input")
if err != nil {
return err
}
var result string
return run.Get(ctx, &result)
})

h.Run()
}
```

3. Add a test function in `scenarios/project/project_test.go`

### Running project tests
There are some basic tests that run a project against a test server at `scenarios/project/project_test.go`.
You can run one via:

```sh
go test ./scenarios/project/ -run TestGoHelloWorld -count=1
```

## Specific Scenarios

### ThroughputStress (Go only)
Expand Down
3 changes: 2 additions & 1 deletion cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
"os"

"github.com/spf13/cobra"
_ "github.com/temporalio/omes/scenarios" // Register scenarios (side-effect)
_ "github.com/temporalio/omes/scenarios" // Register scenarios (side-effect)
_ "github.com/temporalio/omes/scenarios/project" // Register project scenario
)

func Main() {
Expand Down
19 changes: 16 additions & 3 deletions cmd/cli/prepare_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/temporalio/omes/cmd/clioptions"
project "github.com/temporalio/omes/scenarios/project"
"github.com/temporalio/omes/workers"
)

Expand All @@ -22,9 +23,20 @@ func prepareWorkerCmd() *cobra.Command {
if err != nil {
b.Logger.Fatal(fmt.Errorf("failed to get root directory: %w", err))
}
baseDir := workers.BaseDir(repoDir, b.SdkOptions.Language)
if _, err := b.Build(cmd.Context(), baseDir); err != nil {
b.Logger.Fatal(err)
if b.ProjectDir != "" {
if _, err := project.Build(cmd.Context(), project.BuildOptions{
Language: b.SdkOptions.Language,
ProjectDir: b.ProjectDir,
Version: b.SdkOptions.Version,
Logger: b.Logger,
}); err != nil {
b.Logger.Fatal(err)
}
} else {
baseDir := workers.BaseDir(repoDir, b.SdkOptions.Language)
if _, err := b.Build(cmd.Context(), baseDir); err != nil {
b.Logger.Fatal(err)
}
}
},
}
Expand All @@ -41,6 +53,7 @@ type workerBuilder struct {

func (b *workerBuilder) addCLIFlags(fs *pflag.FlagSet) {
fs.StringVar(&b.DirName, "dir-name", "", "Directory name for prepared worker")
fs.StringVar(&b.ProjectDir, "project-dir", "", "Path to project directory (builds a project test instead of standard worker)")
b.SdkOptions.AddCLIFlags(fs)
fs.AddFlagSet(b.loggingOptions.FlagSet())
}
Expand Down
1 change: 1 addition & 0 deletions cmd/cli/run_scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func (r *scenarioRunner) run(ctx context.Context) error {
Logger: r.logger,
MetricsHandler: metrics.NewHandler(),
Client: client,
ClientOptions: r.clientOptions,
Configuration: loadgen.RunConfiguration{
Iterations: r.iterations,
Duration: r.duration,
Expand Down
37 changes: 37 additions & 0 deletions dockerfiles/dotnet-project.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Build the CLI in a Go container
ARG TARGETARCH
FROM --platform=linux/$TARGETARCH golang:1.25 AS cli-build

WORKDIR /app
COPY cmd ./cmd
COPY loadgen ./loadgen
COPY scenarios ./scenarios
COPY metrics ./metrics
COPY workers/*.go ./workers/
COPY workers/go/projects/api ./workers/go/projects/api
COPY go.mod go.sum ./
RUN CGO_ENABLED=0 go build -o temporal-omes ./cmd

# Build the .NET project
FROM --platform=linux/$TARGETARCH mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build

WORKDIR /app
COPY --from=cli-build /app/temporal-omes ./temporal-omes
COPY workers/dotnet ./workers/dotnet
COPY workers/proto ./workers/proto

ARG PROJECT_DIR
RUN ./temporal-omes prepare-worker --language cs --dir-name project-prepared --project-dir "$PROJECT_DIR"

# Runtime container with ASP.NET for gRPC server support
FROM --platform=linux/$TARGETARCH mcr.microsoft.com/dotnet/aspnet:8.0-jammy

ENV OMES_PROJECT_LANGUAGE=cs
ENV OMES_PROJECT_BINARY=/app/prebuilt-project/build/program

COPY --from=cli-build /app/temporal-omes /app/temporal-omes
COPY --from=build /app/workers/dotnet/projects/tests/*/build /app/prebuilt-project/build
COPY --from=build /app/workers/dotnet/projects/tests/*/program.csproj /app/prebuilt-project/
COPY dockerfiles/project-entrypoint.sh /app/entrypoint.sh

ENTRYPOINT ["/app/entrypoint.sh"]
2 changes: 2 additions & 0 deletions dockerfiles/dotnet.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ COPY loadgen ./loadgen
COPY scenarios ./scenarios
COPY metrics ./metrics
COPY workers/*.go ./workers/
COPY workers/go/projects/api ./workers/go/projects/api
COPY go.mod go.sum ./

# Build the CLI
Expand All @@ -41,6 +42,7 @@ COPY ${SDK_DIR} ./repo

# Copy the worker files
COPY workers/dotnet ./workers/dotnet
COPY workers/proto ./workers/proto

# Prepare the worker
RUN CGO_ENABLED=0 ./temporal-omes prepare-worker --language cs --dir-name prepared --version "$SDK_VERSION"
Expand Down
37 changes: 37 additions & 0 deletions dockerfiles/go-project.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Build in a full featured container
ARG TARGETARCH
FROM --platform=linux/$TARGETARCH golang:1.25 AS build

WORKDIR /app

# Copy CLI build dependencies
COPY cmd ./cmd
COPY loadgen ./loadgen
COPY scenarios ./scenarios
COPY metrics ./metrics
COPY workers/*.go ./workers/
COPY workers/go/projects/api ./workers/go/projects/api
COPY go.mod go.sum ./

# Build the CLI
RUN CGO_ENABLED=0 go build -o temporal-omes ./cmd

# Copy the worker + project files
COPY workers/go ./workers/go
COPY workers/proto ./workers/proto

# Build the project
ARG PROJECT_DIR
RUN CGO_ENABLED=0 ./temporal-omes prepare-worker --language go --dir-name project-prepared --project-dir "$PROJECT_DIR"

# Runtime container
FROM --platform=linux/$TARGETARCH alpine:3

ENV OMES_PROJECT_LANGUAGE=go
ENV OMES_PROJECT_BINARY=/app/prebuilt-project/program

COPY --from=build /app/temporal-omes /app/temporal-omes
COPY --from=build /app/workers/go/projects/tests/project-build-*/. /app/prebuilt-project/
COPY dockerfiles/project-entrypoint.sh /app/entrypoint.sh

ENTRYPOINT ["/app/entrypoint.sh"]
Loading
Loading