refactor: rename project uatu -> sanderling (#24)

* refactor: rename Go module path uatu -> sanderling

Module path github.com/priyanshujain/uatu -> github.com/priyanshujain/sanderling,
including all imports and the proto go_package option. Generated .pb.go files
rewritten in-place; safe to regenerate with protoc later.

* chore(proto): regenerate driverpb after module path rename

The previous sed-based module rename corrupted the embedded descriptor
byte lengths. buf generate rewrites them cleanly.

* refactor: rename CLI binary uatu -> sanderling

Updates Makefile target + UATU_BIN var, .goreleaser project/build IDs,
.gitignore comment, and all user-facing strings in the CLI help text,
error messages, and tests. Binary is now bin/sanderling.

* refactor(sdk): rename Kotlin package dev.uatu.sdk -> dev.sanderling.sdk

Moves sdk/android/src/{main,test}/kotlin/dev/uatu -> dev/sanderling and
rewrites package declarations, imports, and the Gradle namespace. Class
names (Uatu, UatuRuntime) are renamed in a follow-up commit.

* refactor(sidecar): rename Kotlin package dev.uatu.sidecar -> dev.sanderling.sidecar

Moves sidecar/src/{main,test}/kotlin/dev/uatu -> dev/sanderling and
rewrites package declarations, imports, and the application mainClass.

* refactor: rename Uatu API surface -> Sanderling

- Kotlin: Uatu -> Sanderling, UatuRuntime -> SanderlingRuntime (+ files).
- JS host binding: globalThis.__uatu__ -> __sanderling__ (Go verifier,
  spec-api, tests).
- TS interface: UatuRuntime -> SanderlingRuntime; internal tags
  __uatuFormula / __uatuActionGenerator -> __sanderling* variants.
- Go trace: UatuVersion field + uatu_version JSON tag renamed.
- Socket naming: uatu-agent / uatu-agent-reader -> sanderling-agent*.
- Sample app, docs, inline-JS test strings updated to match.

* refactor(examples): rename examples/folio/uatu -> examples/folio/sanderling

Renames the example spec directory; updates justfile paths + gitignore
entries accordingly. Package.json name/description and @uatu/spec
dependency are renamed in the npm + docs commits.

* chore(build): rename gradle property + rootProject.name uatu -> sanderling

- Renames the uatu.version gradle property and all its -P references in
  Makefile, build.gradle.kts files, and .github/workflows/release.yml.
- settings.gradle.kts rootProject.name = "sanderling".
- Renames .env.local.example header + release-cli workflow job name.

* refactor(proto): rename proto package uatu.driver.v1 -> sanderling.driver.v1

Updates the proto package and java_package, regenerates driver.pb.go +
driver_grpc.pb.go, rewrites Kotlin imports and the gRPC ServiceName
assertion in driver_test.go.

* refactor: rename npm package @uatu/spec -> @sanderling/spec

Renames package name in pkg/spec-api/package.json + lockfile, all
consumer imports (examples/folio spec, testdata, verifier tests), the
esbuild alias in cmd/sanderling/test_run.go, and related doc references.

* docs: rename uatu -> sanderling in README, docs, and URLs

- README + docs/{manual,development}/*: narrative + GitHub + Pages URLs.
- POM + npm package.json repo/homepage/bugs URLs.
- .gitignore + embed_stub + Makefile-comment references updated to
  'make sanderling'.
- Minor narrative comments in cmd/sanderling/test_run.go and
  internal/inspect/server.go.

* refactor: rename remaining internal uatu strings -> sanderling

- SANDERLING_TEST_PHONE/OTP env vars (cmd + bundler tests).
- sanderling-sidecar runtime tmp dir + extracted JAR filename.
- Inspect web UI: @sanderling/inspect-web package, title, theme
  localStorage key, RunList empty-state copy, uatu_version TS field.
- Sample app storage key sanderling.ledger.v1.
- Test data: sanderling_test AVD name + com.example.sanderling_test.
- Release docs tarball name template.
This commit is contained in:
pj authored and GitHub committed 2026-04-21 11:57:49 +07:00
1 parent 13bb2feb82
commit 8ccf95c1cf
111 files changed
+511 -511

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
# uatu release credentials (local dev only). # sanderling release credentials (local dev only).
# #
# Copy to `.env.local` (gitignored) and fill in values ONLY if you need to # Copy to `.env.local` (gitignored) and fill in values ONLY if you need to
# fire a real release from your laptop. Day-to-day work and the `release-cli` # fire a real release from your laptop. Day-to-day work and the `release-cli`
+4 -4
View File
@@ -55,10 +55,10 @@ jobs:
gradle-${{ runner.os }}- gradle-${{ runner.os }}-
- name: Publish to Maven Central - name: Publish to Maven Central
run: ./gradlew :sdk-android:publishAndReleaseToMavenCentral -Puatu.version=${{ steps.ver.outputs.version }} run: ./gradlew :sdk-android:publishAndReleaseToMavenCentral -Psanderling.version=${{ steps.ver.outputs.version }}
release-npm: release-npm:
name: Publish @uatu/spec to npm name: Publish @sanderling/spec to npm
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -91,7 +91,7 @@ jobs:
- name: Publish - name: Publish
working-directory: pkg/spec-api working-directory: pkg/spec-api
# npm tag pre-releases (e.g. 0.1.0-rc1) as "next" so npm install @uatu/spec # npm tag pre-releases (e.g. 0.1.0-rc1) as "next" so npm install @sanderling/spec
# keeps resolving the latest stable. # keeps resolving the latest stable.
run: | run: |
if [[ "${{ steps.ver.outputs.version }}" == *-* ]]; then if [[ "${{ steps.ver.outputs.version }}" == *-* ]]; then
@@ -101,7 +101,7 @@ jobs:
fi fi
release-cli: release-cli:
name: Publish uatu CLI to GitHub Releases name: Publish sanderling CLI to GitHub Releases
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
+2 -2
View File
@@ -17,7 +17,7 @@ local.properties
*.test *.test
*.out *.out
# Uatu runs # Sanderling runs
/runs/ /runs/
# Node # Node
@@ -27,7 +27,7 @@ node_modules/
.env .env
.env.local .env.local
# Sidecar fat JAR. Build artifact copied in by `make uatu` before # Sidecar fat JAR. Build artifact copied in by `make sanderling` before
# `go build -tags withsidecar`. Never commit: it's ~130 MB. # `go build -tags withsidecar`. Never commit: it's ~130 MB.
internal/sidecar/assets/sidecar-all.jar internal/sidecar/assets/sidecar-all.jar
+8 -8
View File
@@ -1,6 +1,6 @@
version: 2 version: 2
project_name: uatu project_name: sanderling
before: before:
hooks: hooks:
@@ -11,9 +11,9 @@ before:
- sh -c 'mkdir -p internal/sidecar/assets && cp sidecar/build/libs/sidecar-all.jar internal/sidecar/assets/sidecar-all.jar' - sh -c 'mkdir -p internal/sidecar/assets && cp sidecar/build/libs/sidecar-all.jar internal/sidecar/assets/sidecar-all.jar'
builds: builds:
- id: uatu - id: sanderling
main: ./cmd/uatu main: ./cmd/sanderling
binary: uatu binary: sanderling
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
flags: flags:
@@ -28,11 +28,11 @@ builds:
- arm64 - arm64
archives: archives:
- id: uatu - id: sanderling
ids: ids:
- uatu - sanderling
formats: [tar.gz] formats: [tar.gz]
name_template: "uatu_{{ .Version }}_{{ .Os }}_{{ .Arch }}" name_template: "sanderling_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files: files:
- README.md - README.md
- LICENSE* - LICENSE*
@@ -59,6 +59,6 @@ changelog:
release: release:
github: github:
owner: priyanshujain owner: priyanshujain
name: uatu name: sanderling
draft: false draft: false
prerelease: auto prerelease: auto
+10 -10
View File
@@ -9,7 +9,7 @@ GO_PACKAGES := ./...
SIDECAR_JAR := sidecar/build/libs/sidecar-all.jar SIDECAR_JAR := sidecar/build/libs/sidecar-all.jar
SIDECAR_EMBED := internal/sidecar/assets/sidecar-all.jar SIDECAR_EMBED := internal/sidecar/assets/sidecar-all.jar
SDK_AAR := sdk/android/build/outputs/aar/sdk-android-release.aar SDK_AAR := sdk/android/build/outputs/aar/sdk-android-release.aar
UATU_BIN := bin/uatu SANDERLING_BIN := bin/sanderling
DOCS_SRC := $(shell find docs -type f -name '*.md' -not -path 'docs/_*') DOCS_SRC := $(shell find docs -type f -name '*.md' -not -path 'docs/_*')
DOCS_OUT := $(patsubst docs/%.md,build/site/%.html,$(DOCS_SRC)) DOCS_OUT := $(patsubst docs/%.md,build/site/%.html,$(DOCS_SRC))
@@ -18,7 +18,7 @@ DOCS_TEMPLATE := docs/_template/page.html
INSPECT_DIST := internal/inspect/dist INSPECT_DIST := internal/inspect/dist
WEB_DIST := web/dist WEB_DIST := web/dist
.PHONY: bootstrap proto sidecar sdk-android sdk-android-publish uatu install test test-go test-kotlin test-spec-api web-typecheck web-build web-dev inspect-dev docs clean release-cli release-android-local release-npm-dry .PHONY: bootstrap proto sidecar sdk-android sdk-android-publish sanderling install test test-go test-kotlin test-spec-api web-typecheck web-build web-dev inspect-dev docs clean release-cli release-android-local release-npm-dry
bootstrap: bootstrap:
$(GO) mod download $(GO) mod download
@@ -37,17 +37,17 @@ sdk-android:
sdk-android-publish: sdk-android-publish:
ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:publishToMavenLocal ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:publishToMavenLocal
uatu: $(UATU_BIN) sanderling: $(SANDERLING_BIN)
$(UATU_BIN): $(SIDECAR_EMBED) web-build $(SANDERLING_BIN): $(SIDECAR_EMBED) web-build
mkdir -p bin mkdir -p bin
$(GO) build -tags withsidecar -o $(UATU_BIN) ./cmd/uatu $(GO) build -tags withsidecar -o $(SANDERLING_BIN) ./cmd/sanderling
# Installs `uatu` into $GOBIN (or $GOPATH/bin) so it's directly on PATH for # Installs `sanderling` into $GOBIN (or $GOPATH/bin) so it's directly on PATH for
# anyone with a standard Go toolchain setup. # anyone with a standard Go toolchain setup.
install: $(SIDECAR_EMBED) web-build install: $(SIDECAR_EMBED) web-build
$(GO) install -tags withsidecar ./cmd/uatu $(GO) install -tags withsidecar ./cmd/sanderling
@dest="$$($(GO) env GOBIN)"; [ -n "$$dest" ] || dest="$$($(GO) env GOPATH)/bin"; echo "installed uatu to $$dest" @dest="$$($(GO) env GOBIN)"; [ -n "$$dest" ] || dest="$$($(GO) env GOPATH)/bin"; echo "installed sanderling to $$dest"
web-build: web-build:
cd web && bun install --frozen-lockfile && bun run build cd web && bun install --frozen-lockfile && bun run build
@@ -59,7 +59,7 @@ web-dev:
cd web && bun run dev cd web && bun run dev
inspect-dev: $(SIDECAR_EMBED) inspect-dev: $(SIDECAR_EMBED)
$(GO) run -tags withsidecar ./cmd/uatu inspect --dev $(GO) run -tags withsidecar ./cmd/sanderling inspect --dev
web-typecheck: web-typecheck:
cd web && bun install --frozen-lockfile && bun run typecheck cd web && bun install --frozen-lockfile && bun run typecheck
@@ -109,7 +109,7 @@ release-cli: $(SIDECAR_JAR)
release-android-local: release-android-local:
@if [ -f .env.local ]; then set -a; . ./.env.local; set +a; fi; \ @if [ -f .env.local ]; then set -a; . ./.env.local; set +a; fi; \
ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:publishToMavenLocal -Puatu.version=0.0.0-local ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:publishToMavenLocal -Psanderling.version=0.0.0-local
release-npm-dry: release-npm-dry:
cd pkg/spec-api && npm ci && npm run build && npm pack --dry-run cd pkg/spec-api && npm ci && npm run build && npm pack --dry-run
+8 -8
View File
@@ -1,15 +1,15 @@
# uatu # sanderling
Autonomous property-based testing for mobile apps. Specs in TypeScript. Core in Go. Drives the app under test through Maestro and an in-app SDK. Autonomous property-based testing for mobile apps. Specs in TypeScript. Core in Go. Drives the app under test through Maestro and an in-app SDK.
> Alpha. Android emulator only. Full scope in the [v0.1.0 roadmap](https://github.com/priyanshujain/uatu/issues/4). > Alpha. Android emulator only. Full scope in the [v0.1.0 roadmap](https://github.com/priyanshujain/sanderling/issues/4).
## Docs ## Docs
- [Getting started](https://priyanshujain.github.io/uatu/manual/getting-started.html) - [Getting started](https://priyanshujain.github.io/sanderling/manual/getting-started.html)
- [Writing specs](https://priyanshujain.github.io/uatu/manual/writing-specs.html) - [Writing specs](https://priyanshujain.github.io/sanderling/manual/writing-specs.html)
- [`uatu inspect` UI](https://priyanshujain.github.io/uatu/manual/inspect.html) - [`sanderling inspect` UI](https://priyanshujain.github.io/sanderling/manual/inspect.html)
- [Example](https://github.com/priyanshujain/uatu/tree/master/examples/folio) - [Example](https://github.com/priyanshujain/sanderling/tree/master/examples/folio)
- [Architecture](https://priyanshujain.github.io/uatu/development/architecture.html) - [Architecture](https://priyanshujain.github.io/sanderling/development/architecture.html)
After a `uatu test` run, browse traces locally with `uatu inspect`. It opens a web UI for stepping through actions, screenshots, snapshots, residual formulas, and exceptions. After a `sanderling test` run, browse traces locally with `sanderling inspect`. It opens a web UI for stepping through actions, screenshots, snapshots, residual formulas, and exceptions.
+3 -3
View File
@@ -5,7 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/priyanshujain/uatu/internal/bundler" "github.com/priyanshujain/sanderling/internal/bundler"
) )
func main() { func main() {
@@ -31,8 +31,8 @@ func main() {
result, err := bundler.Bundle(bundler.Options{ result, err := bundler.Bundle(bundler.Options{
EntryFile: entryFile, EntryFile: entryFile,
Aliases: map[string]string{ Aliases: map[string]string{
"@uatu/spec": specApiPath, "@sanderling/spec": specApiPath,
"@uatu/spec/defaults/properties": defaultPropertiesPath, "@sanderling/spec/defaults/properties": defaultPropertiesPath,
}, },
}) })
if err != nil { if err != nil {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/priyanshujain/uatu/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/hierarchy"
) )
func main() { func main() {
File renamed without changes.
@@ -31,18 +31,18 @@ func TestParseAdbDevices_Empty(t *testing.T) {
func TestParseAVDList_DropsInfoLines(t *testing.T) { func TestParseAVDList_DropsInfoLines(t *testing.T) {
output := `INFO | Storing crashdata in: /tmp/x output := `INFO | Storing crashdata in: /tmp/x
Medium_Phone_API_36.0 Medium_Phone_API_36.0
uatu_test sanderling_test
` `
got := parseAVDList(output) got := parseAVDList(output)
want := []string{"Medium_Phone_API_36.0", "uatu_test"} want := []string{"Medium_Phone_API_36.0", "sanderling_test"}
if !reflect.DeepEqual(got, want) { if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want) t.Fatalf("got %v, want %v", got, want)
} }
} }
func TestPickAVD_ExplicitName(t *testing.T) { func TestPickAVD_ExplicitName(t *testing.T) {
got, err := pickAVD("Pixel_7", []string{"Pixel_7", "uatu_test"}) got, err := pickAVD("Pixel_7", []string{"Pixel_7", "sanderling_test"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -59,12 +59,12 @@ func TestPickAVD_ExplicitMissing(t *testing.T) {
} }
func TestPickAVD_SingleAvailable(t *testing.T) { func TestPickAVD_SingleAvailable(t *testing.T) {
got, err := pickAVD("", []string{"uatu_test"}) got, err := pickAVD("", []string{"sanderling_test"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if got != "uatu_test" { if got != "sanderling_test" {
t.Fatalf("got %q, want uatu_test", got) t.Fatalf("got %q, want sanderling_test", got)
} }
} }
@@ -11,7 +11,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/priyanshujain/uatu/internal/sidecar" "github.com/priyanshujain/sanderling/internal/sidecar"
) )
type doctorCheck struct { type doctorCheck struct {
@@ -30,7 +30,7 @@ func defaultDoctorChecks() []doctorCheck {
func checkSidecarJAR(_ context.Context) error { func checkSidecarJAR(_ context.Context) error {
if sidecar.IsPlaceholder() { if sidecar.IsPlaceholder() {
return fmt.Errorf("placeholder JAR embedded; run `make sidecar && make uatu` to embed the real fat JAR") return fmt.Errorf("placeholder JAR embedded; run `make sidecar && make sanderling` to embed the real fat JAR")
} }
if sidecar.EmbeddedSize() == 0 { if sidecar.EmbeddedSize() == 0 {
return fmt.Errorf("embedded JAR is empty") return fmt.Errorf("embedded JAR is empty")
File renamed without changes.
@@ -14,7 +14,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/priyanshujain/uatu/internal/inspect" "github.com/priyanshujain/sanderling/internal/inspect"
) )
type inspectOptions struct { type inspectOptions struct {
@@ -67,7 +67,7 @@ func runInspect(options inspectOptions, stdout io.Writer) error {
} }
address := listener.Addr().(*net.TCPAddr) address := listener.Addr().(*net.TCPAddr)
browseURL := buildBrowseURL(address, deepLinkID) browseURL := buildBrowseURL(address, deepLinkID)
fmt.Fprintf(stdout, "uatu inspect listening on %s (runs=%s)\n", browseURL, runsDirectory) fmt.Fprintf(stdout, "sanderling inspect listening on %s (runs=%s)\n", browseURL, runsDirectory)
context, cancel := context.WithCancel(context.Background()) context, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@@ -36,7 +36,7 @@ func TestParseInspectArgs_RejectsTooManyPositional(t *testing.T) {
func TestRun_HelpListsInspectCommand(t *testing.T) { func TestRun_HelpListsInspectCommand(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
if err := run([]string{"uatu"}, &stdout, io.Discard); err != nil { if err := run([]string{"sanderling"}, &stdout, io.Discard); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !strings.Contains(stdout.String(), "inspect") { if !strings.Contains(stdout.String(), "inspect") {
+6 -6
View File
@@ -25,18 +25,18 @@ type testOptions struct {
output string output string
} }
const topUsage = `uatu is a property-based UI fuzzer for mobile apps. const topUsage = `sanderling is a property-based UI fuzzer for mobile apps.
Usage: Usage:
uatu <command> [flags] sanderling <command> [flags]
Commands: Commands:
test Run a spec against an app for a fixed duration. test Run a spec against an app for a fixed duration.
inspect Serve a local web UI for browsing runs/. inspect Serve a local web UI for browsing runs/.
doctor Check that the host environment is ready to run uatu. doctor Check that the host environment is ready to run sanderling.
version Print the uatu version. version Print the sanderling version.
Run "uatu <command> -h" for command-specific flags. Run "sanderling <command> -h" for command-specific flags.
` `
func parseTestArgs(args []string, stderr io.Writer) (testOptions, error) { func parseTestArgs(args []string, stderr io.Writer) (testOptions, error) {
@@ -100,7 +100,7 @@ func run(args []string, stdout, stderr io.Writer) error {
fmt.Fprintln(stdout, Version) fmt.Fprintln(stdout, Version)
return nil return nil
default: default:
return fmt.Errorf("unknown command: %q (try 'uatu help')", args[1]) return fmt.Errorf("unknown command: %q (try 'sanderling help')", args[1])
} }
} }
@@ -90,10 +90,10 @@ func TestParseTestArgs_RejectsNonAndroidPlatform(t *testing.T) {
func TestRun_HelpPrintsUsage(t *testing.T) { func TestRun_HelpPrintsUsage(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
if err := run([]string{"uatu"}, &stdout, io.Discard); err != nil { if err := run([]string{"sanderling"}, &stdout, io.Discard); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !strings.Contains(stdout.String(), "uatu <command>") { if !strings.Contains(stdout.String(), "sanderling <command>") {
t.Errorf("usage missing, got: %q", stdout.String()) t.Errorf("usage missing, got: %q", stdout.String())
} }
} }
@@ -105,7 +105,7 @@ func TestRun_VersionPrintsVersion(t *testing.T) {
for _, arg := range []string{"version", "--version", "-v"} { for _, arg := range []string{"version", "--version", "-v"} {
var stdout bytes.Buffer var stdout bytes.Buffer
if err := run([]string{"uatu", arg}, &stdout, io.Discard); err != nil { if err := run([]string{"sanderling", arg}, &stdout, io.Discard); err != nil {
t.Fatalf("%s: %v", arg, err) t.Fatalf("%s: %v", arg, err)
} }
if strings.TrimSpace(stdout.String()) != "1.2.3-test" { if strings.TrimSpace(stdout.String()) != "1.2.3-test" {
@@ -115,7 +115,7 @@ func TestRun_VersionPrintsVersion(t *testing.T) {
} }
func TestRun_UnknownCommand(t *testing.T) { func TestRun_UnknownCommand(t *testing.T) {
err := run([]string{"uatu", "wat"}, io.Discard, io.Discard) err := run([]string{"sanderling", "wat"}, io.Discard, io.Discard)
if err == nil || !strings.Contains(err.Error(), "unknown command") { if err == nil || !strings.Contains(err.Error(), "unknown command") {
t.Fatalf("expected unknown-command error, got %v", err) t.Fatalf("expected unknown-command error, got %v", err)
} }
@@ -125,7 +125,7 @@ func TestRun_Doctor(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
// Doctor may pass or fail depending on host environment; we just want to // Doctor may pass or fail depending on host environment; we just want to
// confirm it runs and emits per-check lines. // confirm it runs and emits per-check lines.
_ = run([]string{"uatu", "doctor"}, &stdout, io.Discard) _ = run([]string{"sanderling", "doctor"}, &stdout, io.Discard)
output := stdout.String() output := stdout.String()
if !strings.Contains(output, "OK") && !strings.Contains(output, "FAIL") { if !strings.Contains(output, "OK") && !strings.Contains(output, "FAIL") {
t.Errorf("doctor output missing OK/FAIL lines: %q", output) t.Errorf("doctor output missing OK/FAIL lines: %q", output)
@@ -137,7 +137,7 @@ func TestRun_TestSubcommand_PipelineErrors(t *testing.T) {
// must surface a specific error rather than panicking — proves the flag // must surface a specific error rather than panicking — proves the flag
// wiring reaches the runner. // wiring reaches the runner.
err := run([]string{ err := run([]string{
"uatu", "test", "sanderling", "test",
"--spec", "definitely-missing-spec.ts", "--spec", "definitely-missing-spec.ts",
"--bundle-id", "com.example", "--bundle-id", "com.example",
"--avd", "definitely-missing-avd", "--avd", "definitely-missing-avd",
File renamed without changes.
@@ -12,17 +12,17 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/priyanshujain/uatu/internal/agent" "github.com/priyanshujain/sanderling/internal/agent"
"github.com/priyanshujain/uatu/internal/bundler" "github.com/priyanshujain/sanderling/internal/bundler"
"github.com/priyanshujain/uatu/internal/driver/maestro" "github.com/priyanshujain/sanderling/internal/driver/maestro"
"github.com/priyanshujain/uatu/internal/runner" "github.com/priyanshujain/sanderling/internal/runner"
"github.com/priyanshujain/uatu/internal/sidecar" "github.com/priyanshujain/sanderling/internal/sidecar"
"github.com/priyanshujain/uatu/internal/trace" "github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/uatu/internal/verifier" "github.com/priyanshujain/sanderling/internal/verifier"
) )
const ( const (
socketName = "uatu-agent" socketName = "sanderling-agent"
sidecarStartupTimeout = 30 * time.Second sidecarStartupTimeout = 30 * time.Second
sdkAcceptTimeout = 60 * time.Second sdkAcceptTimeout = 60 * time.Second
) )
@@ -35,17 +35,17 @@ func runTestPipeline(ctx context.Context, options testOptions, stdout io.Writer)
} }
aliases := map[string]string{} aliases := map[string]string{}
if specApiPath := resolveSpecAPIPath(options.spec); specApiPath != "" { if specApiPath := resolveSpecAPIPath(options.spec); specApiPath != "" {
aliases["@uatu/spec"] = specApiPath aliases["@sanderling/spec"] = specApiPath
// Also alias published subpath exports so specs importing from // Also alias published subpath exports so specs importing from
// "@uatu/spec/defaults/properties" resolve to the in-tree source. // "@sanderling/spec/defaults/properties" resolve to the in-tree source.
base := filepath.Dir(specApiPath) base := filepath.Dir(specApiPath)
aliases["@uatu/spec/defaults/properties"] = filepath.Join(base, "defaults/properties.ts") aliases["@sanderling/spec/defaults/properties"] = filepath.Join(base, "defaults/properties.ts")
} }
bundle, err := bundler.Bundle(bundler.Options{ bundle, err := bundler.Bundle(bundler.Options{
EntryFile: options.spec, EntryFile: options.spec,
Defines: map[string]string{ Defines: map[string]string{
"UATU_TEST_PHONE": os.Getenv("UATU_TEST_PHONE"), "SANDERLING_TEST_PHONE": os.Getenv("SANDERLING_TEST_PHONE"),
"UATU_TEST_OTP": os.Getenv("UATU_TEST_OTP"), "SANDERLING_TEST_OTP": os.Getenv("SANDERLING_TEST_OTP"),
}, },
Aliases: aliases, Aliases: aliases,
}) })
@@ -54,7 +54,7 @@ func runTestPipeline(ctx context.Context, options testOptions, stdout io.Writer)
} }
fmt.Fprintf(stdout, "bundled spec: %d bytes (sha256=%s)\n", len(bundle.JavaScript), bundle.SHA256[:12]) fmt.Fprintf(stdout, "bundled spec: %d bytes (sha256=%s)\n", len(bundle.JavaScript), bundle.SHA256[:12])
sidecarDirectory := filepath.Join(os.TempDir(), "uatu-sidecar") sidecarDirectory := filepath.Join(os.TempDir(), "sanderling-sidecar")
jarPath, err := sidecar.Extract(sidecarDirectory) jarPath, err := sidecar.Extract(sidecarDirectory)
if err != nil { if err != nil {
return fmt.Errorf("extract sidecar: %w", err) return fmt.Errorf("extract sidecar: %w", err)
@@ -166,7 +166,7 @@ func runTestPipeline(ctx context.Context, options testOptions, stdout io.Writer)
Platform: options.platform, Platform: options.platform,
BundleID: options.bundleID, BundleID: options.bundleID,
StartedAt: time.Now().UTC(), StartedAt: time.Now().UTC(),
UatuVersion: "0.0.1", SanderlingVersion: "0.0.1",
} }
if err := traceWriter.WriteMeta(meta); err != nil { if err := traceWriter.WriteMeta(meta); err != nil {
return fmt.Errorf("trace meta: %w", err) return fmt.Errorf("trace meta: %w", err)
@@ -212,8 +212,8 @@ func runTestPipeline(ctx context.Context, options testOptions, stdout io.Writer)
} }
// resolveSpecAPIPath returns the path to pkg/spec-api/src/index.ts inside // resolveSpecAPIPath returns the path to pkg/spec-api/src/index.ts inside
// a uatu source checkout, searched upward from the spec file and the cwd. // a sanderling source checkout, searched upward from the spec file and the cwd.
// Returns "" when not found, in which case esbuild resolves @uatu/spec via // Returns "" when not found, in which case esbuild resolves @sanderling/spec via
// node_modules the way a downstream user's project would. // node_modules the way a downstream user's project would.
func resolveSpecAPIPath(specPath string) string { func resolveSpecAPIPath(specPath string) string {
candidates := []string{} candidates := []string{}
@@ -47,6 +47,6 @@ func TestResolveSpecAPIPath_ReturnsEmptyWhenMissing(t *testing.T) {
got := resolveSpecAPIPath(specPath) got := resolveSpecAPIPath(specPath)
if got != "" { if got != "" {
t.Fatalf("got %q, want empty (no uatu source tree reachable)", got) t.Fatalf("got %q, want empty (no sanderling source tree reachable)", got)
} }
} }
+4 -4
View File
@@ -3,14 +3,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>$if(title)$$title$ · $endif$uatu</title> <title>$if(title)$$title$ · $endif$sanderling</title>
<link rel="stylesheet" href="__ROOT___assets/style.css"> <link rel="stylesheet" href="__ROOT___assets/style.css">
<style>$highlighting-css$</style> <style>$highlighting-css$</style>
</head> </head>
<body> <body>
<div class="layout"> <div class="layout">
<aside class="sidebar"> <aside class="sidebar">
<a class="brand" href="__ROOT__index.html">uatu</a> <a class="brand" href="__ROOT__index.html">sanderling</a>
<nav> <nav>
<h3>Manual</h3> <h3>Manual</h3>
<ul> <ul>
@@ -31,8 +31,8 @@
$body$ $body$
</article> </article>
<footer> <footer>
<a href="https://github.com/priyanshujain/uatu">github.com/priyanshujain/uatu</a> <a href="https://github.com/priyanshujain/sanderling">github.com/priyanshujain/sanderling</a>
· <a href="https://github.com/priyanshujain/uatu/issues/4">v0.1.0 roadmap</a> · <a href="https://github.com/priyanshujain/sanderling/issues/4">v0.1.0 roadmap</a>
</footer> </footer>
</main> </main>
</div> </div>
+3 -3
View File
@@ -8,7 +8,7 @@ Three processes, two transports.
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph Go["uatu (Go)"] subgraph Go["sanderling (Go)"]
Bundler[Bundler<br/>esbuild] --> Verifier[Verifier<br/>goja + LTL] Bundler[Bundler<br/>esbuild] --> Verifier[Verifier<br/>goja + LTL]
Verifier <--> Runner[Runner] Verifier <--> Runner[Runner]
Runner --> Trace[Trace writer<br/>JSONL + PNG] Runner --> Trace[Trace writer<br/>JSONL + PNG]
@@ -21,7 +21,7 @@ flowchart TB
subgraph Device["Emulator"] subgraph Device["Emulator"]
subgraph App["Android app (debug)"] subgraph App["Android app (debug)"]
SDK[uatu-sdk<br/>pause / hierarchy<br/>logs / coverage] SDK[sanderling-sdk<br/>pause / hierarchy<br/>logs / coverage]
end end
end end
@@ -33,7 +33,7 @@ flowchart TB
## Processes ## Processes
**uatu (Go).** The top-level binary. Bundles the spec with esbuild, evaluates it in goja, runs the main loop, dispatches actions through the driver, writes the trace. **sanderling (Go).** The top-level binary. Bundles the spec with esbuild, evaluates it in goja, runs the main loop, dispatches actions through the driver, writes the trace.
**Maestro sidecar (JVM).** A Kotlin process that wraps `maestro-client` and exposes a gRPC surface matching the `driver.Driver` interface. Handles UI input, screenshots, the system accessibility tree, and OS-level alerts. **Maestro sidecar (JVM).** A Kotlin process that wraps `maestro-client` and exposes a gRPC surface matching the `driver.Driver` interface. Handles UI input, screenshots, the system accessibility tree, and OS-level alerts.
+1 -1
View File
@@ -33,7 +33,7 @@ A 30-minute run is about 10,000 steps. Every step has at least one hierarchy dum
A seeded PRNG drives action selection. Spec evaluation is pure given state and snapshots. The bundle hash and seed are recorded in `meta.json`. A seeded PRNG drives action selection. Spec evaluation is pure given state and snapshots. The bundle hash and seed are recorded in `meta.json`.
uatu does not attempt byte-exact replay. Animation timing, keyboard popup timing, and system daemons are non-deterministic on mobile, and the cost of trying to suppress that is not worth the payoff. Same seed produces a similar trajectory, which is usually enough to reproduce the bug. sanderling does not attempt byte-exact replay. Animation timing, keyboard popup timing, and system daemons are non-deterministic on mobile, and the cost of trying to suppress that is not worth the payoff. Same seed produces a similar trajectory, which is usually enough to reproduce the bug.
## 6. Fail honest ## 6. Fail honest
+1 -1
View File
@@ -6,7 +6,7 @@ title: Development
- [Design principles](./design-principles.html) - [Design principles](./design-principles.html)
- [Architecture](./architecture.html) - [Architecture](./architecture.html)
- v0.1.0 scope: [issue #4](https://github.com/priyanshujain/uatu/issues/4) - v0.1.0 scope: [issue #4](https://github.com/priyanshujain/sanderling/issues/4)
## Building the docs site locally ## Building the docs site locally
+3 -3
View File
@@ -1,12 +1,12 @@
--- ---
title: uatu title: sanderling
--- ---
# uatu # sanderling
Autonomous property-based testing for mobile apps. Specs in TypeScript. Core in Go. Drives the app under test through Maestro and an in-app SDK. Autonomous property-based testing for mobile apps. Specs in TypeScript. Core in Go. Drives the app under test through Maestro and an in-app SDK.
Alpha: Android emulator only. Scope of v0.1.0 is tracked in [issue #4](https://github.com/priyanshujain/uatu/issues/4). Alpha: Android emulator only. Scope of v0.1.0 is tracked in [issue #4](https://github.com/priyanshujain/sanderling/issues/4).
## Manual ## Manual
+7 -7
View File
@@ -5,10 +5,10 @@ title: CLI reference
# CLI reference # CLI reference
``` ```
uatu <command> [flags] sanderling <command> [flags]
``` ```
## `uatu test` ## `sanderling test`
Run a spec against an app for a fixed duration. Run a spec against an app for a fixed duration.
@@ -23,7 +23,7 @@ Run a spec against an app for a fixed duration.
| `--seed` | `0` | PRNG seed. `0` uses a random seed and records it in `meta.json`. | | `--seed` | `0` | PRNG seed. `0` uses a random seed and records it in `meta.json`. |
| `--output` | `./runs` | Output directory for traces. | | `--output` | `./runs` | Output directory for traces. |
## `uatu inspect [run-or-runs-dir]` ## `sanderling inspect [run-or-runs-dir]`
Serve a local web UI for browsing traces. The positional argument is optional and may point at either a runs directory (the parent of many runs) or a single run directory (auto-detected by the presence of `meta.json`). Defaults to `./runs`. Serve a local web UI for browsing traces. The positional argument is optional and may point at either a runs directory (the parent of many runs) or a single run directory (auto-detected by the presence of `meta.json`). Defaults to `./runs`.
@@ -35,11 +35,11 @@ Serve a local web UI for browsing traces. The positional argument is optional an
See [the inspect UI page](inspect.md) for the panel reference and keyboard shortcuts. See [the inspect UI page](inspect.md) for the panel reference and keyboard shortcuts.
## `uatu doctor` ## `sanderling doctor`
Check the host environment for a working uatu setup: Go toolchain, JDK, Maestro availability, emulator reachability, SDK linkage hints. Check the host environment for a working sanderling setup: Go toolchain, JDK, Maestro availability, emulator reachability, SDK linkage hints.
## `uatu version` ## `sanderling version`
Print the CLI version. Print the CLI version.
@@ -49,4 +49,4 @@ Print the CLI version.
- `--max-steps` hard cap on step count. - `--max-steps` hard cap on step count.
- `--exit-on-violation` stop the run on the first property violation. - `--exit-on-violation` stop the run on the first property violation.
Tracked in [issue #4](https://github.com/priyanshujain/uatu/issues/4). Tracked in [issue #4](https://github.com/priyanshujain/sanderling/issues/4).
+9 -9
View File
@@ -9,10 +9,10 @@ Install the CLI, link the SDK into your debug build, run a spec.
## Prerequisites ## Prerequisites
- An Android emulator with API level 30 or newer. - An Android emulator with API level 30 or newer.
- The app under test built as a debug variant with the uatu Android SDK linked in. - The app under test built as a debug variant with the sanderling Android SDK linked in.
- `adb` on your PATH. - `adb` on your PATH.
Run `uatu doctor` to check the host environment. Run `sanderling doctor` to check the host environment.
## Install ## Install
@@ -21,15 +21,15 @@ Run `uatu doctor` to check the host environment.
macOS arm64: macOS arm64:
```sh ```sh
curl -L https://github.com/priyanshujain/uatu/releases/latest/download/uatu_<version>_darwin_arm64.tar.gz | tar xz curl -L https://github.com/priyanshujain/sanderling/releases/latest/download/sanderling_<version>_darwin_arm64.tar.gz | tar xz
./uatu version ./sanderling version
``` ```
Linux amd64: Linux amd64:
```sh ```sh
curl -L https://github.com/priyanshujain/uatu/releases/latest/download/uatu_<version>_linux_amd64.tar.gz | tar xz curl -L https://github.com/priyanshujain/sanderling/releases/latest/download/sanderling_<version>_linux_amd64.tar.gz | tar xz
./uatu version ./sanderling version
``` ```
Pre-built for `darwin/arm64`, `darwin/amd64`, `linux/amd64`, `linux/arm64`. Pre-built for `darwin/arm64`, `darwin/amd64`, `linux/amd64`, `linux/arm64`.
@@ -37,7 +37,7 @@ Pre-built for `darwin/arm64`, `darwin/amd64`, `linux/amd64`, `linux/arm64`.
### Spec package (npm) ### Spec package (npm)
```sh ```sh
npm install --save-dev @uatu/spec npm install --save-dev @sanderling/spec
``` ```
### Android SDK (Maven Central) ### Android SDK (Maven Central)
@@ -55,14 +55,14 @@ The repo ships a working sample at `examples/folio`. From that directory:
```sh ```sh
npm install npm install
(cd android && ./gradlew installDebug) (cd android && ./gradlew installDebug)
uatu test \ sanderling test \
--spec spec.ts \ --spec spec.ts \
--bundle-id app.folio \ --bundle-id app.folio \
--platform android \ --platform android \
--duration 2m --duration 2m
``` ```
Pass `--avd <name>` only when no device is connected and you have multiple AVDs; otherwise uatu uses the connected device or boots the single AVD it finds. Pass `--avd <name>` only when no device is connected and you have multiple AVDs; otherwise sanderling uses the connected device or boots the single AVD it finds.
When the run ends, the trace lands in `runs/<timestamp>/`: When the run ends, the trace lands in `runs/<timestamp>/`:
+6 -6
View File
@@ -1,13 +1,13 @@
--- ---
title: uatu inspect title: sanderling inspect
--- ---
# uatu inspect # sanderling inspect
`uatu inspect` is a local web UI for exploring runs produced by `uatu test`. It reads `runs/<id>/meta.json` and `runs/<id>/trace.jsonl` and renders each step with its action, screenshot, snapshots, residual formulas, and exceptions. `sanderling inspect` is a local web UI for exploring runs produced by `sanderling test`. It reads `runs/<id>/meta.json` and `runs/<id>/trace.jsonl` and renders each step with its action, screenshot, snapshots, residual formulas, and exceptions.
``` ```
uatu inspect [run-or-runs-dir] [--port N] [--no-open] [--dev] sanderling inspect [run-or-runs-dir] [--port N] [--no-open] [--dev]
``` ```
The positional argument can be either a runs directory or a single run directory (auto-detected by the presence of `meta.json`). When omitted, it defaults to `./runs`. The positional argument can be either a runs directory or a single run directory (auto-detected by the presence of `meta.json`). When omitted, it defaults to `./runs`.
@@ -51,11 +51,11 @@ Two-process loop:
``` ```
make web-dev # bun + vite, http://127.0.0.1:5173 make web-dev # bun + vite, http://127.0.0.1:5173
make inspect-dev # uatu inspect --dev, proxies non-API to 5173 make inspect-dev # sanderling inspect --dev, proxies non-API to 5173
``` ```
For a single binary with embedded assets: For a single binary with embedded assets:
``` ```
make uatu # builds web/dist, copies to internal/inspect/dist, then go build make sanderling # builds web/dist, copies to internal/inspect/dist, then go build
``` ```
+4 -4
View File
@@ -4,14 +4,14 @@ title: Runs
# What is a run? # What is a run?
One `uatu test` invocation. Fresh install, spec-driven exploration, then the trace lands in `runs/<timestamp>/`. Typically minutes to hours, not seconds. One `sanderling test` invocation. Fresh install, spec-driven exploration, then the trace lands in `runs/<timestamp>/`. Typically minutes to hours, not seconds.
A run is not analogous to a unit test. A closer framing is: boot a fuzzer for an hour and see what breaks. Violations are recorded in the trace and exploration continues, so one run can surface many bugs. A run is not analogous to a unit test. A closer framing is: boot a fuzzer for an hour and see what breaks. Violations are recorded in the trace and exploration continues, so one run can surface many bugs.
## Lifecycle ## Lifecycle
``` ```
uatu test --spec spec.ts --bundle-id com.example.app --duration 30m sanderling test --spec spec.ts --bundle-id com.example.app --duration 30m
│ │
├── uninstall and reinstall the app (clean slate, every run) ├── uninstall and reinstall the app (clean slate, every run)
├── boot the sidecar, connect the agent socket ├── boot the sidecar, connect the agent socket
@@ -28,7 +28,7 @@ uatu test --spec spec.ts --bundle-id com.example.app --duration 30m
## Why runs are long and linear ## Why runs are long and linear
uatu does not restart the app every N steps. Each restart throws away two things. sanderling does not restart the app every N steps. Each restart throws away two things.
**Novelty and coverage signal.** The exploration strategy weights actions by whether they reach previously unseen state. Restarting resets that history. **Novelty and coverage signal.** The exploration strategy weights actions by whether they reach previously unseen state. Restarting resets that history.
@@ -62,4 +62,4 @@ A run ends when either of these happens.
The trace is written incrementally, so an interrupted run is still fully inspectable. The trace is written incrementally, so an interrupted run is still fully inspectable.
Additional termination conditions (`--max-steps`, `--exit-on-violation`, hard crash handling) land in [v0.1.0](https://github.com/priyanshujain/uatu/issues/4). Additional termination conditions (`--max-steps`, `--exit-on-violation`, hard crash handling) land in [v0.1.0](https://github.com/priyanshujain/sanderling/issues/4).
+5 -5
View File
@@ -7,7 +7,7 @@ title: Writing specs
A spec has three parts: extractors, properties, and actions. A spec has three parts: extractors, properties, and actions.
```ts ```ts
import { extract, always, actions, weighted, Tap, taps, swipes } from "@uatu/spec"; import { extract, always, actions, weighted, Tap, taps, swipes } from "@sanderling/spec";
// 1. Extractors pull values from each observed state. // 1. Extractors pull values from each observed state.
const loggedIn = extract((s) => !!s.ax.find("id:home-tab-bar")); const loggedIn = extract((s) => !!s.ax.find("id:home-tab-bar"));
@@ -18,7 +18,7 @@ export const properties = {
cartNeverNegative: always(() => cartCount.current >= 0), cartNeverNegative: always(() => cartCount.current >= 0),
}; };
// 3. Actions are a weighted tree of what uatu is allowed to do. // 3. Actions are a weighted tree of what sanderling is allowed to do.
export const actions = weighted( export const actions = weighted(
[10, taps], [10, taps],
[2, swipes], [2, swipes],
@@ -45,11 +45,11 @@ interface State {
`ax.find("text:Click me")`, `ax.find("id:login-form")`, `ax.findAll("role:todo-row")` are the common accessors. Prefer stable testID-style identifiers over positional selectors, for the same reason you would in Espresso or XCUITest. `ax.find("text:Click me")`, `ax.find("id:login-form")`, `ax.findAll("role:todo-row")` are the common accessors. Prefer stable testID-style identifiers over positional selectors, for the same reason you would in Espresso or XCUITest.
`snapshots` is populated by the in-app SDK via `Uatu.extract("name") { value }`. Use it when the UI does not expose a value you need, such as business-logic state or hidden fields. `snapshots` is populated by the in-app SDK via `Sanderling.extract("name") { value }`. Use it when the UI does not expose a value you need, such as business-logic state or hidden fields.
## Pattern: preconditions (login, onboarding) ## Pattern: preconditions (login, onboarding)
uatu has no setup phase and no fixtures. Preconditions are action generators with two properties: sanderling has no setup phase and no fixtures. Preconditions are action generators with two properties:
1. High weight, so they fire whenever applicable. 1. High weight, so they fire whenever applicable.
2. Gated on a state extractor, so they return an empty tree when not applicable and self-disable once the precondition is met. 2. Gated on a state extractor, so they return an empty tree when not applicable and self-disable once the precondition is met.
@@ -145,7 +145,7 @@ When the UI does not expose a value but the app knows it, use the SDK's extracto
```kotlin ```kotlin
// in the app (Android) // in the app (Android)
Uatu.extract("cart_count") { store.cart.size } Sanderling.extract("cart_count") { store.cart.size }
``` ```
```ts ```ts
+2 -2
View File
@@ -6,7 +6,7 @@ iosApp/build/
iosApp/iosApp.xcodeproj/ iosApp/iosApp.xcodeproj/
iosApp/iosApp.xcodeproj/** iosApp/iosApp.xcodeproj/**
.DS_Store .DS_Store
uatu/node_modules/ sanderling/node_modules/
uatu/runs/ sanderling/runs/
.playwright-mcp/ .playwright-mcp/
web-*.png web-*.png
+9 -9
View File
@@ -3,14 +3,14 @@
A minimal Kotlin Multiplatform personal-ledger app: login with demo A minimal Kotlin Multiplatform personal-ledger app: login with demo
credentials, create accounts, add credits and debits. Shared UI across credentials, create accounts, add credits and debits. Shared UI across
Android, iOS, and Web via Compose Multiplatform. Doubles as the example Android, iOS, and Web via Compose Multiplatform. Doubles as the example
uatu runs its property-based specs against. sanderling runs its property-based specs against.
## Stack ## Stack
- Kotlin Multiplatform + Compose Multiplatform (shared UI) - Kotlin Multiplatform + Compose Multiplatform (shared UI)
- kotlinx.serialization for file-backed persistence - kotlinx.serialization for file-backed persistence
- kotlinx.coroutines for state flows - kotlinx.coroutines for state flows
- uatu `sdk-android` for harness integration on Android - sanderling `sdk-android` for harness integration on Android
## Prerequisites ## Prerequisites
@@ -46,13 +46,13 @@ email: [email protected]
password: ledger123 password: ledger123
``` ```
## Run a uatu test (Android) ## Run a sanderling test (Android)
```sh ```sh
just test just test
``` ```
If no device is connected, uatu boots the single AVD it finds. With multiple If no device is connected, sanderling boots the single AVD it finds. With multiple
AVDs, pick one: AVDs, pick one:
```sh ```sh
@@ -66,13 +66,13 @@ AVD=Pixel_7
DURATION=5m DURATION=5m
``` ```
Traces land in `./uatu/runs/<timestamp>/`. Traces land in `./sanderling/runs/<timestamp>/`.
## How it connects to uatu ## How it connects to sanderling
- `composeApp/src/androidMain/.../FolioApplication.kt` calls `Uatu.start(this)` - `composeApp/src/androidMain/.../FolioApplication.kt` calls `Sanderling.start(this)`
and registers snapshot extractors (`logged_in`, `account_count`, and registers snapshot extractors (`logged_in`, `account_count`,
`total_balance`, `route`) `total_balance`, `route`)
- `uatu/spec.ts` imports `@uatu/spec`, reads those snapshots, asserts - `sanderling/spec.ts` imports `@sanderling/spec`, reads those snapshots, asserts
properties, and weights the actions the fuzzer picks from properties, and weights the actions the fuzzer picks from
- `just test` invokes `uatu test` against the installed APK - `just test` invokes `sanderling test` against the installed APK
+3 -3
View File
@@ -10,7 +10,7 @@ plugins {
id("app.cash.sqldelight") id("app.cash.sqldelight")
} }
val uatuVersion = findProperty("uatu.version") as String? ?: "0.0.0-dev" val sanderlingVersion = findProperty("sanderling.version") as String? ?: "0.0.0-dev"
val sqldelightVersion = "2.3.2" val sqldelightVersion = "2.3.2"
kotlin { kotlin {
@@ -83,7 +83,7 @@ kotlin {
androidMain.dependencies { androidMain.dependencies {
implementation("androidx.activity:activity-compose:1.13.0") implementation("androidx.activity:activity-compose:1.13.0")
implementation("app.cash.sqldelight:android-driver:$sqldelightVersion") implementation("app.cash.sqldelight:android-driver:$sqldelightVersion")
implementation("io.github.priyanshujain:sdk-android:$uatuVersion") implementation("io.github.priyanshujain:sdk-android:$sanderlingVersion")
} }
iosMain.dependencies { iosMain.dependencies {
@@ -101,7 +101,7 @@ android {
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = uatuVersion versionName = sanderlingVersion
} }
compileOptions { compileOptions {
@@ -11,18 +11,18 @@ import app.folio.navigation.Navigator
import app.folio.navigation.Route import app.folio.navigation.Route
import app.folio.platform.balanceOf import app.folio.platform.balanceOf
import app.folio.platform.signedAmount import app.folio.platform.signedAmount
import dev.uatu.sdk.Uatu import dev.sanderling.sdk.Sanderling
class FolioApplication : Application() { class FolioApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
AndroidLedgerContext.context = applicationContext AndroidLedgerContext.context = applicationContext
Repository.init() Repository.init()
Uatu.start(this) Sanderling.start(this)
Uatu.extract("logged_in") { Repository.session.value != null } Sanderling.extract("logged_in") { Repository.session.value != null }
Uatu.extract("account_count") { Repository.accounts.value.size } Sanderling.extract("account_count") { Repository.accounts.value.size }
Uatu.extract("total_balance") { balanceOf(Repository.transactions.value) } Sanderling.extract("total_balance") { balanceOf(Repository.transactions.value) }
Uatu.extract("route") { Sanderling.extract("route") {
when (Navigator.current.value) { when (Navigator.current.value) {
Route.Login -> "login" Route.Login -> "login"
Route.Home -> "home" Route.Home -> "home"
@@ -31,10 +31,10 @@ class FolioApplication : Application() {
is Route.AddTransaction -> "add-transaction" is Route.AddTransaction -> "add-transaction"
} }
} }
Uatu.extract("auth_status") { Sanderling.extract("auth_status") {
if (Repository.session.value != null) "logged-in" else "logged-out" if (Repository.session.value != null) "logged-in" else "logged-out"
} }
Uatu.extract("accounts") { Sanderling.extract("accounts") {
val txns = Repository.transactions.value val txns = Repository.transactions.value
Repository.accounts.value.map { a -> Repository.accounts.value.map { a ->
val rows = txns.filter { it.accountId == a.id } val rows = txns.filter { it.accountId == a.id }
@@ -46,14 +46,14 @@ class FolioApplication : Application() {
) )
} }
} }
Uatu.extract("active_account_id") { Sanderling.extract("active_account_id") {
when (val r = Navigator.current.value) { when (val r = Navigator.current.value) {
is Route.Ledger -> r.accountId is Route.Ledger -> r.accountId
is Route.AddTransaction -> r.accountId is Route.AddTransaction -> r.accountId
else -> null else -> null
} }
} }
Uatu.extract("ledger_rows") { Sanderling.extract("ledger_rows") {
val active = when (val r = Navigator.current.value) { val active = when (val r = Navigator.current.value) {
is Route.Ledger -> r.accountId is Route.Ledger -> r.accountId
is Route.AddTransaction -> r.accountId is Route.AddTransaction -> r.accountId
@@ -72,7 +72,7 @@ class FolioApplication : Application() {
) )
} }
} }
Uatu.extract("ledger_balance") { Sanderling.extract("ledger_balance") {
val active = when (val r = Navigator.current.value) { val active = when (val r = Navigator.current.value) {
is Route.Ledger -> r.accountId is Route.Ledger -> r.accountId
is Route.AddTransaction -> r.accountId is Route.AddTransaction -> r.accountId
@@ -81,13 +81,13 @@ class FolioApplication : Application() {
if (active == null) 0L if (active == null) 0L
else balanceOf(Repository.transactions.value.filter { it.accountId == active }) else balanceOf(Repository.transactions.value.filter { it.accountId == active })
} }
Uatu.extract("focused_input") { FocusTracker.current.value } Sanderling.extract("focused_input") { FocusTracker.current.value }
Uatu.extract("txn_form_type") { AddTransactionUiState.txnFormType.value } Sanderling.extract("txn_form_type") { AddTransactionUiState.txnFormType.value }
Uatu.extract("txn_form_account_id") { Sanderling.extract("txn_form_account_id") {
(Navigator.current.value as? Route.AddTransaction)?.accountId (Navigator.current.value as? Route.AddTransaction)?.accountId
} }
Uatu.extract("login_error") { LoginUiState.loginError.value } Sanderling.extract("login_error") { LoginUiState.loginError.value }
Uatu.extract("add_account_error") { AddAccountUiState.addAccountError.value } Sanderling.extract("add_account_error") { AddAccountUiState.addAccountError.value }
Uatu.extract("txn_error") { AddTransactionUiState.txnError.value } Sanderling.extract("txn_error") { AddTransactionUiState.txnError.value }
} }
} }
@@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.browser.localStorage import kotlinx.browser.localStorage
private const val STORAGE_KEY = "uatu.ledger.v1" private const val STORAGE_KEY = "sanderling.ledger.v1"
class WebLedgerStore : LedgerStore { class WebLedgerStore : LedgerStore {
private val _accounts = MutableStateFlow<List<Account>>(emptyList()) private val _accounts = MutableStateFlow<List<Account>>(emptyList())
+3 -3
View File
@@ -5,6 +5,6 @@ kotlin.code.style=official
android.useAndroidX=true android.useAndroidX=true
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
# Pin the uatu SDK version consumed by this app. Override with # Pin the sanderling SDK version consumed by this app. Override with
# `-Puatu.version=<version>`. # `-Psanderling.version=<version>`.
uatu.version=0.0.0-dev sanderling.version=0.0.0-dev
+5 -5
View File
@@ -1,11 +1,11 @@
set shell := ["bash", "-cu"] set shell := ["bash", "-cu"]
set dotenv-load := true set dotenv-load := true
uatu := env_var_or_default("UATU", "uatu") sanderling := env_var_or_default("SANDERLING", "sanderling")
avd := env_var_or_default("AVD", "") avd := env_var_or_default("AVD", "")
duration := env_var_or_default("DURATION", "1m") duration := env_var_or_default("DURATION", "1m")
seed := env_var_or_default("SEED", "0") seed := env_var_or_default("SEED", "0")
output := env_var_or_default("OUTPUT", justfile_directory() / "uatu" / "runs") output := env_var_or_default("OUTPUT", justfile_directory() / "sanderling" / "runs")
ios_device := env_var_or_default("IOS_DEVICE", "iPhone 17 Pro") ios_device := env_var_or_default("IOS_DEVICE", "iPhone 17 Pro")
default: default:
@@ -81,7 +81,7 @@ ios:
xcrun simctl install booted iosApp/build/Build/Products/Debug-iphonesimulator/iosApp.app xcrun simctl install booted iosApp/build/Build/Products/Debug-iphonesimulator/iosApp.app
xcrun simctl launch booted app.folio xcrun simctl launch booted app.folio
# Run 'uatu test' against the folio app. Uses a connected device if one is # Run 'sanderling test' against the folio app. Uses a connected device if one is
# online, otherwise boots AVD=<name> if provided. # online, otherwise boots AVD=<name> if provided.
test: test:
#!/usr/bin/env bash #!/usr/bin/env bash
@@ -90,8 +90,8 @@ test:
if [[ -n "{{avd}}" ]]; then if [[ -n "{{avd}}" ]]; then
avd_flag=(--avd "{{avd}}") avd_flag=(--avd "{{avd}}")
fi fi
"{{uatu}}" test \ "{{sanderling}}" test \
--spec "{{justfile_directory()}}/uatu/spec.ts" \ --spec "{{justfile_directory()}}/sanderling/spec.ts" \
--bundle-id app.folio \ --bundle-id app.folio \
"${avd_flag[@]}" \ "${avd_flag[@]}" \
--duration "{{duration}}" \ --duration "{{duration}}" \
@@ -6,12 +6,12 @@
"": { "": {
"name": "folio", "name": "folio",
"devDependencies": { "devDependencies": {
"@uatu/spec": "next" "@sanderling/spec": "next"
} }
}, },
"node_modules/@uatu/spec": { "node_modules/@sanderling/spec": {
"version": "0.0.1-rc3", "version": "0.0.1-rc3",
"resolved": "https://registry.npmjs.org/@uatu/spec/-/spec-0.0.1-rc3.tgz", "resolved": "https://registry.npmjs.org/@sanderling/spec/-/spec-0.0.1-rc3.tgz",
"integrity": "sha512-mCayssOqM2wfzrQDnFGE8FAlUlDzBl1fx2L/MLDIuPrQHBMlUC350xBB8Z7I/DYI98eCWJMqjtw3iU6yVWKtIQ==", "integrity": "sha512-mCayssOqM2wfzrQDnFGE8FAlUlDzBl1fx2L/MLDIuPrQHBMlUC350xBB8Z7I/DYI98eCWJMqjtw3iU6yVWKtIQ==",
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
+8
View File
@@ -0,0 +1,8 @@
{
"name": "folio",
"private": true,
"description": "Minimal sanderling integration example.",
"devDependencies": {
"@sanderling/spec": "next"
}
}
@@ -13,8 +13,8 @@ import {
taps, taps,
waitOnce, waitOnce,
weighted, weighted,
} from "@uatu/spec"; } from "@sanderling/spec";
import { noUncaughtExceptions } from "@uatu/spec/defaults/properties"; import { noUncaughtExceptions } from "@sanderling/spec/defaults/properties";
interface AccountSnapshot { interface AccountSnapshot {
id: string; id: string;
-8
View File
@@ -1,8 +0,0 @@
{
"name": "folio",
"private": true,
"description": "Minimal uatu integration example.",
"devDependencies": {
"@uatu/spec": "next"
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/priyanshujain/uatu module github.com/priyanshujain/sanderling
go 1.26.1 go 1.26.1
+2 -2
View File
@@ -10,6 +10,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
# Single source of truth at release time is the git tag (e.g. v0.1.0 → 0.1.0), # Single source of truth at release time is the git tag (e.g. v0.1.0 → 0.1.0),
# forwarded via `-Puatu.version=...` or `ORG_GRADLE_PROJECT_uatu.version`. # forwarded via `-Psanderling.version=...` or `ORG_GRADLE_PROJECT_sanderling.version`.
# The default below only applies to untagged local builds. # The default below only applies to untagged local builds.
uatu.version=0.0.0-dev sanderling.version=0.0.0-dev
+7 -7
View File
@@ -47,15 +47,15 @@ func TestBundle_ProducesIIFE(t *testing.T) {
func TestBundle_InlinesDefines(t *testing.T) { func TestBundle_InlinesDefines(t *testing.T) {
entry := writeFixture(t, "spec.ts", ` entry := writeFixture(t, "spec.ts", `
const phone = process.env.UATU_TEST_PHONE; const phone = process.env.SANDERLING_TEST_PHONE;
const otp = process.env.UATU_TEST_OTP; const otp = process.env.SANDERLING_TEST_OTP;
export const credentials = { phone, otp }; export const credentials = { phone, otp };
`) `)
result, err := Bundle(Options{ result, err := Bundle(Options{
EntryFile: entry, EntryFile: entry,
Defines: map[string]string{ Defines: map[string]string{
"UATU_TEST_PHONE": "+919876543210", "SANDERLING_TEST_PHONE": "+919876543210",
"UATU_TEST_OTP": "123456", "SANDERLING_TEST_OTP": "123456",
}, },
}) })
if err != nil { if err != nil {
@@ -87,13 +87,13 @@ func TestBundle_DeterministicHash(t *testing.T) {
func TestBundle_HashChangesWithDefines(t *testing.T) { func TestBundle_HashChangesWithDefines(t *testing.T) {
entry := writeFixture(t, "spec.ts", ` entry := writeFixture(t, "spec.ts", `
export const phone = process.env.UATU_TEST_PHONE; export const phone = process.env.SANDERLING_TEST_PHONE;
`) `)
first, err := Bundle(Options{EntryFile: entry, Defines: map[string]string{"UATU_TEST_PHONE": "1111"}}) first, err := Bundle(Options{EntryFile: entry, Defines: map[string]string{"SANDERLING_TEST_PHONE": "1111"}})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
second, err := Bundle(Options{EntryFile: entry, Defines: map[string]string{"UATU_TEST_PHONE": "2222"}}) second, err := Bundle(Options{EntryFile: entry, Defines: map[string]string{"SANDERLING_TEST_PHONE": "2222"}})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
"github.com/priyanshujain/uatu/internal/driver" "github.com/priyanshujain/sanderling/internal/driver"
driverpb "github.com/priyanshujain/uatu/proto/driverpb" driverpb "github.com/priyanshujain/sanderling/proto/driverpb"
) )
type Client struct { type Client struct {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
driverpb "github.com/priyanshujain/uatu/proto/driverpb" driverpb "github.com/priyanshujain/sanderling/proto/driverpb"
) )
type fakeServer struct { type fakeServer struct {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/priyanshujain/uatu/internal/driver" "github.com/priyanshujain/sanderling/internal/driver"
) )
type ActionKind string type ActionKind string
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/priyanshujain/uatu/internal/driver" "github.com/priyanshujain/sanderling/internal/driver"
) )
func TestNew_Defaults(t *testing.T) { func TestNew_Defaults(t *testing.T) {
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>uatu inspect</title> <title>sanderling inspect</title>
<script type="module" crossorigin src="/assets/index-DFpYQDqN.js"></script> <script type="module" crossorigin src="/assets/index-DFpYQDqN.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BSJUf7yI.css"> <link rel="stylesheet" crossorigin href="/assets/index-BSJUf7yI.css">
</head> </head>
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/priyanshujain/uatu/internal/trace" "github.com/priyanshujain/sanderling/internal/trace"
) )
// Maximum size of a single trace.jsonl line. Hierarchies and snapshots // Maximum size of a single trace.jsonl line. Hierarchies and snapshots
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/priyanshujain/uatu/internal/trace" "github.com/priyanshujain/sanderling/internal/trace"
) )
func TestScan_OrdersByStartedAtDescendingAndCountsViolations(t *testing.T) { func TestScan_OrdersByStartedAtDescendingAndCountsViolations(t *testing.T) {
+1 -1
View File
@@ -20,7 +20,7 @@ type ServerOptions struct {
DevTarget string DevTarget string
} }
// Server holds the HTTP handlers for `uatu inspect`. // Server holds the HTTP handlers for `sanderling inspect`.
type Server struct { type Server struct {
options ServerOptions options ServerOptions
cache *Cache cache *Cache
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/priyanshujain/uatu/internal/trace" "github.com/priyanshujain/sanderling/internal/trace"
) )
func newFixtureServer(t *testing.T) (*Server, string) { func newFixtureServer(t *testing.T) (*Server, string) {
+6 -6
View File
@@ -8,12 +8,12 @@ import (
"log/slog" "log/slog"
"time" "time"
"github.com/priyanshujain/uatu/internal/agent" "github.com/priyanshujain/sanderling/internal/agent"
"github.com/priyanshujain/uatu/internal/driver" "github.com/priyanshujain/sanderling/internal/driver"
"github.com/priyanshujain/uatu/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/uatu/internal/ltl" "github.com/priyanshujain/sanderling/internal/ltl"
"github.com/priyanshujain/uatu/internal/trace" "github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/uatu/internal/verifier" "github.com/priyanshujain/sanderling/internal/verifier"
) )
type Options struct { type Options struct {
+10 -10
View File
@@ -15,19 +15,19 @@ import (
"testing" "testing"
"time" "time"
"github.com/priyanshujain/uatu/internal/agent" "github.com/priyanshujain/sanderling/internal/agent"
mockdriver "github.com/priyanshujain/uatu/internal/driver/mock" mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
"github.com/priyanshujain/uatu/internal/trace" "github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/uatu/internal/verifier" "github.com/priyanshujain/sanderling/internal/verifier"
) )
const fixtureSpec = ` const fixtureSpec = `
const screen = __uatu__.extract(state => state.snapshots.screen ?? ""); const screen = __sanderling__.extract(state => state.snapshots.screen ?? "");
const balance = __uatu__.extract(state => state.snapshots.balance ?? 0); const balance = __sanderling__.extract(state => state.snapshots.balance ?? 0);
globalThis.properties = { globalThis.properties = {
balanceNonNegative: __uatu__.always(() => balance.current >= 0), balanceNonNegative: __sanderling__.always(() => balance.current >= 0),
}; };
globalThis.actions = __uatu__.actions(() => [__uatu__.tap({ on: "id:next" })]); globalThis.actions = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:next" })]);
` `
type harness struct { type harness struct {
@@ -195,9 +195,9 @@ func TestRunner_ViolationSurfacesInSummary(t *testing.T) {
func TestRunner_ThrowingPredicateIsLoggedNotPanic(t *testing.T) { func TestRunner_ThrowingPredicateIsLoggedNotPanic(t *testing.T) {
const throwingSpec = ` const throwingSpec = `
globalThis.properties = { globalThis.properties = {
broken: __uatu__.always(() => { throw new Error("bad predicate"); }), broken: __sanderling__.always(() => { throw new Error("bad predicate"); }),
}; };
globalThis.actions = __uatu__.actions(() => [__uatu__.tap({ on: "id:next" })]); globalThis.actions = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:next" })]);
` `
state := newHarnessWithSpec(t, []map[string]json.RawMessage{{}, {}}, throwingSpec) state := newHarnessWithSpec(t, []map[string]json.RawMessage{{}, {}}, throwingSpec)
state.startSDK(t) state.startSDK(t)
+2 -2
View File
@@ -23,12 +23,12 @@ func EmbeddedSHA256() string {
// matching checksum, no rewrite happens. Returns the JAR path. // matching checksum, no rewrite happens. Returns the JAR path.
func Extract(dir string) (string, error) { func Extract(dir string) (string, error) {
if len(embeddedJAR) == 0 { if len(embeddedJAR) == 0 {
return "", errors.New("sidecar: binary built without -tags withsidecar; rebuild with `make uatu`") return "", errors.New("sidecar: binary built without -tags withsidecar; rebuild with `make sanderling`")
} }
if err := os.MkdirAll(dir, 0o755); err != nil { if err := os.MkdirAll(dir, 0o755); err != nil {
return "", fmt.Errorf("mkdir %s: %w", dir, err) return "", fmt.Errorf("mkdir %s: %w", dir, err)
} }
jarPath := filepath.Join(dir, "uatu-sidecar.jar") jarPath := filepath.Join(dir, "sanderling-sidecar.jar")
checksumPath := jarPath + ".sha256" checksumPath := jarPath + ".sha256"
checksum := EmbeddedSHA256() checksum := EmbeddedSHA256()
+1 -1
View File
@@ -5,6 +5,6 @@ package sidecar
var embeddedJAR []byte var embeddedJAR []byte
// IsPlaceholder reports whether the binary was built without the real // IsPlaceholder reports whether the binary was built without the real
// sidecar JAR embedded. Build with `make uatu` (which passes // sidecar JAR embedded. Build with `make sanderling` (which passes
// -tags withsidecar) to embed the real fat JAR. // -tags withsidecar) to embed the real fat JAR.
func IsPlaceholder() bool { return true } func IsPlaceholder() bool { return true }
+1 -1
View File
@@ -25,7 +25,7 @@ func TestExtract_WritesJARAndChecksum(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if path != filepath.Join(directory, "uatu-sidecar.jar") { if path != filepath.Join(directory, "sanderling-sidecar.jar") {
t.Errorf("unexpected path: %s", path) t.Errorf("unexpected path: %s", path)
} }
body, err := os.ReadFile(path) body, err := os.ReadFile(path)
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/priyanshujain/uatu/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/hierarchy"
) )
type Step struct { type Step struct {
@@ -74,7 +74,7 @@ type Meta struct {
BundleID string `json:"bundle_id"` BundleID string `json:"bundle_id"`
StartedAt time.Time `json:"started_at"` StartedAt time.Time `json:"started_at"`
EndedAt *time.Time `json:"ended_at,omitempty"` EndedAt *time.Time `json:"ended_at,omitempty"`
UatuVersion string `json:"uatu_version"` SanderlingVersion string `json:"sanderling_version"`
} }
type Writer struct { type Writer struct {
+2 -2
View File
@@ -25,7 +25,7 @@ func TestWriteMeta_RoundTrip(t *testing.T) {
Platform: "android", Platform: "android",
BundleID: "in.okcredit.merchant", BundleID: "in.okcredit.merchant",
StartedAt: time.Date(2026, 4, 17, 22, 30, 0, 0, time.UTC), StartedAt: time.Date(2026, 4, 17, 22, 30, 0, 0, time.UTC),
UatuVersion: "0.0.1", SanderlingVersion: "0.0.1",
} }
if err := writer.WriteMeta(meta); err != nil { if err := writer.WriteMeta(meta); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -60,7 +60,7 @@ func TestWriteMeta_EndedAtRoundTrip(t *testing.T) {
BundleID: "in.test", BundleID: "in.test",
StartedAt: time.Date(2026, 4, 17, 22, 30, 0, 0, time.UTC), StartedAt: time.Date(2026, 4, 17, 22, 30, 0, 0, time.UTC),
EndedAt: &endedAt, EndedAt: &endedAt,
UatuVersion: "0.0.1", SanderlingVersion: "0.0.1",
} }
if err := writer.WriteMeta(meta); err != nil { if err := writer.WriteMeta(meta); err != nil {
t.Fatal(err) t.Fatal(err)
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/priyanshujain/uatu/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/hierarchy"
) )
// TestStateAxFindWorks verifies that a Parse+PushSnapshot+extract round trip // TestStateAxFindWorks verifies that a Parse+PushSnapshot+extract round trip
@@ -24,11 +24,11 @@ func TestStateAxFindWorks(t *testing.T) {
verifier := newVerifier(t) verifier := newVerifier(t)
mustLoad(t, verifier, ` mustLoad(t, verifier, `
globalThis.probe = __uatu__.extract(state => { globalThis.probe = __sanderling__.extract(state => {
const element = state.ax.find("id:select_language"); const element = state.ax.find("id:select_language");
return element ? "matched:" + element.text : "miss"; return element ? "matched:" + element.text : "miss";
}); });
globalThis.count = __uatu__.extract(state => state.ax.findAll("id:select_language").length); globalThis.count = __sanderling__.extract(state => state.ax.findAll("id:select_language").length);
`) `)
if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}); err != nil { if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}, Tree: tree}); err != nil {
+29 -29
View File
@@ -80,11 +80,11 @@ type formulaSpec struct {
} }
const ( const (
tagFormula = "__uatuFormula" tagFormula = "__sanderlingFormula"
tagFormulaSpecIndex = "__uatuFormulaSpec" tagFormulaSpecIndex = "__sanderlingFormulaSpec"
tagActionGenerator = "__uatuActionGenerator" tagActionGenerator = "__sanderlingActionGenerator"
tagInternalKind = "__uatuKind" tagInternalKind = "__sanderlingKind"
tagSelector = "__uatuSelector" tagSelector = "__sanderlingSelector"
internalKindActions = "actions" internalKindActions = "actions"
internalKindWeighted = "weighted" internalKindWeighted = "weighted"
@@ -94,63 +94,63 @@ const (
internalKindBuiltinPressKey = "pressKey" internalKindBuiltinPressKey = "pressKey"
) )
// installRuntimeBindings exposes globalThis.__uatu__ to the loaded spec. // installRuntimeBindings exposes globalThis.__sanderling__ to the loaded spec.
func (v *Verifier) installRuntimeBindings() error { func (v *Verifier) installRuntimeBindings() error {
uatu := v.runtime.NewObject() sanderling := v.runtime.NewObject()
if err := uatu.Set("extract", v.bindExtract); err != nil { if err := sanderling.Set("extract", v.bindExtract); err != nil {
return err return err
} }
if err := uatu.Set("always", v.bindAlways); err != nil { if err := sanderling.Set("always", v.bindAlways); err != nil {
return err return err
} }
if err := uatu.Set("now", v.bindNow); err != nil { if err := sanderling.Set("now", v.bindNow); err != nil {
return err return err
} }
if err := uatu.Set("next", v.bindNext); err != nil { if err := sanderling.Set("next", v.bindNext); err != nil {
return err return err
} }
if err := uatu.Set("eventually", v.bindEventually); err != nil { if err := sanderling.Set("eventually", v.bindEventually); err != nil {
return err return err
} }
if err := uatu.Set("actions", v.bindActions); err != nil { if err := sanderling.Set("actions", v.bindActions); err != nil {
return err return err
} }
if err := uatu.Set("weighted", v.bindWeighted); err != nil { if err := sanderling.Set("weighted", v.bindWeighted); err != nil {
return err return err
} }
if err := uatu.Set("from", v.bindFrom); err != nil { if err := sanderling.Set("from", v.bindFrom); err != nil {
return err return err
} }
if err := uatu.Set("tap", v.bindTap); err != nil { if err := sanderling.Set("tap", v.bindTap); err != nil {
return err return err
} }
if err := uatu.Set("inputText", v.bindInputText); err != nil { if err := sanderling.Set("inputText", v.bindInputText); err != nil {
return err return err
} }
if err := uatu.Set("swipe", v.bindSwipe); err != nil { if err := sanderling.Set("swipe", v.bindSwipe); err != nil {
return err return err
} }
if err := uatu.Set("pressKey", v.bindPressKey); err != nil { if err := sanderling.Set("pressKey", v.bindPressKey); err != nil {
return err return err
} }
if err := uatu.Set("wait", v.bindWait); err != nil { if err := sanderling.Set("wait", v.bindWait); err != nil {
return err return err
} }
if err := uatu.Set("taps", v.builtinGenerator(internalKindBuiltinTaps)); err != nil { if err := sanderling.Set("taps", v.builtinGenerator(internalKindBuiltinTaps)); err != nil {
return err return err
} }
if err := uatu.Set("swipes", v.builtinGenerator(internalKindBuiltinSwipes)); err != nil { if err := sanderling.Set("swipes", v.builtinGenerator(internalKindBuiltinSwipes)); err != nil {
return err return err
} }
if err := uatu.Set("waitOnce", v.builtinGenerator(internalKindBuiltinWaitOnce)); err != nil { if err := sanderling.Set("waitOnce", v.builtinGenerator(internalKindBuiltinWaitOnce)); err != nil {
return err return err
} }
if err := uatu.Set("pressKeys", v.builtinGenerator(internalKindBuiltinPressKey)); err != nil { if err := sanderling.Set("pressKeys", v.builtinGenerator(internalKindBuiltinPressKey)); err != nil {
return err return err
} }
return v.runtime.GlobalObject().Set("__uatu__", uatu) return v.runtime.GlobalObject().Set("__sanderling__", sanderling)
} }
func (v *Verifier) bindExtract(call goja.FunctionCall) goja.Value { func (v *Verifier) bindExtract(call goja.FunctionCall) goja.Value {
@@ -172,7 +172,7 @@ func (v *Verifier) bindExtract(call goja.FunctionCall) goja.Value {
// bindAlways accepts either a predicate function (legacy shape) or a formula // bindAlways accepts either a predicate function (legacy shape) or a formula
// handle (new shape). Both produce a formula handle tagged with // handle (new shape). Both produce a formula handle tagged with
// __uatuFormulaSpec. // __sanderlingFormulaSpec.
func (v *Verifier) bindAlways(call goja.FunctionCall) goja.Value { func (v *Verifier) bindAlways(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 1 { if len(call.Arguments) != 1 {
panic(v.runtime.NewTypeError("always requires exactly one argument")) panic(v.runtime.NewTypeError("always requires exactly one argument"))
@@ -258,9 +258,9 @@ func (v *Verifier) formulaHandle(kind specKind, index int) *goja.Object {
handle := v.runtime.NewObject() handle := v.runtime.NewObject()
_ = handle.Set(tagFormula, true) _ = handle.Set(tagFormula, true)
_ = handle.Set(tagFormulaSpecIndex, index) _ = handle.Set(tagFormulaSpecIndex, index)
// Keep __uatuIndex as an alias so older property shapes that read it keep // Keep __sanderlingIndex as an alias so older property shapes that read it keep
// working during backward-compat transitions. // working during backward-compat transitions.
_ = handle.Set("__uatuIndex", index) _ = handle.Set("__sanderlingIndex", index)
_ = handle.Set("implies", v.binaryChain(index, specKindImplies)) _ = handle.Set("implies", v.binaryChain(index, specKindImplies))
_ = handle.Set("or", v.binaryChain(index, specKindOr)) _ = handle.Set("or", v.binaryChain(index, specKindOr))
@@ -326,7 +326,7 @@ func (v *Verifier) eventuallyWithin(selfIndex int) func(call goja.FunctionCall)
} }
} }
// extractSpecIndex reads __uatuFormulaSpec from a JS formula handle. // extractSpecIndex reads __sanderlingFormulaSpec from a JS formula handle.
func (v *Verifier) extractSpecIndex(value goja.Value) (int, bool) { func (v *Verifier) extractSpecIndex(value goja.Value) (int, bool) {
if value == nil || goja.IsNull(value) || goja.IsUndefined(value) { if value == nil || goja.IsNull(value) || goja.IsUndefined(value) {
return 0, false return 0, false
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/priyanshujain/uatu/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/hierarchy"
) )
// Snapshots is the per-step extractor output forwarded by the SDK. // Snapshots is the per-step extractor output forwarded by the SDK.
+6 -6
View File
@@ -5,13 +5,13 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/priyanshujain/uatu/internal/bundler" "github.com/priyanshujain/sanderling/internal/bundler"
"github.com/priyanshujain/uatu/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/uatu/internal/ltl" "github.com/priyanshujain/sanderling/internal/ltl"
) )
// bundleIntegrationSpec bundles testdata/integration_spec.ts via the real // bundleIntegrationSpec bundles testdata/integration_spec.ts via the real
// @uatu/spec API so the integration test exercises the same path the CLI // @sanderling/spec API so the integration test exercises the same path the CLI
// uses, with no reference to any specific example app. // uses, with no reference to any specific example app.
func bundleIntegrationSpec(t *testing.T) string { func bundleIntegrationSpec(t *testing.T) string {
t.Helper() t.Helper()
@@ -30,8 +30,8 @@ func bundleIntegrationSpec(t *testing.T) string {
bundle, err := bundler.Bundle(bundler.Options{ bundle, err := bundler.Bundle(bundler.Options{
EntryFile: specPath, EntryFile: specPath,
Aliases: map[string]string{ Aliases: map[string]string{
"@uatu/spec": apiPath, "@sanderling/spec": apiPath,
"@uatu/spec/defaults/properties": defaultsPath, "@sanderling/spec/defaults/properties": defaultsPath,
}, },
}) })
if err != nil { if err != nil {
+2 -2
View File
@@ -6,8 +6,8 @@ import {
eventually, eventually,
extract, extract,
weighted, weighted,
} from "@uatu/spec"; } from "@sanderling/spec";
import { noUncaughtExceptions } from "@uatu/spec/defaults/properties"; import { noUncaughtExceptions } from "@sanderling/spec/defaults/properties";
const route = extract<string>( const route = extract<string>(
(state) => (state.snapshots.route as string) ?? "", (state) => (state.snapshots.route as string) ?? "",
+14 -14
View File
@@ -7,7 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/priyanshujain/uatu/internal/ltl" "github.com/priyanshujain/sanderling/internal/ltl"
) )
func newVerifier(t *testing.T, options ...Option) *Verifier { func newVerifier(t *testing.T, options ...Option) *Verifier {
@@ -27,18 +27,18 @@ func mustLoad(t *testing.T, verifier *Verifier, source string) {
} }
const helloSpec = ` const helloSpec = `
const screen = __uatu__.extract(state => state.snapshots.screen ?? ""); const screen = __sanderling__.extract(state => state.snapshots.screen ?? "");
const balance = __uatu__.extract(state => state.snapshots["ledger.balance"] ?? 0); const balance = __sanderling__.extract(state => state.snapshots["ledger.balance"] ?? 0);
globalThis.screen = screen; globalThis.screen = screen;
globalThis.balance = balance; globalThis.balance = balance;
globalThis.properties = { globalThis.properties = {
balanceNonNegative: __uatu__.always(() => balance.current >= 0), balanceNonNegative: __sanderling__.always(() => balance.current >= 0),
}; };
globalThis.actions = __uatu__.actions(() => [ globalThis.actions = __sanderling__.actions(() => [
__uatu__.tap({ on: "id:home_button" }), __sanderling__.tap({ on: "id:home_button" }),
]); ]);
` `
@@ -135,9 +135,9 @@ func TestNextAction_FromActionsGenerator(t *testing.T) {
func TestNextAction_WeightedSelectsByWeight(t *testing.T) { func TestNextAction_WeightedSelectsByWeight(t *testing.T) {
verifier := newVerifier(t, WithRand(rand.New(rand.NewPCG(42, 0)))) verifier := newVerifier(t, WithRand(rand.New(rand.NewPCG(42, 0))))
mustLoad(t, verifier, ` mustLoad(t, verifier, `
const tapHome = __uatu__.actions(() => [__uatu__.tap({ on: "id:home" })]); const tapHome = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:home" })]);
const tapAway = __uatu__.actions(() => [__uatu__.tap({ on: "id:away" })]); const tapAway = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:away" })]);
globalThis.actions = __uatu__.weighted( globalThis.actions = __sanderling__.weighted(
[1, tapHome], [1, tapHome],
[99, tapAway], [99, tapAway],
); );
@@ -166,7 +166,7 @@ func TestNextAction_WeightedSelectsByWeight(t *testing.T) {
func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) { func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) {
verifier := newVerifier(t) verifier := newVerifier(t)
mustLoad(t, verifier, ` mustLoad(t, verifier, `
globalThis.actions = __uatu__.actions(() => []); globalThis.actions = __sanderling__.actions(() => []);
`) `)
_ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}})
@@ -179,8 +179,8 @@ func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) {
func TestInputText_RoundTrip(t *testing.T) { func TestInputText_RoundTrip(t *testing.T) {
verifier := newVerifier(t) verifier := newVerifier(t)
mustLoad(t, verifier, ` mustLoad(t, verifier, `
globalThis.actions = __uatu__.actions(() => [ globalThis.actions = __sanderling__.actions(() => [
__uatu__.inputText({ into: "id:phone", text: "+919876543210" }), __sanderling__.inputText({ into: "id:phone", text: "+919876543210" }),
]); ]);
`) `)
_ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}})
@@ -200,7 +200,7 @@ func TestInputText_RoundTrip(t *testing.T) {
func TestPushSnapshot_FeedsSnapshotsToExtractorState(t *testing.T) { func TestPushSnapshot_FeedsSnapshotsToExtractorState(t *testing.T) {
verifier := newVerifier(t) verifier := newVerifier(t)
mustLoad(t, verifier, ` mustLoad(t, verifier, `
globalThis.captured = __uatu__.extract(state => state.snapshots["k"]); globalThis.captured = __sanderling__.extract(state => state.snapshots["k"]);
`) `)
if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"k": json.RawMessage(`"hello"`)}}); err != nil { if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"k": json.RawMessage(`"hello"`)}}); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -222,7 +222,7 @@ func TestLoad_PropagatesSyntaxError(t *testing.T) {
func TestEvaluateProperties_ThrowingPredicateDoesNotPanic(t *testing.T) { func TestEvaluateProperties_ThrowingPredicateDoesNotPanic(t *testing.T) {
const spec = ` const spec = `
globalThis.properties = { globalThis.properties = {
broken: __uatu__.always(() => { throw new Error("bad predicate"); }), broken: __sanderling__.always(() => { throw new Error("bad predicate"); }),
}; };
` `
verifier := newVerifier(t) verifier := newVerifier(t)
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/priyanshujain/uatu/internal/hierarchy" "github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/uatu/internal/ltl" "github.com/priyanshujain/sanderling/internal/ltl"
) )
type Verifier struct { type Verifier struct {
+6 -6
View File
@@ -1,19 +1,19 @@
# @uatu/spec # @sanderling/spec
TypeScript spec API for [uatu](https://github.com/priyanshujain/uatu), a property-based UI fuzzer for mobile apps. TypeScript spec API for [sanderling](https://github.com/priyanshujain/sanderling), a property-based UI fuzzer for mobile apps.
Spec authors write specs in TypeScript that describe what an app should *always* do (safety invariants), generate weighted actions to exercise the app, and extract structured state from the accessibility tree. The `uatu` CLI picks up the spec and drives the app under test. Spec authors write specs in TypeScript that describe what an app should *always* do (safety invariants), generate weighted actions to exercise the app, and extract structured state from the accessibility tree. The `sanderling` CLI picks up the spec and drives the app under test.
## Install ## Install
```sh ```sh
npm install --save-dev @uatu/spec npm install --save-dev @sanderling/spec
``` ```
## Usage ## Usage
```ts ```ts
import { extract, always, actions, Tap, weighted } from "@uatu/spec"; import { extract, always, actions, Tap, weighted } from "@sanderling/spec";
export const spec = { export const spec = {
extract: extract((tree) => ({ extract: extract((tree) => ({
@@ -32,7 +32,7 @@ export const spec = {
## Version compatibility ## Version compatibility
`@uatu/spec` is released in lockstep with the uatu CLI. Pin the same major/minor version as your installed `uatu` binary. `@sanderling/spec` is released in lockstep with the sanderling CLI. Pin the same major/minor version as your installed `sanderling` binary.
## License ## License
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "@uatu/spec", "name": "@sanderling/spec",
"version": "0.0.0-dev", "version": "0.0.0-dev",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@uatu/spec", "name": "@sanderling/spec",
"version": "0.0.0-dev", "version": "0.0.0-dev",
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
+5 -5
View File
@@ -1,7 +1,7 @@
{ {
"name": "@uatu/spec", "name": "@sanderling/spec",
"version": "0.0.0-dev", "version": "0.0.0-dev",
"description": "TypeScript spec API for uatu, a property-based UI fuzzer for mobile apps.", "description": "TypeScript spec API for sanderling, a property-based UI fuzzer for mobile apps.",
"type": "module", "type": "module",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -21,12 +21,12 @@
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/priyanshujain/uatu.git", "url": "git+https://github.com/priyanshujain/sanderling.git",
"directory": "pkg/spec-api" "directory": "pkg/spec-api"
}, },
"homepage": "https://github.com/priyanshujain/uatu", "homepage": "https://github.com/priyanshujain/sanderling",
"bugs": { "bugs": {
"url": "https://github.com/priyanshujain/uatu/issues" "url": "https://github.com/priyanshujain/sanderling/issues"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"author": "Priyanshu Jain", "author": "Priyanshu Jain",
+9 -9
View File
@@ -14,26 +14,26 @@ import type {
} from "./types.ts"; } from "./types.ts";
export function actions(generator: () => Action[]): ActionGenerator { export function actions(generator: () => Action[]): ActionGenerator {
return globalThis.__uatu__.actions(generator); return globalThis.__sanderling__.actions(generator);
} }
export function weighted(...entries: WeightedEntry[]): ActionGenerator { export function weighted(...entries: WeightedEntry[]): ActionGenerator {
return globalThis.__uatu__.weighted(...entries); return globalThis.__sanderling__.weighted(...entries);
} }
export function from<T>(items: readonly T[]): Sampler<T> { export function from<T>(items: readonly T[]): Sampler<T> {
return globalThis.__uatu__.from(items); return globalThis.__sanderling__.from(items);
} }
export function Tap(parameters: { on: string | AccessibilityElement }): TapAction { export function Tap(parameters: { on: string | AccessibilityElement }): TapAction {
return globalThis.__uatu__.tap(parameters); return globalThis.__sanderling__.tap(parameters);
} }
export function InputText(parameters: { export function InputText(parameters: {
into: string | AccessibilityElement; into: string | AccessibilityElement;
text: string; text: string;
}): InputTextAction { }): InputTextAction {
return globalThis.__uatu__.inputText(parameters); return globalThis.__sanderling__.inputText(parameters);
} }
export function Swipe(parameters: { export function Swipe(parameters: {
@@ -41,21 +41,21 @@ export function Swipe(parameters: {
to: Point | AccessibilityElement; to: Point | AccessibilityElement;
durationMillis?: number; durationMillis?: number;
}): SwipeAction { }): SwipeAction {
return globalThis.__uatu__.swipe(parameters); return globalThis.__sanderling__.swipe(parameters);
} }
export function PressKey(parameters: { key: Key }): PressKeyAction { export function PressKey(parameters: { key: Key }): PressKeyAction {
return globalThis.__uatu__.pressKey(parameters); return globalThis.__sanderling__.pressKey(parameters);
} }
export function Wait(parameters: { durationMillis: number }): WaitAction { export function Wait(parameters: { durationMillis: number }): WaitAction {
return globalThis.__uatu__.wait(parameters); return globalThis.__sanderling__.wait(parameters);
} }
function builtinGenerator(name: "taps" | "swipes" | "waitOnce" | "pressKeys"): ActionGenerator { function builtinGenerator(name: "taps" | "swipes" | "waitOnce" | "pressKeys"): ActionGenerator {
return new Proxy({} as ActionGenerator, { return new Proxy({} as ActionGenerator, {
get(_target, property) { get(_target, property) {
const runtime = globalThis.__uatu__[name] as unknown as Record< const runtime = globalThis.__sanderling__[name] as unknown as Record<
string | symbol, string | symbol,
unknown unknown
>; >;
+1 -1
View File
@@ -4,7 +4,7 @@ import type { Formula } from "../types.ts";
const exceptionCount = extract<number>((state) => state.exceptions.length); const exceptionCount = extract<number>((state) => state.exceptions.length);
// Fails when the SDK captured an uncaught throwable or a Uatu.reportError // Fails when the SDK captured an uncaught throwable or a Sanderling.reportError
// call surfaced one during the run. // call surfaced one during the run.
export const noUncaughtExceptions: Formula = always( export const noUncaughtExceptions: Formula = always(
() => exceptionCount.current === 0, () => exceptionCount.current === 0,
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Extracted, State } from "./types.ts"; import type { Extracted, State } from "./types.ts";
export function extract<T>(getter: (state: State) => T): Extracted<T> { export function extract<T>(getter: (state: State) => T): Extracted<T> {
return globalThis.__uatu__.extract(getter); return globalThis.__sanderling__.extract(getter);
} }
+1 -1
View File
@@ -17,7 +17,7 @@ export type {
State, State,
SwipeAction, SwipeAction,
TapAction, TapAction,
UatuRuntime, SanderlingRuntime,
WaitAction, WaitAction,
WeightedEntry, WeightedEntry,
} from "./types.ts"; } from "./types.ts";
+4 -4
View File
@@ -1,20 +1,20 @@
import type { EventuallyFormula, Formula } from "./types.ts"; import type { EventuallyFormula, Formula } from "./types.ts";
export function always(predicateOrFormula: (() => boolean) | Formula): Formula { export function always(predicateOrFormula: (() => boolean) | Formula): Formula {
return globalThis.__uatu__.always(predicateOrFormula); return globalThis.__sanderling__.always(predicateOrFormula);
} }
export function now(predicate: () => boolean): Formula { export function now(predicate: () => boolean): Formula {
return globalThis.__uatu__.now(predicate); return globalThis.__sanderling__.now(predicate);
} }
export function next(predicate: () => boolean): Formula { export function next(predicate: () => boolean): Formula {
return globalThis.__uatu__.next(predicate); return globalThis.__sanderling__.next(predicate);
} }
// An unbounded `eventually` never forces a violation within a finite run — // An unbounded `eventually` never forces a violation within a finite run —
// prefer `.within(n, unit)` when you want the verifier to fail a property // prefer `.within(n, unit)` when you want the verifier to fail a property
// that stalls. // that stalls.
export function eventually(predicate: () => boolean): EventuallyFormula { export function eventually(predicate: () => boolean): EventuallyFormula {
return globalThis.__uatu__.eventually(predicate); return globalThis.__sanderling__.eventually(predicate);
} }
+4 -4
View File
@@ -78,12 +78,12 @@ export type Key =
| "right"; | "right";
export interface ActionGenerator { export interface ActionGenerator {
readonly __uatuActionGenerator: true; readonly __sanderlingActionGenerator: true;
generate(): Action[]; generate(): Action[];
} }
export interface Formula { export interface Formula {
readonly __uatuFormula: true; readonly __sanderlingFormula: true;
implies(other: Formula): Formula; implies(other: Formula): Formula;
or(other: Formula): Formula; or(other: Formula): Formula;
and(other: Formula): Formula; and(other: Formula): Formula;
@@ -98,7 +98,7 @@ export interface Sampler<T> {
generate(): T; generate(): T;
} }
export interface UatuRuntime { export interface SanderlingRuntime {
extract: <T>(getter: (state: State) => T) => Extracted<T>; extract: <T>(getter: (state: State) => T) => Extracted<T>;
always: (predicateOrFormula: (() => boolean) | Formula) => Formula; always: (predicateOrFormula: (() => boolean) | Formula) => Formula;
now: (predicate: () => boolean) => Formula; now: (predicate: () => boolean) => Formula;
@@ -129,5 +129,5 @@ export type WeightedEntry = readonly [number, ActionGenerator];
declare global { declare global {
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var __uatu__: UatuRuntime; var __sanderling__: SanderlingRuntime;
} }
+18 -18
View File
@@ -29,11 +29,11 @@ import type {
Formula, Formula,
Sampler, Sampler,
State, State,
UatuRuntime, SanderlingRuntime,
WeightedEntry, WeightedEntry,
} from "../src/types.ts"; } from "../src/types.ts";
interface RecordedRuntime extends UatuRuntime { interface RecordedRuntime extends SanderlingRuntime {
extracts: Array<(state: State) => unknown>; extracts: Array<(state: State) => unknown>;
alwaysArgs: Array<(() => boolean) | Formula>; alwaysArgs: Array<(() => boolean) | Formula>;
nowPredicates: Array<() => boolean>; nowPredicates: Array<() => boolean>;
@@ -51,7 +51,7 @@ interface RecordedRuntime extends UatuRuntime {
function makeChainableFormula(record: RecordedRuntime): Formula { function makeChainableFormula(record: RecordedRuntime): Formula {
const formula: Formula = { const formula: Formula = {
__uatuFormula: true, __sanderlingFormula: true,
implies(other: Formula): Formula { implies(other: Formula): Formula {
record.impliesCalls++; record.impliesCalls++;
void other; void other;
@@ -125,11 +125,11 @@ function installFakeRuntime(): RecordedRuntime {
}, },
actions: (generator: () => Action[]): ActionGenerator => { actions: (generator: () => Action[]): ActionGenerator => {
calls.actionGenerators.push(generator); calls.actionGenerators.push(generator);
return { __uatuActionGenerator: true, generate: generator }; return { __sanderlingActionGenerator: true, generate: generator };
}, },
weighted: (...entries: WeightedEntry[]): ActionGenerator => { weighted: (...entries: WeightedEntry[]): ActionGenerator => {
calls.weightedCalls.push(entries); calls.weightedCalls.push(entries);
return { __uatuActionGenerator: true, generate: () => [] }; return { __sanderlingActionGenerator: true, generate: () => [] };
}, },
from: <T>(items: readonly T[]): Sampler<T> => { from: <T>(items: readonly T[]): Sampler<T> => {
calls.fromCalls.push(items as unknown[]); calls.fromCalls.push(items as unknown[]);
@@ -145,13 +145,13 @@ function installFakeRuntime(): RecordedRuntime {
}), }),
pressKey: ({ key }) => ({ kind: "PressKey", key }), pressKey: ({ key }) => ({ kind: "PressKey", key }),
wait: ({ durationMillis }) => ({ kind: "Wait", durationMillis }), wait: ({ durationMillis }) => ({ kind: "Wait", durationMillis }),
taps: { __uatuActionGenerator: true, generate: () => [] }, taps: { __sanderlingActionGenerator: true, generate: () => [] },
swipes: { __uatuActionGenerator: true, generate: () => [] }, swipes: { __sanderlingActionGenerator: true, generate: () => [] },
waitOnce: { __uatuActionGenerator: true, generate: () => [] }, waitOnce: { __sanderlingActionGenerator: true, generate: () => [] },
pressKeys: { __uatuActionGenerator: true, generate: () => [] }, pressKeys: { __sanderlingActionGenerator: true, generate: () => [] },
} satisfies UatuRuntime; } satisfies SanderlingRuntime;
const recorded = Object.assign(runtime, calls) as RecordedRuntime; const recorded = Object.assign(runtime, calls) as RecordedRuntime;
globalThis.__uatu__ = recorded; globalThis.__sanderling__ = recorded;
return recorded; return recorded;
} }
@@ -168,7 +168,7 @@ test("always wraps a predicate into a formula via the runtime", () => {
const predicate = () => true; const predicate = () => true;
const formula = always(predicate); const formula = always(predicate);
assert.equal(runtime.alwaysArgs[0], predicate); assert.equal(runtime.alwaysArgs[0], predicate);
assert.equal(formula.__uatuFormula, true); assert.equal(formula.__sanderlingFormula, true);
}); });
test("always accepts a formula handle", () => { test("always accepts a formula handle", () => {
@@ -176,7 +176,7 @@ test("always accepts a formula handle", () => {
const inner = now(() => true); const inner = now(() => true);
const wrapped = always(inner); const wrapped = always(inner);
assert.equal(runtime.alwaysArgs.at(-1), inner); assert.equal(runtime.alwaysArgs.at(-1), inner);
assert.equal(wrapped.__uatuFormula, true); assert.equal(wrapped.__sanderlingFormula, true);
}); });
test("now/next/eventually forward predicates", () => { test("now/next/eventually forward predicates", () => {
@@ -257,7 +257,7 @@ test("actions wraps a generator into the runtime's ActionGenerator", () => {
const generator = () => [Tap({ on: "id:x" })]; const generator = () => [Tap({ on: "id:x" })];
const wrapped = actions(generator); const wrapped = actions(generator);
assert.equal(runtime.actionGenerators[0], generator); assert.equal(runtime.actionGenerators[0], generator);
assert.equal(wrapped.__uatuActionGenerator, true); assert.equal(wrapped.__sanderlingActionGenerator, true);
}); });
test("weighted forwards weighted entries to the runtime", () => { test("weighted forwards weighted entries to the runtime", () => {
@@ -279,8 +279,8 @@ test("from forwards items to the runtime", () => {
test("default generators proxy through to the runtime", () => { test("default generators proxy through to the runtime", () => {
installFakeRuntime(); installFakeRuntime();
assert.equal(taps.__uatuActionGenerator, true); assert.equal(taps.__sanderlingActionGenerator, true);
assert.equal(swipes.__uatuActionGenerator, true); assert.equal(swipes.__sanderlingActionGenerator, true);
assert.equal(waitOnce.__uatuActionGenerator, true); assert.equal(waitOnce.__sanderlingActionGenerator, true);
assert.equal(pressKey.__uatuActionGenerator, true); assert.equal(pressKey.__sanderlingActionGenerator, true);
}); });
+16 -16
View File
@@ -9,11 +9,11 @@ import type {
Formula, Formula,
Sampler, Sampler,
State, State,
UatuRuntime, SanderlingRuntime,
WeightedEntry, WeightedEntry,
} from "../src/types.ts"; } from "../src/types.ts";
interface RecordedRuntime extends UatuRuntime { interface RecordedRuntime extends SanderlingRuntime {
currentState: State; currentState: State;
extractors: Array<(state: State) => unknown>; extractors: Array<(state: State) => unknown>;
alwaysArgs: Array<(() => boolean) | Formula>; alwaysArgs: Array<(() => boolean) | Formula>;
@@ -43,17 +43,17 @@ function installRuntime(initialState: State): RecordedRuntime {
if (typeof predicateOrFormula === "function") { if (typeof predicateOrFormula === "function") {
lastPredicate = predicateOrFormula; lastPredicate = predicateOrFormula;
} }
return { __uatuFormula: true } as Formula; return { __sanderlingFormula: true } as Formula;
}, },
now: () => ({ __uatuFormula: true } as Formula), now: () => ({ __sanderlingFormula: true } as Formula),
next: () => ({ __uatuFormula: true } as Formula), next: () => ({ __sanderlingFormula: true } as Formula),
eventually: () => ({ __uatuFormula: true } as EventuallyFormula), eventually: () => ({ __sanderlingFormula: true } as EventuallyFormula),
actions: (generator: () => Action[]): ActionGenerator => ({ actions: (generator: () => Action[]): ActionGenerator => ({
__uatuActionGenerator: true, __sanderlingActionGenerator: true,
generate: generator, generate: generator,
}), }),
weighted: (..._entries: WeightedEntry[]): ActionGenerator => ({ weighted: (..._entries: WeightedEntry[]): ActionGenerator => ({
__uatuActionGenerator: true, __sanderlingActionGenerator: true,
generate: () => [], generate: () => [],
}), }),
from: <T>(_items: readonly T[]): Sampler<T> => ({ generate: () => _items[0] as T }), from: <T>(_items: readonly T[]): Sampler<T> => ({ generate: () => _items[0] as T }),
@@ -62,11 +62,11 @@ function installRuntime(initialState: State): RecordedRuntime {
swipe: (p) => ({ kind: "Swipe", from: p.from, to: p.to, durationMillis: p.durationMillis }), swipe: (p) => ({ kind: "Swipe", from: p.from, to: p.to, durationMillis: p.durationMillis }),
pressKey: ({ key }) => ({ kind: "PressKey", key }), pressKey: ({ key }) => ({ kind: "PressKey", key }),
wait: ({ durationMillis }) => ({ kind: "Wait", durationMillis }), wait: ({ durationMillis }) => ({ kind: "Wait", durationMillis }),
taps: { __uatuActionGenerator: true, generate: () => [] } as ActionGenerator, taps: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
swipes: { __uatuActionGenerator: true, generate: () => [] } as ActionGenerator, swipes: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
waitOnce: { __uatuActionGenerator: true, generate: () => [] } as ActionGenerator, waitOnce: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
pressKeys: { __uatuActionGenerator: true, generate: () => [] } as ActionGenerator, pressKeys: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
} satisfies UatuRuntime; } satisfies SanderlingRuntime;
const state = { currentState: initialState }; const state = { currentState: initialState };
const recorded = Object.assign(runtime, { const recorded = Object.assign(runtime, {
@@ -77,7 +77,7 @@ function installRuntime(initialState: State): RecordedRuntime {
return lastPredicate; return lastPredicate;
}, },
}) as unknown as RecordedRuntime; }) as unknown as RecordedRuntime;
globalThis.__uatu__ = recorded; globalThis.__sanderling__ = recorded;
// Re-bind state ref so subsequent extract() calls read the up-to-date state. // Re-bind state ref so subsequent extract() calls read the up-to-date state.
Object.defineProperty(recorded, "currentState", { Object.defineProperty(recorded, "currentState", {
get() { get() {
@@ -105,6 +105,6 @@ test("defaults bundle exports formulas tagged as LTL properties", async () => {
logs: [{ unixMillis: 1, level: "W", tag: "X", message: "warn" }], logs: [{ unixMillis: 1, level: "W", tag: "X", message: "warn" }],
}); });
const defaults = await import("../src/defaults/properties.ts"); const defaults = await import("../src/defaults/properties.ts");
assert.equal(defaults.noUncaughtExceptions.__uatuFormula, true); assert.equal(defaults.noUncaughtExceptions.__sanderlingFormula, true);
assert.equal(defaults.noLogcatErrors.__uatuFormula, true); assert.equal(defaults.noLogcatErrors.__sanderlingFormula, true);
}); });
+69 -69
View File
@@ -849,7 +849,7 @@ var File_driverpb_driver_proto protoreflect.FileDescriptor
const file_driverpb_driver_proto_rawDesc = "" + const file_driverpb_driver_proto_rawDesc = "" +
"\n" + "\n" +
"\x15driverpb/driver.proto\x12\x0euatu.driver.v1\"\a\n" + "\x15driverpb/driver.proto\x12\x14sanderling.driver.v1\"\a\n" +
"\x05Empty\"z\n" + "\x05Empty\"z\n" +
"\rLaunchRequest\x12\x1b\n" + "\rLaunchRequest\x12\x1b\n" +
"\tbundle_id\x18\x01 \x01(\tR\bbundleId\x12\x1f\n" + "\tbundle_id\x18\x01 \x01(\tR\bbundleId\x12\x1f\n" +
@@ -874,19 +874,19 @@ const file_driverpb_driver_proto_rawDesc = "" +
"\fHealthStatus\x12\x14\n" + "\fHealthStatus\x12\x14\n" +
"\x05ready\x18\x01 \x01(\bR\x05ready\x12\x18\n" + "\x05ready\x18\x01 \x01(\bR\x05ready\x12\x18\n" +
"\aversion\x18\x02 \x01(\tR\aversion\x12\x1a\n" + "\aversion\x18\x02 \x01(\tR\aversion\x12\x1a\n" +
"\bplatform\x18\x03 \x01(\tR\bplatform\"\x89\x01\n" + "\bplatform\x18\x03 \x01(\tR\bplatform\"\x95\x01\n" +
"\fSwipeRequest\x12)\n" + "\fSwipeRequest\x12/\n" +
"\x04from\x18\x01 \x01(\v2\x15.uatu.driver.v1.PointR\x04from\x12%\n" + "\x04from\x18\x01 \x01(\v2\x1b.sanderling.driver.v1.PointR\x04from\x12+\n" +
"\x02to\x18\x02 \x01(\v2\x15.uatu.driver.v1.PointR\x02to\x12'\n" + "\x02to\x18\x02 \x01(\v2\x1b.sanderling.driver.v1.PointR\x02to\x12'\n" +
"\x0fduration_millis\x18\x03 \x01(\x03R\x0edurationMillis\"#\n" + "\x0fduration_millis\x18\x03 \x01(\x03R\x0edurationMillis\"#\n" +
"\x0fPressKeyRequest\x12\x10\n" + "\x0fPressKeyRequest\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\"e\n" + "\x03key\x18\x01 \x01(\tR\x03key\"e\n" +
"\x11RecentLogsRequest\x12*\n" + "\x11RecentLogsRequest\x12*\n" +
"\x11since_unix_millis\x18\x01 \x01(\x03R\x0fsinceUnixMillis\x12$\n" + "\x11since_unix_millis\x18\x01 \x01(\x03R\x0fsinceUnixMillis\x12$\n" +
"\x0elevel_at_least\x18\x02 \x01(\tR\flevelAtLeast\"@\n" + "\x0elevel_at_least\x18\x02 \x01(\tR\flevelAtLeast\"F\n" +
"\n" + "\n" +
"LogEntries\x122\n" + "LogEntries\x128\n" +
"\aentries\x18\x01 \x03(\v2\x18.uatu.driver.v1.LogEntryR\aentries\"m\n" + "\aentries\x18\x01 \x03(\v2\x1e.sanderling.driver.v1.LogEntryR\aentries\"m\n" +
"\bLogEntry\x12\x1f\n" + "\bLogEntry\x12\x1f\n" +
"\vunix_millis\x18\x01 \x01(\x03R\n" + "\vunix_millis\x18\x01 \x01(\x03R\n" +
"unixMillis\x12\x14\n" + "unixMillis\x12\x14\n" +
@@ -900,24 +900,24 @@ const file_driverpb_driver_proto_rawDesc = "" +
"cpuPercent\x12\x1d\n" + "cpuPercent\x12\x1d\n" +
"\n" + "\n" +
"heap_bytes\x18\x02 \x01(\x03R\theapBytes\x12,\n" + "heap_bytes\x18\x02 \x01(\x03R\theapBytes\x12,\n" +
"\x12total_memory_bytes\x18\x03 \x01(\x03R\x10totalMemoryBytes2\xcb\x06\n" + "\x12total_memory_bytes\x18\x03 \x01(\x03R\x10totalMemoryBytes2\xe7\a\n" +
"\x06Driver\x12>\n" + "\x06Driver\x12J\n" +
"\x06Launch\x12\x1d.uatu.driver.v1.LaunchRequest\x1a\x15.uatu.driver.v1.Empty\x129\n" + "\x06Launch\x12#.sanderling.driver.v1.LaunchRequest\x1a\x1b.sanderling.driver.v1.Empty\x12E\n" +
"\tTerminate\x12\x15.uatu.driver.v1.Empty\x1a\x15.uatu.driver.v1.Empty\x123\n" + "\tTerminate\x12\x1b.sanderling.driver.v1.Empty\x1a\x1b.sanderling.driver.v1.Empty\x12?\n" +
"\x03Tap\x12\x15.uatu.driver.v1.Point\x1a\x15.uatu.driver.v1.Empty\x12>\n" + "\x03Tap\x12\x1b.sanderling.driver.v1.Point\x1a\x1b.sanderling.driver.v1.Empty\x12J\n" +
"\vTapSelector\x12\x18.uatu.driver.v1.Selector\x1a\x15.uatu.driver.v1.Empty\x128\n" + "\vTapSelector\x12\x1e.sanderling.driver.v1.Selector\x1a\x1b.sanderling.driver.v1.Empty\x12D\n" +
"\tInputText\x12\x14.uatu.driver.v1.Text\x1a\x15.uatu.driver.v1.Empty\x12<\n" + "\tInputText\x12\x1a.sanderling.driver.v1.Text\x1a\x1b.sanderling.driver.v1.Empty\x12H\n" +
"\x05Swipe\x12\x1c.uatu.driver.v1.SwipeRequest\x1a\x15.uatu.driver.v1.Empty\x12B\n" + "\x05Swipe\x12\".sanderling.driver.v1.SwipeRequest\x1a\x1b.sanderling.driver.v1.Empty\x12N\n" +
"\bPressKey\x12\x1f.uatu.driver.v1.PressKeyRequest\x1a\x15.uatu.driver.v1.Empty\x12:\n" + "\bPressKey\x12%.sanderling.driver.v1.PressKeyRequest\x1a\x1b.sanderling.driver.v1.Empty\x12F\n" +
"\n" + "\n" +
"Screenshot\x12\x15.uatu.driver.v1.Empty\x1a\x15.uatu.driver.v1.Image\x12A\n" + "Screenshot\x12\x1b.sanderling.driver.v1.Empty\x1a\x1b.sanderling.driver.v1.Image\x12M\n" +
"\tHierarchy\x12\x15.uatu.driver.v1.Empty\x1a\x1d.uatu.driver.v1.HierarchyJSON\x12K\n" + "\tHierarchy\x12\x1b.sanderling.driver.v1.Empty\x1a#.sanderling.driver.v1.HierarchyJSON\x12W\n" +
"\n" + "\n" +
"RecentLogs\x12!.uatu.driver.v1.RecentLogsRequest\x1a\x1a.uatu.driver.v1.LogEntries\x12>\n" + "RecentLogs\x12'.sanderling.driver.v1.RecentLogsRequest\x1a .sanderling.driver.v1.LogEntries\x12J\n" +
"\vWaitForIdle\x12\x18.uatu.driver.v1.Duration\x1a\x15.uatu.driver.v1.Empty\x12=\n" + "\vWaitForIdle\x12\x1e.sanderling.driver.v1.Duration\x1a\x1b.sanderling.driver.v1.Empty\x12I\n" +
"\x06Health\x12\x15.uatu.driver.v1.Empty\x1a\x1c.uatu.driver.v1.HealthStatus\x12J\n" + "\x06Health\x12\x1b.sanderling.driver.v1.Empty\x1a\".sanderling.driver.v1.HealthStatus\x12V\n" +
"\aMetrics\x12\x1e.uatu.driver.v1.MetricsRequest\x1a\x1f.uatu.driver.v1.MetricsResponseBM\n" + "\aMetrics\x12$.sanderling.driver.v1.MetricsRequest\x1a%.sanderling.driver.v1.MetricsResponseBY\n" +
"\x12dev.uatu.driver.v1P\x01Z5github.com/priyanshujain/uatu/proto/driverpb;driverpbb\x06proto3" "\x18dev.sanderling.driver.v1P\x01Z;github.com/priyanshujain/sanderling/proto/driverpb;driverpbb\x06proto3"
var ( var (
file_driverpb_driver_proto_rawDescOnce sync.Once file_driverpb_driver_proto_rawDescOnce sync.Once
@@ -933,53 +933,53 @@ func file_driverpb_driver_proto_rawDescGZIP() []byte {
var file_driverpb_driver_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_driverpb_driver_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
var file_driverpb_driver_proto_goTypes = []any{ var file_driverpb_driver_proto_goTypes = []any{
(*Empty)(nil), // 0: uatu.driver.v1.Empty (*Empty)(nil), // 0: sanderling.driver.v1.Empty
(*LaunchRequest)(nil), // 1: uatu.driver.v1.LaunchRequest (*LaunchRequest)(nil), // 1: sanderling.driver.v1.LaunchRequest
(*Point)(nil), // 2: uatu.driver.v1.Point (*Point)(nil), // 2: sanderling.driver.v1.Point
(*Text)(nil), // 3: uatu.driver.v1.Text (*Text)(nil), // 3: sanderling.driver.v1.Text
(*Selector)(nil), // 4: uatu.driver.v1.Selector (*Selector)(nil), // 4: sanderling.driver.v1.Selector
(*Image)(nil), // 5: uatu.driver.v1.Image (*Image)(nil), // 5: sanderling.driver.v1.Image
(*HierarchyJSON)(nil), // 6: uatu.driver.v1.HierarchyJSON (*HierarchyJSON)(nil), // 6: sanderling.driver.v1.HierarchyJSON
(*Duration)(nil), // 7: uatu.driver.v1.Duration (*Duration)(nil), // 7: sanderling.driver.v1.Duration
(*HealthStatus)(nil), // 8: uatu.driver.v1.HealthStatus (*HealthStatus)(nil), // 8: sanderling.driver.v1.HealthStatus
(*SwipeRequest)(nil), // 9: uatu.driver.v1.SwipeRequest (*SwipeRequest)(nil), // 9: sanderling.driver.v1.SwipeRequest
(*PressKeyRequest)(nil), // 10: uatu.driver.v1.PressKeyRequest (*PressKeyRequest)(nil), // 10: sanderling.driver.v1.PressKeyRequest
(*RecentLogsRequest)(nil), // 11: uatu.driver.v1.RecentLogsRequest (*RecentLogsRequest)(nil), // 11: sanderling.driver.v1.RecentLogsRequest
(*LogEntries)(nil), // 12: uatu.driver.v1.LogEntries (*LogEntries)(nil), // 12: sanderling.driver.v1.LogEntries
(*LogEntry)(nil), // 13: uatu.driver.v1.LogEntry (*LogEntry)(nil), // 13: sanderling.driver.v1.LogEntry
(*MetricsRequest)(nil), // 14: uatu.driver.v1.MetricsRequest (*MetricsRequest)(nil), // 14: sanderling.driver.v1.MetricsRequest
(*MetricsResponse)(nil), // 15: uatu.driver.v1.MetricsResponse (*MetricsResponse)(nil), // 15: sanderling.driver.v1.MetricsResponse
} }
var file_driverpb_driver_proto_depIdxs = []int32{ var file_driverpb_driver_proto_depIdxs = []int32{
2, // 0: uatu.driver.v1.SwipeRequest.from:type_name -> uatu.driver.v1.Point 2, // 0: sanderling.driver.v1.SwipeRequest.from:type_name -> sanderling.driver.v1.Point
2, // 1: uatu.driver.v1.SwipeRequest.to:type_name -> uatu.driver.v1.Point 2, // 1: sanderling.driver.v1.SwipeRequest.to:type_name -> sanderling.driver.v1.Point
13, // 2: uatu.driver.v1.LogEntries.entries:type_name -> uatu.driver.v1.LogEntry 13, // 2: sanderling.driver.v1.LogEntries.entries:type_name -> sanderling.driver.v1.LogEntry
1, // 3: uatu.driver.v1.Driver.Launch:input_type -> uatu.driver.v1.LaunchRequest 1, // 3: sanderling.driver.v1.Driver.Launch:input_type -> sanderling.driver.v1.LaunchRequest
0, // 4: uatu.driver.v1.Driver.Terminate:input_type -> uatu.driver.v1.Empty 0, // 4: sanderling.driver.v1.Driver.Terminate:input_type -> sanderling.driver.v1.Empty
2, // 5: uatu.driver.v1.Driver.Tap:input_type -> uatu.driver.v1.Point 2, // 5: sanderling.driver.v1.Driver.Tap:input_type -> sanderling.driver.v1.Point
4, // 6: uatu.driver.v1.Driver.TapSelector:input_type -> uatu.driver.v1.Selector 4, // 6: sanderling.driver.v1.Driver.TapSelector:input_type -> sanderling.driver.v1.Selector
3, // 7: uatu.driver.v1.Driver.InputText:input_type -> uatu.driver.v1.Text 3, // 7: sanderling.driver.v1.Driver.InputText:input_type -> sanderling.driver.v1.Text
9, // 8: uatu.driver.v1.Driver.Swipe:input_type -> uatu.driver.v1.SwipeRequest 9, // 8: sanderling.driver.v1.Driver.Swipe:input_type -> sanderling.driver.v1.SwipeRequest
10, // 9: uatu.driver.v1.Driver.PressKey:input_type -> uatu.driver.v1.PressKeyRequest 10, // 9: sanderling.driver.v1.Driver.PressKey:input_type -> sanderling.driver.v1.PressKeyRequest
0, // 10: uatu.driver.v1.Driver.Screenshot:input_type -> uatu.driver.v1.Empty 0, // 10: sanderling.driver.v1.Driver.Screenshot:input_type -> sanderling.driver.v1.Empty
0, // 11: uatu.driver.v1.Driver.Hierarchy:input_type -> uatu.driver.v1.Empty 0, // 11: sanderling.driver.v1.Driver.Hierarchy:input_type -> sanderling.driver.v1.Empty
11, // 12: uatu.driver.v1.Driver.RecentLogs:input_type -> uatu.driver.v1.RecentLogsRequest 11, // 12: sanderling.driver.v1.Driver.RecentLogs:input_type -> sanderling.driver.v1.RecentLogsRequest
7, // 13: uatu.driver.v1.Driver.WaitForIdle:input_type -> uatu.driver.v1.Duration 7, // 13: sanderling.driver.v1.Driver.WaitForIdle:input_type -> sanderling.driver.v1.Duration
0, // 14: uatu.driver.v1.Driver.Health:input_type -> uatu.driver.v1.Empty 0, // 14: sanderling.driver.v1.Driver.Health:input_type -> sanderling.driver.v1.Empty
14, // 15: uatu.driver.v1.Driver.Metrics:input_type -> uatu.driver.v1.MetricsRequest 14, // 15: sanderling.driver.v1.Driver.Metrics:input_type -> sanderling.driver.v1.MetricsRequest
0, // 16: uatu.driver.v1.Driver.Launch:output_type -> uatu.driver.v1.Empty 0, // 16: sanderling.driver.v1.Driver.Launch:output_type -> sanderling.driver.v1.Empty
0, // 17: uatu.driver.v1.Driver.Terminate:output_type -> uatu.driver.v1.Empty 0, // 17: sanderling.driver.v1.Driver.Terminate:output_type -> sanderling.driver.v1.Empty
0, // 18: uatu.driver.v1.Driver.Tap:output_type -> uatu.driver.v1.Empty 0, // 18: sanderling.driver.v1.Driver.Tap:output_type -> sanderling.driver.v1.Empty
0, // 19: uatu.driver.v1.Driver.TapSelector:output_type -> uatu.driver.v1.Empty 0, // 19: sanderling.driver.v1.Driver.TapSelector:output_type -> sanderling.driver.v1.Empty
0, // 20: uatu.driver.v1.Driver.InputText:output_type -> uatu.driver.v1.Empty 0, // 20: sanderling.driver.v1.Driver.InputText:output_type -> sanderling.driver.v1.Empty
0, // 21: uatu.driver.v1.Driver.Swipe:output_type -> uatu.driver.v1.Empty 0, // 21: sanderling.driver.v1.Driver.Swipe:output_type -> sanderling.driver.v1.Empty
0, // 22: uatu.driver.v1.Driver.PressKey:output_type -> uatu.driver.v1.Empty 0, // 22: sanderling.driver.v1.Driver.PressKey:output_type -> sanderling.driver.v1.Empty
5, // 23: uatu.driver.v1.Driver.Screenshot:output_type -> uatu.driver.v1.Image 5, // 23: sanderling.driver.v1.Driver.Screenshot:output_type -> sanderling.driver.v1.Image
6, // 24: uatu.driver.v1.Driver.Hierarchy:output_type -> uatu.driver.v1.HierarchyJSON 6, // 24: sanderling.driver.v1.Driver.Hierarchy:output_type -> sanderling.driver.v1.HierarchyJSON
12, // 25: uatu.driver.v1.Driver.RecentLogs:output_type -> uatu.driver.v1.LogEntries 12, // 25: sanderling.driver.v1.Driver.RecentLogs:output_type -> sanderling.driver.v1.LogEntries
0, // 26: uatu.driver.v1.Driver.WaitForIdle:output_type -> uatu.driver.v1.Empty 0, // 26: sanderling.driver.v1.Driver.WaitForIdle:output_type -> sanderling.driver.v1.Empty
8, // 27: uatu.driver.v1.Driver.Health:output_type -> uatu.driver.v1.HealthStatus 8, // 27: sanderling.driver.v1.Driver.Health:output_type -> sanderling.driver.v1.HealthStatus
15, // 28: uatu.driver.v1.Driver.Metrics:output_type -> uatu.driver.v1.MetricsResponse 15, // 28: sanderling.driver.v1.Driver.Metrics:output_type -> sanderling.driver.v1.MetricsResponse
16, // [16:29] is the sub-list for method output_type 16, // [16:29] is the sub-list for method output_type
3, // [3:16] is the sub-list for method input_type 3, // [3:16] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name 3, // [3:3] is the sub-list for extension type_name
+3 -3
View File
@@ -1,9 +1,9 @@
syntax = "proto3"; syntax = "proto3";
package uatu.driver.v1; package sanderling.driver.v1;
option go_package = "github.com/priyanshujain/uatu/proto/driverpb;driverpb"; option go_package = "github.com/priyanshujain/sanderling/proto/driverpb;driverpb";
option java_package = "dev.uatu.driver.v1"; option java_package = "dev.sanderling.driver.v1";
option java_multiple_files = true; option java_multiple_files = true;
service Driver { service Driver {
+14 -14
View File
@@ -19,19 +19,19 @@ import (
const _ = grpc.SupportPackageIsVersion9 const _ = grpc.SupportPackageIsVersion9
const ( const (
Driver_Launch_FullMethodName = "/uatu.driver.v1.Driver/Launch" Driver_Launch_FullMethodName = "/sanderling.driver.v1.Driver/Launch"
Driver_Terminate_FullMethodName = "/uatu.driver.v1.Driver/Terminate" Driver_Terminate_FullMethodName = "/sanderling.driver.v1.Driver/Terminate"
Driver_Tap_FullMethodName = "/uatu.driver.v1.Driver/Tap" Driver_Tap_FullMethodName = "/sanderling.driver.v1.Driver/Tap"
Driver_TapSelector_FullMethodName = "/uatu.driver.v1.Driver/TapSelector" Driver_TapSelector_FullMethodName = "/sanderling.driver.v1.Driver/TapSelector"
Driver_InputText_FullMethodName = "/uatu.driver.v1.Driver/InputText" Driver_InputText_FullMethodName = "/sanderling.driver.v1.Driver/InputText"
Driver_Swipe_FullMethodName = "/uatu.driver.v1.Driver/Swipe" Driver_Swipe_FullMethodName = "/sanderling.driver.v1.Driver/Swipe"
Driver_PressKey_FullMethodName = "/uatu.driver.v1.Driver/PressKey" Driver_PressKey_FullMethodName = "/sanderling.driver.v1.Driver/PressKey"
Driver_Screenshot_FullMethodName = "/uatu.driver.v1.Driver/Screenshot" Driver_Screenshot_FullMethodName = "/sanderling.driver.v1.Driver/Screenshot"
Driver_Hierarchy_FullMethodName = "/uatu.driver.v1.Driver/Hierarchy" Driver_Hierarchy_FullMethodName = "/sanderling.driver.v1.Driver/Hierarchy"
Driver_RecentLogs_FullMethodName = "/uatu.driver.v1.Driver/RecentLogs" Driver_RecentLogs_FullMethodName = "/sanderling.driver.v1.Driver/RecentLogs"
Driver_WaitForIdle_FullMethodName = "/uatu.driver.v1.Driver/WaitForIdle" Driver_WaitForIdle_FullMethodName = "/sanderling.driver.v1.Driver/WaitForIdle"
Driver_Health_FullMethodName = "/uatu.driver.v1.Driver/Health" Driver_Health_FullMethodName = "/sanderling.driver.v1.Driver/Health"
Driver_Metrics_FullMethodName = "/uatu.driver.v1.Driver/Metrics" Driver_Metrics_FullMethodName = "/sanderling.driver.v1.Driver/Metrics"
) )
// DriverClient is the client API for Driver service. // DriverClient is the client API for Driver service.
@@ -516,7 +516,7 @@ func _Driver_Metrics_Handler(srv interface{}, ctx context.Context, dec func(inte
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
var Driver_ServiceDesc = grpc.ServiceDesc{ var Driver_ServiceDesc = grpc.ServiceDesc{
ServiceName: "uatu.driver.v1.Driver", ServiceName: "sanderling.driver.v1.Driver",
HandlerType: (*DriverServer)(nil), HandlerType: (*DriverServer)(nil),
Methods: []grpc.MethodDesc{ Methods: []grpc.MethodDesc{
{ {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
func TestDriverServiceDescriptor(t *testing.T) { func TestDriverServiceDescriptor(t *testing.T) {
var sd grpc.ServiceDesc = Driver_ServiceDesc var sd grpc.ServiceDesc = Driver_ServiceDesc
if sd.ServiceName != "uatu.driver.v1.Driver" { if sd.ServiceName != "sanderling.driver.v1.Driver" {
t.Fatalf("unexpected service name: %q", sd.ServiceName) t.Fatalf("unexpected service name: %q", sd.ServiceName)
} }
+9 -9
View File
@@ -10,11 +10,11 @@ plugins {
id("org.jetbrains.dokka-javadoc") version "2.2.0" id("org.jetbrains.dokka-javadoc") version "2.2.0"
} }
version = findProperty("uatu.version") as String? ?: "0.0.0-dev" version = findProperty("sanderling.version") as String? ?: "0.0.0-dev"
group = "io.github.priyanshujain" group = "io.github.priyanshujain"
android { android {
namespace = "dev.uatu.sdk" namespace = "dev.sanderling.sdk"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
@@ -61,12 +61,12 @@ mavenPublishing {
) )
pom { pom {
name.set("uatu sdk-android") name.set("sanderling sdk-android")
description.set( description.set(
"Android runtime SDK for uatu, a property-based UI fuzzer for mobile apps. " + "Android runtime SDK for sanderling, a property-based UI fuzzer for mobile apps. " +
"Exposes a content-provider accessibility bridge consumed by the uatu CLI at test time.", "Exposes a content-provider accessibility bridge consumed by the sanderling CLI at test time.",
) )
url.set("https://github.com/priyanshujain/uatu") url.set("https://github.com/priyanshujain/sanderling")
licenses { licenses {
license { license {
@@ -85,9 +85,9 @@ mavenPublishing {
} }
scm { scm {
url.set("https://github.com/priyanshujain/uatu") url.set("https://github.com/priyanshujain/sanderling")
connection.set("scm:git:git://github.com/priyanshujain/uatu.git") connection.set("scm:git:git://github.com/priyanshujain/sanderling.git")
developerConnection.set("scm:git:ssh://[email protected]/priyanshujain/uatu.git") developerConnection.set("scm:git:ssh://[email protected]/priyanshujain/sanderling.git")
} }
} }
} }
+1 -1
View File
@@ -1,2 +1,2 @@
# ProGuard rules shipped to consumers of dev.uatu:sdk-android. # ProGuard rules shipped to consumers of dev.sanderling:sdk-android.
# None needed for v0.1. Socket and semaphore APIs are all reflection-free. # None needed for v0.1. Socket and semaphore APIs are all reflection-free.
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import android.net.LocalSocket import android.net.LocalSocket
import android.net.LocalSocketAddress import android.net.LocalSocketAddress
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.Semaphore import java.util.concurrent.Semaphore
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
@@ -1,24 +1,24 @@
package dev.uatu.sdk package dev.sanderling.sdk
import android.app.Application import android.app.Application
import android.util.Log import android.util.Log
data class Configuration( data class Configuration(
val socketName: String = "uatu-agent", val socketName: String = "sanderling-agent",
val pauseTimeoutMillis: Long = 5_000L, val pauseTimeoutMillis: Long = 5_000L,
) )
object Uatu { object Sanderling {
const val VERSION: String = "0.0.1" const val VERSION: String = "0.0.1"
private const val LOG_TAG = "Uatu" private const val LOG_TAG = "Sanderling"
@Volatile private var runtime: UatuRuntime? = null @Volatile private var runtime: SanderlingRuntime? = null
@JvmOverloads @JvmOverloads
@Synchronized @Synchronized
fun start(application: Application, configuration: Configuration = Configuration()) { fun start(application: Application, configuration: Configuration = Configuration()) {
if (runtime != null) return if (runtime != null) return
val newRuntime = UatuRuntime( val newRuntime = SanderlingRuntime(
transport = LocalAbstractTransport(configuration.socketName), transport = LocalAbstractTransport(configuration.socketName),
pauser = Pauser(ChoreographerPoster(), configuration.pauseTimeoutMillis), pauser = Pauser(ChoreographerPoster(), configuration.pauseTimeoutMillis),
version = VERSION, version = VERSION,
@@ -32,7 +32,7 @@ object Uatu {
fun extract(name: String, function: () -> Any?) { fun extract(name: String, function: () -> Any?) {
val activeRuntime = runtime val activeRuntime = runtime
?: throw IllegalStateException("Uatu.start must be called before registering extractors") ?: throw IllegalStateException("Sanderling.start must be called before registering extractors")
activeRuntime.register(name, function) activeRuntime.register(name, function)
} }
@@ -1,8 +1,8 @@
package dev.uatu.sdk package dev.sanderling.sdk
import android.util.Log import android.util.Log
internal class UatuRuntime( internal class SanderlingRuntime(
transport: AgentTransport, transport: AgentTransport,
private val pauser: Pauser, private val pauser: Pauser,
private val version: String, private val version: String,
@@ -45,7 +45,7 @@ internal class UatuRuntime(
private inner class AgentHandler : SocketClient.Handler { private inner class AgentHandler : SocketClient.Handler {
override fun onConnected(sender: SocketClient.MessageSender) { override fun onConnected(sender: SocketClient.MessageSender) {
this@UatuRuntime.sender = sender this@SanderlingRuntime.sender = sender
try { try {
sender.send(Message.hello(version, platform, appPackage)) sender.send(Message.hello(version, platform, appPackage))
} catch (cause: Exception) { } catch (cause: Exception) {
@@ -92,6 +92,6 @@ internal class UatuRuntime(
} }
companion object { companion object {
private const val LOG_TAG = "Uatu" private const val LOG_TAG = "Sanderling"
} }
} }
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@@ -30,7 +30,7 @@ class SocketClient(
private val transport: AgentTransport, private val transport: AgentTransport,
private val handler: Handler, private val handler: Handler,
private val backoff: Backoff = Backoff(), private val backoff: Backoff = Backoff(),
private val threadFactory: (Runnable) -> Thread = { runnable -> Thread(runnable, "uatu-agent-reader") }, private val threadFactory: (Runnable) -> Thread = { runnable -> Thread(runnable, "sanderling-agent-reader") },
private val sleeper: (Long) -> Unit = { millis -> if (millis > 0L) Thread.sleep(millis) }, private val sleeper: (Long) -> Unit = { millis -> if (millis > 0L) Thread.sleep(millis) },
) { ) {
interface Handler { interface Handler {
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@@ -12,22 +12,22 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
class UatuRuntimeTest { class SanderlingRuntimeTest {
private lateinit var transport: SocketClientTest.FakeTransport private lateinit var transport: SocketClientTest.FakeTransport
private lateinit var frameThread: PauserTest.FakeFrameThread private lateinit var frameThread: PauserTest.FakeFrameThread
private lateinit var runtime: UatuRuntime private lateinit var runtime: SanderlingRuntime
private fun newRuntime(): UatuRuntime { private fun newRuntime(): SanderlingRuntime {
transport = SocketClientTest.FakeTransport() transport = SocketClientTest.FakeTransport()
frameThread = PauserTest.FakeFrameThread() frameThread = PauserTest.FakeFrameThread()
val pauser = Pauser(frameThread, pauseTimeoutMillis = 2_000L) val pauser = Pauser(frameThread, pauseTimeoutMillis = 2_000L)
return UatuRuntime( return SanderlingRuntime(
transport = transport, transport = transport,
pauser = pauser, pauser = pauser,
version = "0.0.1", version = "0.0.1",
platform = "android", platform = "android",
appPackage = "com.example.uatu_test", appPackage = "com.example.sanderling_test",
).also { runtime = it } ).also { runtime = it }
} }
@@ -43,7 +43,7 @@ class UatuRuntimeTest {
assertEquals(MessageType.HELLO, hello.type) assertEquals(MessageType.HELLO, hello.type)
assertEquals("0.0.1", hello.version) assertEquals("0.0.1", hello.version)
assertEquals("android", hello.platform) assertEquals("android", hello.platform)
assertEquals("com.example.uatu_test", hello.appPackage) assertEquals("com.example.sanderling_test", hello.appPackage)
} }
@Test fun pauseTriggersExtractorsAndReturnsState() { @Test fun pauseTriggersExtractorsAndReturnsState() {
@@ -1,4 +1,4 @@
package dev.uatu.sdk package dev.sanderling.sdk
import java.io.IOException import java.io.IOException
import java.io.PipedInputStream import java.io.PipedInputStream
+1 -1
View File
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
} }
} }
rootProject.name = "uatu" rootProject.name = "sanderling"
include(":sidecar") include(":sidecar")
include(":sdk-android") include(":sdk-android")
+2 -2
View File
@@ -7,7 +7,7 @@ plugins {
id("com.google.protobuf") version "0.9.5" id("com.google.protobuf") version "0.9.5"
} }
version = findProperty("uatu.version") as String? ?: "0.0.0-dev" version = findProperty("sanderling.version") as String? ?: "0.0.0-dev"
java { java {
toolchain { toolchain {
@@ -70,7 +70,7 @@ sourceSets {
} }
application { application {
mainClass.set("dev.uatu.sidecar.MainKt") mainClass.set("dev.sanderling.sidecar.MainKt")
} }
// The Go binary embeds this fat JAR by fixed path (internal/sidecar/assets/ // The Go binary embeds this fat JAR by fixed path (internal/sidecar/assets/
@@ -1,4 +1,4 @@
package dev.uatu.sidecar package dev.sanderling.sidecar
interface DriverBackend { interface DriverBackend {
fun launch(bundleId: String, launcherActivity: String, clearState: Boolean) fun launch(bundleId: String, launcherActivity: String, clearState: Boolean)
@@ -1,23 +1,23 @@
package dev.uatu.sidecar package dev.sanderling.sidecar
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import dev.uatu.driver.v1.DriverGrpc import dev.sanderling.driver.v1.DriverGrpc
import dev.uatu.driver.v1.Duration import dev.sanderling.driver.v1.Duration
import dev.uatu.driver.v1.Empty import dev.sanderling.driver.v1.Empty
import dev.uatu.driver.v1.HealthStatus import dev.sanderling.driver.v1.HealthStatus
import dev.uatu.driver.v1.HierarchyJSON import dev.sanderling.driver.v1.HierarchyJSON
import dev.uatu.driver.v1.Image import dev.sanderling.driver.v1.Image
import dev.uatu.driver.v1.LaunchRequest import dev.sanderling.driver.v1.LaunchRequest
import dev.uatu.driver.v1.LogEntries import dev.sanderling.driver.v1.LogEntries
import dev.uatu.driver.v1.LogEntry import dev.sanderling.driver.v1.LogEntry
import dev.uatu.driver.v1.MetricsRequest import dev.sanderling.driver.v1.MetricsRequest
import dev.uatu.driver.v1.MetricsResponse import dev.sanderling.driver.v1.MetricsResponse
import dev.uatu.driver.v1.Point import dev.sanderling.driver.v1.Point
import dev.uatu.driver.v1.PressKeyRequest import dev.sanderling.driver.v1.PressKeyRequest
import dev.uatu.driver.v1.RecentLogsRequest import dev.sanderling.driver.v1.RecentLogsRequest
import dev.uatu.driver.v1.Selector import dev.sanderling.driver.v1.Selector
import dev.uatu.driver.v1.SwipeRequest import dev.sanderling.driver.v1.SwipeRequest
import dev.uatu.driver.v1.Text import dev.sanderling.driver.v1.Text
import io.grpc.stub.StreamObserver import io.grpc.stub.StreamObserver
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more