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
+514 -514

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
# 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 }}-
- 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:
name: Publish @uatu/spec to npm
name: Publish @sanderling/spec to npm
runs-on: ubuntu-latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -91,7 +91,7 @@ jobs:
- name: Publish
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.
run: |
if [[ "${{ steps.ver.outputs.version }}" == *-* ]]; then
@@ -101,7 +101,7 @@ jobs:
fi
release-cli:
name: Publish uatu CLI to GitHub Releases
name: Publish sanderling CLI to GitHub Releases
runs-on: ubuntu-latest
permissions:
contents: write
+2 -2
View File
@@ -17,7 +17,7 @@ local.properties
*.test
*.out
# Uatu runs
# Sanderling runs
/runs/
# Node
@@ -27,7 +27,7 @@ node_modules/
.env
.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.
internal/sidecar/assets/sidecar-all.jar
+8 -8
View File
@@ -1,6 +1,6 @@
version: 2
project_name: uatu
project_name: sanderling
before:
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'
builds:
- id: uatu
main: ./cmd/uatu
binary: uatu
- id: sanderling
main: ./cmd/sanderling
binary: sanderling
env:
- CGO_ENABLED=0
flags:
@@ -28,11 +28,11 @@ builds:
- arm64
archives:
- id: uatu
- id: sanderling
ids:
- uatu
- sanderling
formats: [tar.gz]
name_template: "uatu_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
name_template: "sanderling_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- README.md
- LICENSE*
@@ -59,6 +59,6 @@ changelog:
release:
github:
owner: priyanshujain
name: uatu
name: sanderling
draft: false
prerelease: auto
+10 -10
View File
@@ -9,7 +9,7 @@ GO_PACKAGES := ./...
SIDECAR_JAR := sidecar/build/libs/sidecar-all.jar
SIDECAR_EMBED := internal/sidecar/assets/sidecar-all.jar
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_OUT := $(patsubst docs/%.md,build/site/%.html,$(DOCS_SRC))
@@ -18,7 +18,7 @@ DOCS_TEMPLATE := docs/_template/page.html
INSPECT_DIST := internal/inspect/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:
$(GO) mod download
@@ -37,17 +37,17 @@ sdk-android:
sdk-android-publish:
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
$(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.
install: $(SIDECAR_EMBED) web-build
$(GO) install -tags withsidecar ./cmd/uatu
@dest="$$($(GO) env GOBIN)"; [ -n "$$dest" ] || dest="$$($(GO) env GOPATH)/bin"; echo "installed uatu to $$dest"
$(GO) install -tags withsidecar ./cmd/sanderling
@dest="$$($(GO) env GOBIN)"; [ -n "$$dest" ] || dest="$$($(GO) env GOPATH)/bin"; echo "installed sanderling to $$dest"
web-build:
cd web && bun install --frozen-lockfile && bun run build
@@ -59,7 +59,7 @@ web-dev:
cd web && bun run dev
inspect-dev: $(SIDECAR_EMBED)
$(GO) run -tags withsidecar ./cmd/uatu inspect --dev
$(GO) run -tags withsidecar ./cmd/sanderling inspect --dev
web-typecheck:
cd web && bun install --frozen-lockfile && bun run typecheck
@@ -109,7 +109,7 @@ release-cli: $(SIDECAR_JAR)
release-android-local:
@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:
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.
> 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
- [Getting started](https://priyanshujain.github.io/uatu/manual/getting-started.html)
- [Writing specs](https://priyanshujain.github.io/uatu/manual/writing-specs.html)
- [`uatu inspect` UI](https://priyanshujain.github.io/uatu/manual/inspect.html)
- [Example](https://github.com/priyanshujain/uatu/tree/master/examples/folio)
- [Architecture](https://priyanshujain.github.io/uatu/development/architecture.html)
- [Getting started](https://priyanshujain.github.io/sanderling/manual/getting-started.html)
- [Writing specs](https://priyanshujain.github.io/sanderling/manual/writing-specs.html)
- [`sanderling inspect` UI](https://priyanshujain.github.io/sanderling/manual/inspect.html)
- [Example](https://github.com/priyanshujain/sanderling/tree/master/examples/folio)
- [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"
"path/filepath"
"github.com/priyanshujain/uatu/internal/bundler"
"github.com/priyanshujain/sanderling/internal/bundler"
)
func main() {
@@ -31,8 +31,8 @@ func main() {
result, err := bundler.Bundle(bundler.Options{
EntryFile: entryFile,
Aliases: map[string]string{
"@uatu/spec": specApiPath,
"@uatu/spec/defaults/properties": defaultPropertiesPath,
"@sanderling/spec": specApiPath,
"@sanderling/spec/defaults/properties": defaultPropertiesPath,
},
})
if err != nil {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"os"
"github.com/priyanshujain/uatu/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/hierarchy"
)
func main() {
File renamed without changes.
@@ -31,18 +31,18 @@ func TestParseAdbDevices_Empty(t *testing.T) {
func TestParseAVDList_DropsInfoLines(t *testing.T) {
output := `INFO | Storing crashdata in: /tmp/x
Medium_Phone_API_36.0
uatu_test
sanderling_test
`
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) {
t.Fatalf("got %v, want %v", got, want)
}
}
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 {
t.Fatal(err)
}
@@ -59,12 +59,12 @@ func TestPickAVD_ExplicitMissing(t *testing.T) {
}
func TestPickAVD_SingleAvailable(t *testing.T) {
got, err := pickAVD("", []string{"uatu_test"})
got, err := pickAVD("", []string{"sanderling_test"})
if err != nil {
t.Fatal(err)
}
if got != "uatu_test" {
t.Fatalf("got %q, want uatu_test", got)
if got != "sanderling_test" {
t.Fatalf("got %q, want sanderling_test", got)
}
}
@@ -11,7 +11,7 @@ import (
"strconv"
"time"
"github.com/priyanshujain/uatu/internal/sidecar"
"github.com/priyanshujain/sanderling/internal/sidecar"
)
type doctorCheck struct {
@@ -30,7 +30,7 @@ func defaultDoctorChecks() []doctorCheck {
func checkSidecarJAR(_ context.Context) error {
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 {
return fmt.Errorf("embedded JAR is empty")
File renamed without changes.
@@ -14,7 +14,7 @@ import (
"strconv"
"time"
"github.com/priyanshujain/uatu/internal/inspect"
"github.com/priyanshujain/sanderling/internal/inspect"
)
type inspectOptions struct {
@@ -67,7 +67,7 @@ func runInspect(options inspectOptions, stdout io.Writer) error {
}
address := listener.Addr().(*net.TCPAddr)
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())
defer cancel()
@@ -36,7 +36,7 @@ func TestParseInspectArgs_RejectsTooManyPositional(t *testing.T) {
func TestRun_HelpListsInspectCommand(t *testing.T) {
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)
}
if !strings.Contains(stdout.String(), "inspect") {
+6 -6
View File
@@ -25,18 +25,18 @@ type testOptions struct {
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:
uatu <command> [flags]
sanderling <command> [flags]
Commands:
test Run a spec against an app for a fixed duration.
inspect Serve a local web UI for browsing runs/.
doctor Check that the host environment is ready to run uatu.
version Print the uatu version.
doctor Check that the host environment is ready to run sanderling.
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) {
@@ -100,7 +100,7 @@ func run(args []string, stdout, stderr io.Writer) error {
fmt.Fprintln(stdout, Version)
return nil
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) {
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)
}
if !strings.Contains(stdout.String(), "uatu <command>") {
if !strings.Contains(stdout.String(), "sanderling <command>") {
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"} {
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)
}
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) {
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") {
t.Fatalf("expected unknown-command error, got %v", err)
}
@@ -125,7 +125,7 @@ func TestRun_Doctor(t *testing.T) {
var stdout bytes.Buffer
// Doctor may pass or fail depending on host environment; we just want to
// 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()
if !strings.Contains(output, "OK") && !strings.Contains(output, "FAIL") {
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
// wiring reaches the runner.
err := run([]string{
"uatu", "test",
"sanderling", "test",
"--spec", "definitely-missing-spec.ts",
"--bundle-id", "com.example",
"--avd", "definitely-missing-avd",
File renamed without changes.
@@ -12,17 +12,17 @@ import (
"strconv"
"time"
"github.com/priyanshujain/uatu/internal/agent"
"github.com/priyanshujain/uatu/internal/bundler"
"github.com/priyanshujain/uatu/internal/driver/maestro"
"github.com/priyanshujain/uatu/internal/runner"
"github.com/priyanshujain/uatu/internal/sidecar"
"github.com/priyanshujain/uatu/internal/trace"
"github.com/priyanshujain/uatu/internal/verifier"
"github.com/priyanshujain/sanderling/internal/agent"
"github.com/priyanshujain/sanderling/internal/bundler"
"github.com/priyanshujain/sanderling/internal/driver/maestro"
"github.com/priyanshujain/sanderling/internal/runner"
"github.com/priyanshujain/sanderling/internal/sidecar"
"github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/sanderling/internal/verifier"
)
const (
socketName = "uatu-agent"
socketName = "sanderling-agent"
sidecarStartupTimeout = 30 * time.Second
sdkAcceptTimeout = 60 * time.Second
)
@@ -35,17 +35,17 @@ func runTestPipeline(ctx context.Context, options testOptions, stdout io.Writer)
}
aliases := map[string]string{}
if specApiPath := resolveSpecAPIPath(options.spec); specApiPath != "" {
aliases["@uatu/spec"] = specApiPath
aliases["@sanderling/spec"] = specApiPath
// 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)
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{
EntryFile: options.spec,
Defines: map[string]string{
"UATU_TEST_PHONE": os.Getenv("UATU_TEST_PHONE"),
"UATU_TEST_OTP": os.Getenv("UATU_TEST_OTP"),
"SANDERLING_TEST_PHONE": os.Getenv("SANDERLING_TEST_PHONE"),
"SANDERLING_TEST_OTP": os.Getenv("SANDERLING_TEST_OTP"),
},
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])
sidecarDirectory := filepath.Join(os.TempDir(), "uatu-sidecar")
sidecarDirectory := filepath.Join(os.TempDir(), "sanderling-sidecar")
jarPath, err := sidecar.Extract(sidecarDirectory)
if err != nil {
return fmt.Errorf("extract sidecar: %w", err)
@@ -166,7 +166,7 @@ func runTestPipeline(ctx context.Context, options testOptions, stdout io.Writer)
Platform: options.platform,
BundleID: options.bundleID,
StartedAt: time.Now().UTC(),
UatuVersion: "0.0.1",
SanderlingVersion: "0.0.1",
}
if err := traceWriter.WriteMeta(meta); err != nil {
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
// a uatu source checkout, searched upward from the spec file and the cwd.
// Returns "" when not found, in which case esbuild resolves @uatu/spec via
// a sanderling source checkout, searched upward from the spec file and the cwd.
// Returns "" when not found, in which case esbuild resolves @sanderling/spec via
// node_modules the way a downstream user's project would.
func resolveSpecAPIPath(specPath string) string {
candidates := []string{}
@@ -47,6 +47,6 @@ func TestResolveSpecAPIPath_ReturnsEmptyWhenMissing(t *testing.T) {
got := resolveSpecAPIPath(specPath)
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>
<meta charset="utf-8">
<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">
<style>$highlighting-css$</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a class="brand" href="__ROOT__index.html">uatu</a>
<a class="brand" href="__ROOT__index.html">sanderling</a>
<nav>
<h3>Manual</h3>
<ul>
@@ -31,8 +31,8 @@
$body$
</article>
<footer>
<a href="https://github.com/priyanshujain/uatu">github.com/priyanshujain/uatu</a>
· <a href="https://github.com/priyanshujain/uatu/issues/4">v0.1.0 roadmap</a>
<a href="https://github.com/priyanshujain/sanderling">github.com/priyanshujain/sanderling</a>
· <a href="https://github.com/priyanshujain/sanderling/issues/4">v0.1.0 roadmap</a>
</footer>
</main>
</div>
+3 -3
View File
@@ -8,7 +8,7 @@ Three processes, two transports.
```mermaid
flowchart TB
subgraph Go["uatu (Go)"]
subgraph Go["sanderling (Go)"]
Bundler[Bundler<br/>esbuild] --> Verifier[Verifier<br/>goja + LTL]
Verifier <--> Runner[Runner]
Runner --> Trace[Trace writer<br/>JSONL + PNG]
@@ -21,7 +21,7 @@ flowchart TB
subgraph Device["Emulator"]
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
@@ -33,7 +33,7 @@ flowchart TB
## 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.
+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`.
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
+1 -1
View File
@@ -6,7 +6,7 @@ title: Development
- [Design principles](./design-principles.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
+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.
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
+7 -7
View File
@@ -5,10 +5,10 @@ title: CLI reference
# CLI reference
```
uatu <command> [flags]
sanderling <command> [flags]
```
## `uatu test`
## `sanderling test`
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`. |
| `--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`.
@@ -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.
## `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.
@@ -49,4 +49,4 @@ Print the CLI version.
- `--max-steps` hard cap on step count.
- `--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
- 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.
Run `uatu doctor` to check the host environment.
Run `sanderling doctor` to check the host environment.
## Install
@@ -21,15 +21,15 @@ Run `uatu doctor` to check the host environment.
macOS arm64:
```sh
curl -L https://github.com/priyanshujain/uatu/releases/latest/download/uatu_<version>_darwin_arm64.tar.gz | tar xz
./uatu version
curl -L https://github.com/priyanshujain/sanderling/releases/latest/download/sanderling_<version>_darwin_arm64.tar.gz | tar xz
./sanderling version
```
Linux amd64:
```sh
curl -L https://github.com/priyanshujain/uatu/releases/latest/download/uatu_<version>_linux_amd64.tar.gz | tar xz
./uatu version
curl -L https://github.com/priyanshujain/sanderling/releases/latest/download/sanderling_<version>_linux_amd64.tar.gz | tar xz
./sanderling version
```
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)
```sh
npm install --save-dev @uatu/spec
npm install --save-dev @sanderling/spec
```
### Android SDK (Maven Central)
@@ -55,14 +55,14 @@ The repo ships a working sample at `examples/folio`. From that directory:
```sh
npm install
(cd android && ./gradlew installDebug)
uatu test \
sanderling test \
--spec spec.ts \
--bundle-id app.folio \
--platform android \
--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>/`:
+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`.
@@ -51,11 +51,11 @@ Two-process loop:
```
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:
```
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?
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.
## 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)
├── 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
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.
@@ -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.
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.
```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.
const loggedIn = extract((s) => !!s.ax.find("id:home-tab-bar"));
@@ -18,7 +18,7 @@ export const properties = {
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(
[10, taps],
[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.
`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)
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.
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
// in the app (Android)
Uatu.extract("cart_count") { store.cart.size }
Sanderling.extract("cart_count") { store.cart.size }
```
```ts
+2 -2
View File
@@ -6,7 +6,7 @@ iosApp/build/
iosApp/iosApp.xcodeproj/
iosApp/iosApp.xcodeproj/**
.DS_Store
uatu/node_modules/
uatu/runs/
sanderling/node_modules/
sanderling/runs/
.playwright-mcp/
web-*.png
+9 -9
View File
@@ -3,14 +3,14 @@
A minimal Kotlin Multiplatform personal-ledger app: login with demo
credentials, create accounts, add credits and debits. Shared UI across
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
- Kotlin Multiplatform + Compose Multiplatform (shared UI)
- kotlinx.serialization for file-backed persistence
- kotlinx.coroutines for state flows
- uatu `sdk-android` for harness integration on Android
- sanderling `sdk-android` for harness integration on Android
## Prerequisites
@@ -46,13 +46,13 @@ email: [email protected]
password: ledger123
```
## Run a uatu test (Android)
## Run a sanderling test (Android)
```sh
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:
```sh
@@ -66,13 +66,13 @@ AVD=Pixel_7
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`,
`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
- `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")
}
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"
kotlin {
@@ -83,7 +83,7 @@ kotlin {
androidMain.dependencies {
implementation("androidx.activity:activity-compose:1.13.0")
implementation("app.cash.sqldelight:android-driver:$sqldelightVersion")
implementation("io.github.priyanshujain:sdk-android:$uatuVersion")
implementation("io.github.priyanshujain:sdk-android:$sanderlingVersion")
}
iosMain.dependencies {
@@ -101,7 +101,7 @@ android {
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = uatuVersion
versionName = sanderlingVersion
}
compileOptions {
@@ -11,18 +11,18 @@ import app.folio.navigation.Navigator
import app.folio.navigation.Route
import app.folio.platform.balanceOf
import app.folio.platform.signedAmount
import dev.uatu.sdk.Uatu
import dev.sanderling.sdk.Sanderling
class FolioApplication : Application() {
override fun onCreate() {
super.onCreate()
AndroidLedgerContext.context = applicationContext
Repository.init()
Uatu.start(this)
Uatu.extract("logged_in") { Repository.session.value != null }
Uatu.extract("account_count") { Repository.accounts.value.size }
Uatu.extract("total_balance") { balanceOf(Repository.transactions.value) }
Uatu.extract("route") {
Sanderling.start(this)
Sanderling.extract("logged_in") { Repository.session.value != null }
Sanderling.extract("account_count") { Repository.accounts.value.size }
Sanderling.extract("total_balance") { balanceOf(Repository.transactions.value) }
Sanderling.extract("route") {
when (Navigator.current.value) {
Route.Login -> "login"
Route.Home -> "home"
@@ -31,10 +31,10 @@ class FolioApplication : Application() {
is Route.AddTransaction -> "add-transaction"
}
}
Uatu.extract("auth_status") {
Sanderling.extract("auth_status") {
if (Repository.session.value != null) "logged-in" else "logged-out"
}
Uatu.extract("accounts") {
Sanderling.extract("accounts") {
val txns = Repository.transactions.value
Repository.accounts.value.map { a ->
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) {
is Route.Ledger -> r.accountId
is Route.AddTransaction -> r.accountId
else -> null
}
}
Uatu.extract("ledger_rows") {
Sanderling.extract("ledger_rows") {
val active = when (val r = Navigator.current.value) {
is Route.Ledger -> 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) {
is Route.Ledger -> r.accountId
is Route.AddTransaction -> r.accountId
@@ -81,13 +81,13 @@ class FolioApplication : Application() {
if (active == null) 0L
else balanceOf(Repository.transactions.value.filter { it.accountId == active })
}
Uatu.extract("focused_input") { FocusTracker.current.value }
Uatu.extract("txn_form_type") { AddTransactionUiState.txnFormType.value }
Uatu.extract("txn_form_account_id") {
Sanderling.extract("focused_input") { FocusTracker.current.value }
Sanderling.extract("txn_form_type") { AddTransactionUiState.txnFormType.value }
Sanderling.extract("txn_form_account_id") {
(Navigator.current.value as? Route.AddTransaction)?.accountId
}
Uatu.extract("login_error") { LoginUiState.loginError.value }
Uatu.extract("add_account_error") { AddAccountUiState.addAccountError.value }
Uatu.extract("txn_error") { AddTransactionUiState.txnError.value }
Sanderling.extract("login_error") { LoginUiState.loginError.value }
Sanderling.extract("add_account_error") { AddAccountUiState.addAccountError.value }
Sanderling.extract("txn_error") { AddTransactionUiState.txnError.value }
}
}
@@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.browser.localStorage
private const val STORAGE_KEY = "uatu.ledger.v1"
private const val STORAGE_KEY = "sanderling.ledger.v1"
class WebLedgerStore : LedgerStore {
private val _accounts = MutableStateFlow<List<Account>>(emptyList())
+3 -3
View File
@@ -5,6 +5,6 @@ kotlin.code.style=official
android.useAndroidX=true
android.nonTransitiveRClass=true
# Pin the uatu SDK version consumed by this app. Override with
# `-Puatu.version=<version>`.
uatu.version=0.0.0-dev
# Pin the sanderling SDK version consumed by this app. Override with
# `-Psanderling.version=<version>`.
sanderling.version=0.0.0-dev
+8 -8
View File
@@ -1,11 +1,11 @@
set shell := ["bash", "-cu"]
set dotenv-load := true
uatu := env_var_or_default("UATU", "uatu")
avd := env_var_or_default("AVD", "")
duration := env_var_or_default("DURATION", "1m")
seed := env_var_or_default("SEED", "0")
output := env_var_or_default("OUTPUT", justfile_directory() / "uatu" / "runs")
sanderling := env_var_or_default("SANDERLING", "sanderling")
avd := env_var_or_default("AVD", "")
duration := env_var_or_default("DURATION", "1m")
seed := env_var_or_default("SEED", "0")
output := env_var_or_default("OUTPUT", justfile_directory() / "sanderling" / "runs")
ios_device := env_var_or_default("IOS_DEVICE", "iPhone 17 Pro")
default:
@@ -81,7 +81,7 @@ ios:
xcrun simctl install booted iosApp/build/Build/Products/Debug-iphonesimulator/iosApp.app
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.
test:
#!/usr/bin/env bash
@@ -90,8 +90,8 @@ test:
if [[ -n "{{avd}}" ]]; then
avd_flag=(--avd "{{avd}}")
fi
"{{uatu}}" test \
--spec "{{justfile_directory()}}/uatu/spec.ts" \
"{{sanderling}}" test \
--spec "{{justfile_directory()}}/sanderling/spec.ts" \
--bundle-id app.folio \
"${avd_flag[@]}" \
--duration "{{duration}}" \
@@ -6,12 +6,12 @@
"": {
"name": "folio",
"devDependencies": {
"@uatu/spec": "next"
"@sanderling/spec": "next"
}
},
"node_modules/@uatu/spec": {
"node_modules/@sanderling/spec": {
"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==",
"dev": true,
"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,
waitOnce,
weighted,
} from "@uatu/spec";
import { noUncaughtExceptions } from "@uatu/spec/defaults/properties";
} from "@sanderling/spec";
import { noUncaughtExceptions } from "@sanderling/spec/defaults/properties";
interface AccountSnapshot {
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
+2 -2
View File
@@ -10,6 +10,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
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),
# 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.
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) {
entry := writeFixture(t, "spec.ts", `
const phone = process.env.UATU_TEST_PHONE;
const otp = process.env.UATU_TEST_OTP;
const phone = process.env.SANDERLING_TEST_PHONE;
const otp = process.env.SANDERLING_TEST_OTP;
export const credentials = { phone, otp };
`)
result, err := Bundle(Options{
EntryFile: entry,
Defines: map[string]string{
"UATU_TEST_PHONE": "+919876543210",
"UATU_TEST_OTP": "123456",
"SANDERLING_TEST_PHONE": "+919876543210",
"SANDERLING_TEST_OTP": "123456",
},
})
if err != nil {
@@ -87,13 +87,13 @@ func TestBundle_DeterministicHash(t *testing.T) {
func TestBundle_HashChangesWithDefines(t *testing.T) {
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 {
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 {
t.Fatal(err)
}
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/priyanshujain/uatu/internal/driver"
driverpb "github.com/priyanshujain/uatu/proto/driverpb"
"github.com/priyanshujain/sanderling/internal/driver"
driverpb "github.com/priyanshujain/sanderling/proto/driverpb"
)
type Client struct {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"google.golang.org/grpc"
driverpb "github.com/priyanshujain/uatu/proto/driverpb"
driverpb "github.com/priyanshujain/sanderling/proto/driverpb"
)
type fakeServer struct {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"sync"
"time"
"github.com/priyanshujain/uatu/internal/driver"
"github.com/priyanshujain/sanderling/internal/driver"
)
type ActionKind string
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"testing"
"time"
"github.com/priyanshujain/uatu/internal/driver"
"github.com/priyanshujain/sanderling/internal/driver"
)
func TestNew_Defaults(t *testing.T) {
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<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>
<link rel="stylesheet" crossorigin href="/assets/index-BSJUf7yI.css">
</head>
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"sync"
"time"
"github.com/priyanshujain/uatu/internal/trace"
"github.com/priyanshujain/sanderling/internal/trace"
)
// Maximum size of a single trace.jsonl line. Hierarchies and snapshots
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"testing"
"time"
"github.com/priyanshujain/uatu/internal/trace"
"github.com/priyanshujain/sanderling/internal/trace"
)
func TestScan_OrdersByStartedAtDescendingAndCountsViolations(t *testing.T) {
+1 -1
View File
@@ -20,7 +20,7 @@ type ServerOptions struct {
DevTarget string
}
// Server holds the HTTP handlers for `uatu inspect`.
// Server holds the HTTP handlers for `sanderling inspect`.
type Server struct {
options ServerOptions
cache *Cache
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"testing"
"time"
"github.com/priyanshujain/uatu/internal/trace"
"github.com/priyanshujain/sanderling/internal/trace"
)
func newFixtureServer(t *testing.T) (*Server, string) {
+6 -6
View File
@@ -8,12 +8,12 @@ import (
"log/slog"
"time"
"github.com/priyanshujain/uatu/internal/agent"
"github.com/priyanshujain/uatu/internal/driver"
"github.com/priyanshujain/uatu/internal/hierarchy"
"github.com/priyanshujain/uatu/internal/ltl"
"github.com/priyanshujain/uatu/internal/trace"
"github.com/priyanshujain/uatu/internal/verifier"
"github.com/priyanshujain/sanderling/internal/agent"
"github.com/priyanshujain/sanderling/internal/driver"
"github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/ltl"
"github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/sanderling/internal/verifier"
)
type Options struct {
+10 -10
View File
@@ -15,19 +15,19 @@ import (
"testing"
"time"
"github.com/priyanshujain/uatu/internal/agent"
mockdriver "github.com/priyanshujain/uatu/internal/driver/mock"
"github.com/priyanshujain/uatu/internal/trace"
"github.com/priyanshujain/uatu/internal/verifier"
"github.com/priyanshujain/sanderling/internal/agent"
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
"github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/sanderling/internal/verifier"
)
const fixtureSpec = `
const screen = __uatu__.extract(state => state.snapshots.screen ?? "");
const balance = __uatu__.extract(state => state.snapshots.balance ?? 0);
const screen = __sanderling__.extract(state => state.snapshots.screen ?? "");
const balance = __sanderling__.extract(state => state.snapshots.balance ?? 0);
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 {
@@ -195,9 +195,9 @@ func TestRunner_ViolationSurfacesInSummary(t *testing.T) {
func TestRunner_ThrowingPredicateIsLoggedNotPanic(t *testing.T) {
const throwingSpec = `
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.startSDK(t)
+2 -2
View File
@@ -23,12 +23,12 @@ func EmbeddedSHA256() string {
// matching checksum, no rewrite happens. Returns the JAR path.
func Extract(dir string) (string, error) {
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 {
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"
checksum := EmbeddedSHA256()
+1 -1
View File
@@ -5,6 +5,6 @@ package sidecar
var embeddedJAR []byte
// 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.
func IsPlaceholder() bool { return true }
+1 -1
View File
@@ -25,7 +25,7 @@ func TestExtract_WritesJARAndChecksum(t *testing.T) {
if err != nil {
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)
}
body, err := os.ReadFile(path)
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"sync"
"time"
"github.com/priyanshujain/uatu/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/hierarchy"
)
type Step struct {
@@ -74,7 +74,7 @@ type Meta struct {
BundleID string `json:"bundle_id"`
StartedAt time.Time `json:"started_at"`
EndedAt *time.Time `json:"ended_at,omitempty"`
UatuVersion string `json:"uatu_version"`
SanderlingVersion string `json:"sanderling_version"`
}
type Writer struct {
+2 -2
View File
@@ -25,7 +25,7 @@ func TestWriteMeta_RoundTrip(t *testing.T) {
Platform: "android",
BundleID: "in.okcredit.merchant",
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 {
t.Fatal(err)
@@ -60,7 +60,7 @@ func TestWriteMeta_EndedAtRoundTrip(t *testing.T) {
BundleID: "in.test",
StartedAt: time.Date(2026, 4, 17, 22, 30, 0, 0, time.UTC),
EndedAt: &endedAt,
UatuVersion: "0.0.1",
SanderlingVersion: "0.0.1",
}
if err := writer.WriteMeta(meta); err != nil {
t.Fatal(err)
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"os"
"testing"
"github.com/priyanshujain/uatu/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/hierarchy"
)
// TestStateAxFindWorks verifies that a Parse+PushSnapshot+extract round trip
@@ -24,11 +24,11 @@ func TestStateAxFindWorks(t *testing.T) {
verifier := newVerifier(t)
mustLoad(t, verifier, `
globalThis.probe = __uatu__.extract(state => {
globalThis.probe = __sanderling__.extract(state => {
const element = state.ax.find("id:select_language");
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 {
+29 -29
View File
@@ -80,11 +80,11 @@ type formulaSpec struct {
}
const (
tagFormula = "__uatuFormula"
tagFormulaSpecIndex = "__uatuFormulaSpec"
tagActionGenerator = "__uatuActionGenerator"
tagInternalKind = "__uatuKind"
tagSelector = "__uatuSelector"
tagFormula = "__sanderlingFormula"
tagFormulaSpecIndex = "__sanderlingFormulaSpec"
tagActionGenerator = "__sanderlingActionGenerator"
tagInternalKind = "__sanderlingKind"
tagSelector = "__sanderlingSelector"
internalKindActions = "actions"
internalKindWeighted = "weighted"
@@ -94,63 +94,63 @@ const (
internalKindBuiltinPressKey = "pressKey"
)
// installRuntimeBindings exposes globalThis.__uatu__ to the loaded spec.
// installRuntimeBindings exposes globalThis.__sanderling__ to the loaded spec.
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
}
if err := uatu.Set("always", v.bindAlways); err != nil {
if err := sanderling.Set("always", v.bindAlways); err != nil {
return err
}
if err := uatu.Set("now", v.bindNow); err != nil {
if err := sanderling.Set("now", v.bindNow); err != nil {
return err
}
if err := uatu.Set("next", v.bindNext); err != nil {
if err := sanderling.Set("next", v.bindNext); err != nil {
return err
}
if err := uatu.Set("eventually", v.bindEventually); err != nil {
if err := sanderling.Set("eventually", v.bindEventually); err != nil {
return err
}
if err := uatu.Set("actions", v.bindActions); err != nil {
if err := sanderling.Set("actions", v.bindActions); err != nil {
return err
}
if err := uatu.Set("weighted", v.bindWeighted); err != nil {
if err := sanderling.Set("weighted", v.bindWeighted); err != nil {
return err
}
if err := uatu.Set("from", v.bindFrom); err != nil {
if err := sanderling.Set("from", v.bindFrom); err != nil {
return err
}
if err := uatu.Set("tap", v.bindTap); err != nil {
if err := sanderling.Set("tap", v.bindTap); err != nil {
return err
}
if err := uatu.Set("inputText", v.bindInputText); err != nil {
if err := sanderling.Set("inputText", v.bindInputText); err != nil {
return err
}
if err := uatu.Set("swipe", v.bindSwipe); err != nil {
if err := sanderling.Set("swipe", v.bindSwipe); err != nil {
return err
}
if err := uatu.Set("pressKey", v.bindPressKey); err != nil {
if err := sanderling.Set("pressKey", v.bindPressKey); err != nil {
return err
}
if err := uatu.Set("wait", v.bindWait); err != nil {
if err := sanderling.Set("wait", v.bindWait); err != nil {
return err
}
if err := uatu.Set("taps", v.builtinGenerator(internalKindBuiltinTaps)); err != nil {
if err := sanderling.Set("taps", v.builtinGenerator(internalKindBuiltinTaps)); err != nil {
return err
}
if err := uatu.Set("swipes", v.builtinGenerator(internalKindBuiltinSwipes)); err != nil {
if err := sanderling.Set("swipes", v.builtinGenerator(internalKindBuiltinSwipes)); err != nil {
return err
}
if err := uatu.Set("waitOnce", v.builtinGenerator(internalKindBuiltinWaitOnce)); err != nil {
if err := sanderling.Set("waitOnce", v.builtinGenerator(internalKindBuiltinWaitOnce)); err != nil {
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 v.runtime.GlobalObject().Set("__uatu__", uatu)
return v.runtime.GlobalObject().Set("__sanderling__", sanderling)
}
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
// handle (new shape). Both produce a formula handle tagged with
// __uatuFormulaSpec.
// __sanderlingFormulaSpec.
func (v *Verifier) bindAlways(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 1 {
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.Set(tagFormula, true)
_ = 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.
_ = handle.Set("__uatuIndex", index)
_ = handle.Set("__sanderlingIndex", index)
_ = handle.Set("implies", v.binaryChain(index, specKindImplies))
_ = 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) {
if value == nil || goja.IsNull(value) || goja.IsUndefined(value) {
return 0, false
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"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.
+6 -6
View File
@@ -5,13 +5,13 @@ import (
"path/filepath"
"testing"
"github.com/priyanshujain/uatu/internal/bundler"
"github.com/priyanshujain/uatu/internal/hierarchy"
"github.com/priyanshujain/uatu/internal/ltl"
"github.com/priyanshujain/sanderling/internal/bundler"
"github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/ltl"
)
// 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.
func bundleIntegrationSpec(t *testing.T) string {
t.Helper()
@@ -30,8 +30,8 @@ func bundleIntegrationSpec(t *testing.T) string {
bundle, err := bundler.Bundle(bundler.Options{
EntryFile: specPath,
Aliases: map[string]string{
"@uatu/spec": apiPath,
"@uatu/spec/defaults/properties": defaultsPath,
"@sanderling/spec": apiPath,
"@sanderling/spec/defaults/properties": defaultsPath,
},
})
if err != nil {
+2 -2
View File
@@ -6,8 +6,8 @@ import {
eventually,
extract,
weighted,
} from "@uatu/spec";
import { noUncaughtExceptions } from "@uatu/spec/defaults/properties";
} from "@sanderling/spec";
import { noUncaughtExceptions } from "@sanderling/spec/defaults/properties";
const route = extract<string>(
(state) => (state.snapshots.route as string) ?? "",
+14 -14
View File
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
"github.com/priyanshujain/uatu/internal/ltl"
"github.com/priyanshujain/sanderling/internal/ltl"
)
func newVerifier(t *testing.T, options ...Option) *Verifier {
@@ -27,18 +27,18 @@ func mustLoad(t *testing.T, verifier *Verifier, source string) {
}
const helloSpec = `
const screen = __uatu__.extract(state => state.snapshots.screen ?? "");
const balance = __uatu__.extract(state => state.snapshots["ledger.balance"] ?? 0);
const screen = __sanderling__.extract(state => state.snapshots.screen ?? "");
const balance = __sanderling__.extract(state => state.snapshots["ledger.balance"] ?? 0);
globalThis.screen = screen;
globalThis.balance = balance;
globalThis.properties = {
balanceNonNegative: __uatu__.always(() => balance.current >= 0),
balanceNonNegative: __sanderling__.always(() => balance.current >= 0),
};
globalThis.actions = __uatu__.actions(() => [
__uatu__.tap({ on: "id:home_button" }),
globalThis.actions = __sanderling__.actions(() => [
__sanderling__.tap({ on: "id:home_button" }),
]);
`
@@ -135,9 +135,9 @@ func TestNextAction_FromActionsGenerator(t *testing.T) {
func TestNextAction_WeightedSelectsByWeight(t *testing.T) {
verifier := newVerifier(t, WithRand(rand.New(rand.NewPCG(42, 0))))
mustLoad(t, verifier, `
const tapHome = __uatu__.actions(() => [__uatu__.tap({ on: "id:home" })]);
const tapAway = __uatu__.actions(() => [__uatu__.tap({ on: "id:away" })]);
globalThis.actions = __uatu__.weighted(
const tapHome = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:home" })]);
const tapAway = __sanderling__.actions(() => [__sanderling__.tap({ on: "id:away" })]);
globalThis.actions = __sanderling__.weighted(
[1, tapHome],
[99, tapAway],
);
@@ -166,7 +166,7 @@ func TestNextAction_WeightedSelectsByWeight(t *testing.T) {
func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) {
verifier := newVerifier(t)
mustLoad(t, verifier, `
globalThis.actions = __uatu__.actions(() => []);
globalThis.actions = __sanderling__.actions(() => []);
`)
_ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}})
@@ -179,8 +179,8 @@ func TestNextAction_EmptyGeneratorReturnsErrNoAction(t *testing.T) {
func TestInputText_RoundTrip(t *testing.T) {
verifier := newVerifier(t)
mustLoad(t, verifier, `
globalThis.actions = __uatu__.actions(() => [
__uatu__.inputText({ into: "id:phone", text: "+919876543210" }),
globalThis.actions = __sanderling__.actions(() => [
__sanderling__.inputText({ into: "id:phone", text: "+919876543210" }),
]);
`)
_ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}})
@@ -200,7 +200,7 @@ func TestInputText_RoundTrip(t *testing.T) {
func TestPushSnapshot_FeedsSnapshotsToExtractorState(t *testing.T) {
verifier := newVerifier(t)
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 {
t.Fatal(err)
@@ -222,7 +222,7 @@ func TestLoad_PropagatesSyntaxError(t *testing.T) {
func TestEvaluateProperties_ThrowingPredicateDoesNotPanic(t *testing.T) {
const spec = `
globalThis.properties = {
broken: __uatu__.always(() => { throw new Error("bad predicate"); }),
broken: __sanderling__.always(() => { throw new Error("bad predicate"); }),
};
`
verifier := newVerifier(t)
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"github.com/dop251/goja"
"github.com/priyanshujain/uatu/internal/hierarchy"
"github.com/priyanshujain/uatu/internal/ltl"
"github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/ltl"
)
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
```sh
npm install --save-dev @uatu/spec
npm install --save-dev @sanderling/spec
```
## Usage
```ts
import { extract, always, actions, Tap, weighted } from "@uatu/spec";
import { extract, always, actions, Tap, weighted } from "@sanderling/spec";
export const spec = {
extract: extract((tree) => ({
@@ -32,7 +32,7 @@ export const spec = {
## 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
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@uatu/spec",
"name": "@sanderling/spec",
"version": "0.0.0-dev",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@uatu/spec",
"name": "@sanderling/spec",
"version": "0.0.0-dev",
"license": "Apache-2.0",
"devDependencies": {
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@uatu/spec",
"name": "@sanderling/spec",
"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",
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
@@ -21,12 +21,12 @@
],
"repository": {
"type": "git",
"url": "git+https://github.com/priyanshujain/uatu.git",
"url": "git+https://github.com/priyanshujain/sanderling.git",
"directory": "pkg/spec-api"
},
"homepage": "https://github.com/priyanshujain/uatu",
"homepage": "https://github.com/priyanshujain/sanderling",
"bugs": {
"url": "https://github.com/priyanshujain/uatu/issues"
"url": "https://github.com/priyanshujain/sanderling/issues"
},
"license": "Apache-2.0",
"author": "Priyanshu Jain",
+9 -9
View File
@@ -14,26 +14,26 @@ import type {
} from "./types.ts";
export function actions(generator: () => Action[]): ActionGenerator {
return globalThis.__uatu__.actions(generator);
return globalThis.__sanderling__.actions(generator);
}
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> {
return globalThis.__uatu__.from(items);
return globalThis.__sanderling__.from(items);
}
export function Tap(parameters: { on: string | AccessibilityElement }): TapAction {
return globalThis.__uatu__.tap(parameters);
return globalThis.__sanderling__.tap(parameters);
}
export function InputText(parameters: {
into: string | AccessibilityElement;
text: string;
}): InputTextAction {
return globalThis.__uatu__.inputText(parameters);
return globalThis.__sanderling__.inputText(parameters);
}
export function Swipe(parameters: {
@@ -41,21 +41,21 @@ export function Swipe(parameters: {
to: Point | AccessibilityElement;
durationMillis?: number;
}): SwipeAction {
return globalThis.__uatu__.swipe(parameters);
return globalThis.__sanderling__.swipe(parameters);
}
export function PressKey(parameters: { key: Key }): PressKeyAction {
return globalThis.__uatu__.pressKey(parameters);
return globalThis.__sanderling__.pressKey(parameters);
}
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 {
return new Proxy({} as ActionGenerator, {
get(_target, property) {
const runtime = globalThis.__uatu__[name] as unknown as Record<
const runtime = globalThis.__sanderling__[name] as unknown as Record<
string | symbol,
unknown
>;
+1 -1
View File
@@ -4,7 +4,7 @@ import type { Formula } from "../types.ts";
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.
export const noUncaughtExceptions: Formula = always(
() => exceptionCount.current === 0,
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Extracted, State } from "./types.ts";
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,
SwipeAction,
TapAction,
UatuRuntime,
SanderlingRuntime,
WaitAction,
WeightedEntry,
} from "./types.ts";
+4 -4
View File
@@ -1,20 +1,20 @@
import type { EventuallyFormula, Formula } from "./types.ts";
export function always(predicateOrFormula: (() => boolean) | Formula): Formula {
return globalThis.__uatu__.always(predicateOrFormula);
return globalThis.__sanderling__.always(predicateOrFormula);
}
export function now(predicate: () => boolean): Formula {
return globalThis.__uatu__.now(predicate);
return globalThis.__sanderling__.now(predicate);
}
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 —
// prefer `.within(n, unit)` when you want the verifier to fail a property
// that stalls.
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";
export interface ActionGenerator {
readonly __uatuActionGenerator: true;
readonly __sanderlingActionGenerator: true;
generate(): Action[];
}
export interface Formula {
readonly __uatuFormula: true;
readonly __sanderlingFormula: true;
implies(other: Formula): Formula;
or(other: Formula): Formula;
and(other: Formula): Formula;
@@ -98,7 +98,7 @@ export interface Sampler<T> {
generate(): T;
}
export interface UatuRuntime {
export interface SanderlingRuntime {
extract: <T>(getter: (state: State) => T) => Extracted<T>;
always: (predicateOrFormula: (() => boolean) | Formula) => Formula;
now: (predicate: () => boolean) => Formula;
@@ -129,5 +129,5 @@ export type WeightedEntry = readonly [number, ActionGenerator];
declare global {
// eslint-disable-next-line no-var
var __uatu__: UatuRuntime;
var __sanderling__: SanderlingRuntime;
}
+18 -18
View File
@@ -29,11 +29,11 @@ import type {
Formula,
Sampler,
State,
UatuRuntime,
SanderlingRuntime,
WeightedEntry,
} from "../src/types.ts";
interface RecordedRuntime extends UatuRuntime {
interface RecordedRuntime extends SanderlingRuntime {
extracts: Array<(state: State) => unknown>;
alwaysArgs: Array<(() => boolean) | Formula>;
nowPredicates: Array<() => boolean>;
@@ -51,7 +51,7 @@ interface RecordedRuntime extends UatuRuntime {
function makeChainableFormula(record: RecordedRuntime): Formula {
const formula: Formula = {
__uatuFormula: true,
__sanderlingFormula: true,
implies(other: Formula): Formula {
record.impliesCalls++;
void other;
@@ -125,11 +125,11 @@ function installFakeRuntime(): RecordedRuntime {
},
actions: (generator: () => Action[]): ActionGenerator => {
calls.actionGenerators.push(generator);
return { __uatuActionGenerator: true, generate: generator };
return { __sanderlingActionGenerator: true, generate: generator };
},
weighted: (...entries: WeightedEntry[]): ActionGenerator => {
calls.weightedCalls.push(entries);
return { __uatuActionGenerator: true, generate: () => [] };
return { __sanderlingActionGenerator: true, generate: () => [] };
},
from: <T>(items: readonly T[]): Sampler<T> => {
calls.fromCalls.push(items as unknown[]);
@@ -145,13 +145,13 @@ function installFakeRuntime(): RecordedRuntime {
}),
pressKey: ({ key }) => ({ kind: "PressKey", key }),
wait: ({ durationMillis }) => ({ kind: "Wait", durationMillis }),
taps: { __uatuActionGenerator: true, generate: () => [] },
swipes: { __uatuActionGenerator: true, generate: () => [] },
waitOnce: { __uatuActionGenerator: true, generate: () => [] },
pressKeys: { __uatuActionGenerator: true, generate: () => [] },
} satisfies UatuRuntime;
taps: { __sanderlingActionGenerator: true, generate: () => [] },
swipes: { __sanderlingActionGenerator: true, generate: () => [] },
waitOnce: { __sanderlingActionGenerator: true, generate: () => [] },
pressKeys: { __sanderlingActionGenerator: true, generate: () => [] },
} satisfies SanderlingRuntime;
const recorded = Object.assign(runtime, calls) as RecordedRuntime;
globalThis.__uatu__ = recorded;
globalThis.__sanderling__ = recorded;
return recorded;
}
@@ -168,7 +168,7 @@ test("always wraps a predicate into a formula via the runtime", () => {
const predicate = () => true;
const formula = always(predicate);
assert.equal(runtime.alwaysArgs[0], predicate);
assert.equal(formula.__uatuFormula, true);
assert.equal(formula.__sanderlingFormula, true);
});
test("always accepts a formula handle", () => {
@@ -176,7 +176,7 @@ test("always accepts a formula handle", () => {
const inner = now(() => true);
const wrapped = always(inner);
assert.equal(runtime.alwaysArgs.at(-1), inner);
assert.equal(wrapped.__uatuFormula, true);
assert.equal(wrapped.__sanderlingFormula, true);
});
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 wrapped = actions(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", () => {
@@ -279,8 +279,8 @@ test("from forwards items to the runtime", () => {
test("default generators proxy through to the runtime", () => {
installFakeRuntime();
assert.equal(taps.__uatuActionGenerator, true);
assert.equal(swipes.__uatuActionGenerator, true);
assert.equal(waitOnce.__uatuActionGenerator, true);
assert.equal(pressKey.__uatuActionGenerator, true);
assert.equal(taps.__sanderlingActionGenerator, true);
assert.equal(swipes.__sanderlingActionGenerator, true);
assert.equal(waitOnce.__sanderlingActionGenerator, true);
assert.equal(pressKey.__sanderlingActionGenerator, true);
});
+16 -16
View File
@@ -9,11 +9,11 @@ import type {
Formula,
Sampler,
State,
UatuRuntime,
SanderlingRuntime,
WeightedEntry,
} from "../src/types.ts";
interface RecordedRuntime extends UatuRuntime {
interface RecordedRuntime extends SanderlingRuntime {
currentState: State;
extractors: Array<(state: State) => unknown>;
alwaysArgs: Array<(() => boolean) | Formula>;
@@ -43,17 +43,17 @@ function installRuntime(initialState: State): RecordedRuntime {
if (typeof predicateOrFormula === "function") {
lastPredicate = predicateOrFormula;
}
return { __uatuFormula: true } as Formula;
return { __sanderlingFormula: true } as Formula;
},
now: () => ({ __uatuFormula: true } as Formula),
next: () => ({ __uatuFormula: true } as Formula),
eventually: () => ({ __uatuFormula: true } as EventuallyFormula),
now: () => ({ __sanderlingFormula: true } as Formula),
next: () => ({ __sanderlingFormula: true } as Formula),
eventually: () => ({ __sanderlingFormula: true } as EventuallyFormula),
actions: (generator: () => Action[]): ActionGenerator => ({
__uatuActionGenerator: true,
__sanderlingActionGenerator: true,
generate: generator,
}),
weighted: (..._entries: WeightedEntry[]): ActionGenerator => ({
__uatuActionGenerator: true,
__sanderlingActionGenerator: true,
generate: () => [],
}),
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 }),
pressKey: ({ key }) => ({ kind: "PressKey", key }),
wait: ({ durationMillis }) => ({ kind: "Wait", durationMillis }),
taps: { __uatuActionGenerator: true, generate: () => [] } as ActionGenerator,
swipes: { __uatuActionGenerator: true, generate: () => [] } as ActionGenerator,
waitOnce: { __uatuActionGenerator: true, generate: () => [] } as ActionGenerator,
pressKeys: { __uatuActionGenerator: true, generate: () => [] } as ActionGenerator,
} satisfies UatuRuntime;
taps: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
swipes: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
waitOnce: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
pressKeys: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
} satisfies SanderlingRuntime;
const state = { currentState: initialState };
const recorded = Object.assign(runtime, {
@@ -77,7 +77,7 @@ function installRuntime(initialState: State): RecordedRuntime {
return lastPredicate;
},
}) as unknown as RecordedRuntime;
globalThis.__uatu__ = recorded;
globalThis.__sanderling__ = recorded;
// Re-bind state ref so subsequent extract() calls read the up-to-date state.
Object.defineProperty(recorded, "currentState", {
get() {
@@ -105,6 +105,6 @@ test("defaults bundle exports formulas tagged as LTL properties", async () => {
logs: [{ unixMillis: 1, level: "W", tag: "X", message: "warn" }],
});
const defaults = await import("../src/defaults/properties.ts");
assert.equal(defaults.noUncaughtExceptions.__uatuFormula, true);
assert.equal(defaults.noLogcatErrors.__uatuFormula, true);
assert.equal(defaults.noUncaughtExceptions.__sanderlingFormula, 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 = "" +
"\n" +
"\x15driverpb/driver.proto\x12\x0euatu.driver.v1\"\a\n" +
"\x15driverpb/driver.proto\x12\x14sanderling.driver.v1\"\a\n" +
"\x05Empty\"z\n" +
"\rLaunchRequest\x12\x1b\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" +
"\x05ready\x18\x01 \x01(\bR\x05ready\x12\x18\n" +
"\aversion\x18\x02 \x01(\tR\aversion\x12\x1a\n" +
"\bplatform\x18\x03 \x01(\tR\bplatform\"\x89\x01\n" +
"\fSwipeRequest\x12)\n" +
"\x04from\x18\x01 \x01(\v2\x15.uatu.driver.v1.PointR\x04from\x12%\n" +
"\x02to\x18\x02 \x01(\v2\x15.uatu.driver.v1.PointR\x02to\x12'\n" +
"\bplatform\x18\x03 \x01(\tR\bplatform\"\x95\x01\n" +
"\fSwipeRequest\x12/\n" +
"\x04from\x18\x01 \x01(\v2\x1b.sanderling.driver.v1.PointR\x04from\x12+\n" +
"\x02to\x18\x02 \x01(\v2\x1b.sanderling.driver.v1.PointR\x02to\x12'\n" +
"\x0fduration_millis\x18\x03 \x01(\x03R\x0edurationMillis\"#\n" +
"\x0fPressKeyRequest\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\"e\n" +
"\x11RecentLogsRequest\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" +
"LogEntries\x122\n" +
"\aentries\x18\x01 \x03(\v2\x18.uatu.driver.v1.LogEntryR\aentries\"m\n" +
"LogEntries\x128\n" +
"\aentries\x18\x01 \x03(\v2\x1e.sanderling.driver.v1.LogEntryR\aentries\"m\n" +
"\bLogEntry\x12\x1f\n" +
"\vunix_millis\x18\x01 \x01(\x03R\n" +
"unixMillis\x12\x14\n" +
@@ -900,24 +900,24 @@ const file_driverpb_driver_proto_rawDesc = "" +
"cpuPercent\x12\x1d\n" +
"\n" +
"heap_bytes\x18\x02 \x01(\x03R\theapBytes\x12,\n" +
"\x12total_memory_bytes\x18\x03 \x01(\x03R\x10totalMemoryBytes2\xcb\x06\n" +
"\x06Driver\x12>\n" +
"\x06Launch\x12\x1d.uatu.driver.v1.LaunchRequest\x1a\x15.uatu.driver.v1.Empty\x129\n" +
"\tTerminate\x12\x15.uatu.driver.v1.Empty\x1a\x15.uatu.driver.v1.Empty\x123\n" +
"\x03Tap\x12\x15.uatu.driver.v1.Point\x1a\x15.uatu.driver.v1.Empty\x12>\n" +
"\vTapSelector\x12\x18.uatu.driver.v1.Selector\x1a\x15.uatu.driver.v1.Empty\x128\n" +
"\tInputText\x12\x14.uatu.driver.v1.Text\x1a\x15.uatu.driver.v1.Empty\x12<\n" +
"\x05Swipe\x12\x1c.uatu.driver.v1.SwipeRequest\x1a\x15.uatu.driver.v1.Empty\x12B\n" +
"\bPressKey\x12\x1f.uatu.driver.v1.PressKeyRequest\x1a\x15.uatu.driver.v1.Empty\x12:\n" +
"\x12total_memory_bytes\x18\x03 \x01(\x03R\x10totalMemoryBytes2\xe7\a\n" +
"\x06Driver\x12J\n" +
"\x06Launch\x12#.sanderling.driver.v1.LaunchRequest\x1a\x1b.sanderling.driver.v1.Empty\x12E\n" +
"\tTerminate\x12\x1b.sanderling.driver.v1.Empty\x1a\x1b.sanderling.driver.v1.Empty\x12?\n" +
"\x03Tap\x12\x1b.sanderling.driver.v1.Point\x1a\x1b.sanderling.driver.v1.Empty\x12J\n" +
"\vTapSelector\x12\x1e.sanderling.driver.v1.Selector\x1a\x1b.sanderling.driver.v1.Empty\x12D\n" +
"\tInputText\x12\x1a.sanderling.driver.v1.Text\x1a\x1b.sanderling.driver.v1.Empty\x12H\n" +
"\x05Swipe\x12\".sanderling.driver.v1.SwipeRequest\x1a\x1b.sanderling.driver.v1.Empty\x12N\n" +
"\bPressKey\x12%.sanderling.driver.v1.PressKeyRequest\x1a\x1b.sanderling.driver.v1.Empty\x12F\n" +
"\n" +
"Screenshot\x12\x15.uatu.driver.v1.Empty\x1a\x15.uatu.driver.v1.Image\x12A\n" +
"\tHierarchy\x12\x15.uatu.driver.v1.Empty\x1a\x1d.uatu.driver.v1.HierarchyJSON\x12K\n" +
"Screenshot\x12\x1b.sanderling.driver.v1.Empty\x1a\x1b.sanderling.driver.v1.Image\x12M\n" +
"\tHierarchy\x12\x1b.sanderling.driver.v1.Empty\x1a#.sanderling.driver.v1.HierarchyJSON\x12W\n" +
"\n" +
"RecentLogs\x12!.uatu.driver.v1.RecentLogsRequest\x1a\x1a.uatu.driver.v1.LogEntries\x12>\n" +
"\vWaitForIdle\x12\x18.uatu.driver.v1.Duration\x1a\x15.uatu.driver.v1.Empty\x12=\n" +
"\x06Health\x12\x15.uatu.driver.v1.Empty\x1a\x1c.uatu.driver.v1.HealthStatus\x12J\n" +
"\aMetrics\x12\x1e.uatu.driver.v1.MetricsRequest\x1a\x1f.uatu.driver.v1.MetricsResponseBM\n" +
"\x12dev.uatu.driver.v1P\x01Z5github.com/priyanshujain/uatu/proto/driverpb;driverpbb\x06proto3"
"RecentLogs\x12'.sanderling.driver.v1.RecentLogsRequest\x1a .sanderling.driver.v1.LogEntries\x12J\n" +
"\vWaitForIdle\x12\x1e.sanderling.driver.v1.Duration\x1a\x1b.sanderling.driver.v1.Empty\x12I\n" +
"\x06Health\x12\x1b.sanderling.driver.v1.Empty\x1a\".sanderling.driver.v1.HealthStatus\x12V\n" +
"\aMetrics\x12$.sanderling.driver.v1.MetricsRequest\x1a%.sanderling.driver.v1.MetricsResponseBY\n" +
"\x18dev.sanderling.driver.v1P\x01Z;github.com/priyanshujain/sanderling/proto/driverpb;driverpbb\x06proto3"
var (
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_goTypes = []any{
(*Empty)(nil), // 0: uatu.driver.v1.Empty
(*LaunchRequest)(nil), // 1: uatu.driver.v1.LaunchRequest
(*Point)(nil), // 2: uatu.driver.v1.Point
(*Text)(nil), // 3: uatu.driver.v1.Text
(*Selector)(nil), // 4: uatu.driver.v1.Selector
(*Image)(nil), // 5: uatu.driver.v1.Image
(*HierarchyJSON)(nil), // 6: uatu.driver.v1.HierarchyJSON
(*Duration)(nil), // 7: uatu.driver.v1.Duration
(*HealthStatus)(nil), // 8: uatu.driver.v1.HealthStatus
(*SwipeRequest)(nil), // 9: uatu.driver.v1.SwipeRequest
(*PressKeyRequest)(nil), // 10: uatu.driver.v1.PressKeyRequest
(*RecentLogsRequest)(nil), // 11: uatu.driver.v1.RecentLogsRequest
(*LogEntries)(nil), // 12: uatu.driver.v1.LogEntries
(*LogEntry)(nil), // 13: uatu.driver.v1.LogEntry
(*MetricsRequest)(nil), // 14: uatu.driver.v1.MetricsRequest
(*MetricsResponse)(nil), // 15: uatu.driver.v1.MetricsResponse
(*Empty)(nil), // 0: sanderling.driver.v1.Empty
(*LaunchRequest)(nil), // 1: sanderling.driver.v1.LaunchRequest
(*Point)(nil), // 2: sanderling.driver.v1.Point
(*Text)(nil), // 3: sanderling.driver.v1.Text
(*Selector)(nil), // 4: sanderling.driver.v1.Selector
(*Image)(nil), // 5: sanderling.driver.v1.Image
(*HierarchyJSON)(nil), // 6: sanderling.driver.v1.HierarchyJSON
(*Duration)(nil), // 7: sanderling.driver.v1.Duration
(*HealthStatus)(nil), // 8: sanderling.driver.v1.HealthStatus
(*SwipeRequest)(nil), // 9: sanderling.driver.v1.SwipeRequest
(*PressKeyRequest)(nil), // 10: sanderling.driver.v1.PressKeyRequest
(*RecentLogsRequest)(nil), // 11: sanderling.driver.v1.RecentLogsRequest
(*LogEntries)(nil), // 12: sanderling.driver.v1.LogEntries
(*LogEntry)(nil), // 13: sanderling.driver.v1.LogEntry
(*MetricsRequest)(nil), // 14: sanderling.driver.v1.MetricsRequest
(*MetricsResponse)(nil), // 15: sanderling.driver.v1.MetricsResponse
}
var file_driverpb_driver_proto_depIdxs = []int32{
2, // 0: uatu.driver.v1.SwipeRequest.from:type_name -> uatu.driver.v1.Point
2, // 1: uatu.driver.v1.SwipeRequest.to:type_name -> uatu.driver.v1.Point
13, // 2: uatu.driver.v1.LogEntries.entries:type_name -> uatu.driver.v1.LogEntry
1, // 3: uatu.driver.v1.Driver.Launch:input_type -> uatu.driver.v1.LaunchRequest
0, // 4: uatu.driver.v1.Driver.Terminate:input_type -> uatu.driver.v1.Empty
2, // 5: uatu.driver.v1.Driver.Tap:input_type -> uatu.driver.v1.Point
4, // 6: uatu.driver.v1.Driver.TapSelector:input_type -> uatu.driver.v1.Selector
3, // 7: uatu.driver.v1.Driver.InputText:input_type -> uatu.driver.v1.Text
9, // 8: uatu.driver.v1.Driver.Swipe:input_type -> uatu.driver.v1.SwipeRequest
10, // 9: uatu.driver.v1.Driver.PressKey:input_type -> uatu.driver.v1.PressKeyRequest
0, // 10: uatu.driver.v1.Driver.Screenshot:input_type -> uatu.driver.v1.Empty
0, // 11: uatu.driver.v1.Driver.Hierarchy:input_type -> uatu.driver.v1.Empty
11, // 12: uatu.driver.v1.Driver.RecentLogs:input_type -> uatu.driver.v1.RecentLogsRequest
7, // 13: uatu.driver.v1.Driver.WaitForIdle:input_type -> uatu.driver.v1.Duration
0, // 14: uatu.driver.v1.Driver.Health:input_type -> uatu.driver.v1.Empty
14, // 15: uatu.driver.v1.Driver.Metrics:input_type -> uatu.driver.v1.MetricsRequest
0, // 16: uatu.driver.v1.Driver.Launch:output_type -> uatu.driver.v1.Empty
0, // 17: uatu.driver.v1.Driver.Terminate:output_type -> uatu.driver.v1.Empty
0, // 18: uatu.driver.v1.Driver.Tap:output_type -> uatu.driver.v1.Empty
0, // 19: uatu.driver.v1.Driver.TapSelector:output_type -> uatu.driver.v1.Empty
0, // 20: uatu.driver.v1.Driver.InputText:output_type -> uatu.driver.v1.Empty
0, // 21: uatu.driver.v1.Driver.Swipe:output_type -> uatu.driver.v1.Empty
0, // 22: uatu.driver.v1.Driver.PressKey:output_type -> uatu.driver.v1.Empty
5, // 23: uatu.driver.v1.Driver.Screenshot:output_type -> uatu.driver.v1.Image
6, // 24: uatu.driver.v1.Driver.Hierarchy:output_type -> uatu.driver.v1.HierarchyJSON
12, // 25: uatu.driver.v1.Driver.RecentLogs:output_type -> uatu.driver.v1.LogEntries
0, // 26: uatu.driver.v1.Driver.WaitForIdle:output_type -> uatu.driver.v1.Empty
8, // 27: uatu.driver.v1.Driver.Health:output_type -> uatu.driver.v1.HealthStatus
15, // 28: uatu.driver.v1.Driver.Metrics:output_type -> uatu.driver.v1.MetricsResponse
2, // 0: sanderling.driver.v1.SwipeRequest.from:type_name -> sanderling.driver.v1.Point
2, // 1: sanderling.driver.v1.SwipeRequest.to:type_name -> sanderling.driver.v1.Point
13, // 2: sanderling.driver.v1.LogEntries.entries:type_name -> sanderling.driver.v1.LogEntry
1, // 3: sanderling.driver.v1.Driver.Launch:input_type -> sanderling.driver.v1.LaunchRequest
0, // 4: sanderling.driver.v1.Driver.Terminate:input_type -> sanderling.driver.v1.Empty
2, // 5: sanderling.driver.v1.Driver.Tap:input_type -> sanderling.driver.v1.Point
4, // 6: sanderling.driver.v1.Driver.TapSelector:input_type -> sanderling.driver.v1.Selector
3, // 7: sanderling.driver.v1.Driver.InputText:input_type -> sanderling.driver.v1.Text
9, // 8: sanderling.driver.v1.Driver.Swipe:input_type -> sanderling.driver.v1.SwipeRequest
10, // 9: sanderling.driver.v1.Driver.PressKey:input_type -> sanderling.driver.v1.PressKeyRequest
0, // 10: sanderling.driver.v1.Driver.Screenshot:input_type -> sanderling.driver.v1.Empty
0, // 11: sanderling.driver.v1.Driver.Hierarchy:input_type -> sanderling.driver.v1.Empty
11, // 12: sanderling.driver.v1.Driver.RecentLogs:input_type -> sanderling.driver.v1.RecentLogsRequest
7, // 13: sanderling.driver.v1.Driver.WaitForIdle:input_type -> sanderling.driver.v1.Duration
0, // 14: sanderling.driver.v1.Driver.Health:input_type -> sanderling.driver.v1.Empty
14, // 15: sanderling.driver.v1.Driver.Metrics:input_type -> sanderling.driver.v1.MetricsRequest
0, // 16: sanderling.driver.v1.Driver.Launch:output_type -> sanderling.driver.v1.Empty
0, // 17: sanderling.driver.v1.Driver.Terminate:output_type -> sanderling.driver.v1.Empty
0, // 18: sanderling.driver.v1.Driver.Tap:output_type -> sanderling.driver.v1.Empty
0, // 19: sanderling.driver.v1.Driver.TapSelector:output_type -> sanderling.driver.v1.Empty
0, // 20: sanderling.driver.v1.Driver.InputText:output_type -> sanderling.driver.v1.Empty
0, // 21: sanderling.driver.v1.Driver.Swipe:output_type -> sanderling.driver.v1.Empty
0, // 22: sanderling.driver.v1.Driver.PressKey:output_type -> sanderling.driver.v1.Empty
5, // 23: sanderling.driver.v1.Driver.Screenshot:output_type -> sanderling.driver.v1.Image
6, // 24: sanderling.driver.v1.Driver.Hierarchy:output_type -> sanderling.driver.v1.HierarchyJSON
12, // 25: sanderling.driver.v1.Driver.RecentLogs:output_type -> sanderling.driver.v1.LogEntries
0, // 26: sanderling.driver.v1.Driver.WaitForIdle:output_type -> sanderling.driver.v1.Empty
8, // 27: sanderling.driver.v1.Driver.Health:output_type -> sanderling.driver.v1.HealthStatus
15, // 28: sanderling.driver.v1.Driver.Metrics:output_type -> sanderling.driver.v1.MetricsResponse
16, // [16:29] is the sub-list for method output_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
View File
@@ -1,9 +1,9 @@
syntax = "proto3";
package uatu.driver.v1;
package sanderling.driver.v1;
option go_package = "github.com/priyanshujain/uatu/proto/driverpb;driverpb";
option java_package = "dev.uatu.driver.v1";
option go_package = "github.com/priyanshujain/sanderling/proto/driverpb;driverpb";
option java_package = "dev.sanderling.driver.v1";
option java_multiple_files = true;
service Driver {
+14 -14
View File
@@ -19,19 +19,19 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
Driver_Launch_FullMethodName = "/uatu.driver.v1.Driver/Launch"
Driver_Terminate_FullMethodName = "/uatu.driver.v1.Driver/Terminate"
Driver_Tap_FullMethodName = "/uatu.driver.v1.Driver/Tap"
Driver_TapSelector_FullMethodName = "/uatu.driver.v1.Driver/TapSelector"
Driver_InputText_FullMethodName = "/uatu.driver.v1.Driver/InputText"
Driver_Swipe_FullMethodName = "/uatu.driver.v1.Driver/Swipe"
Driver_PressKey_FullMethodName = "/uatu.driver.v1.Driver/PressKey"
Driver_Screenshot_FullMethodName = "/uatu.driver.v1.Driver/Screenshot"
Driver_Hierarchy_FullMethodName = "/uatu.driver.v1.Driver/Hierarchy"
Driver_RecentLogs_FullMethodName = "/uatu.driver.v1.Driver/RecentLogs"
Driver_WaitForIdle_FullMethodName = "/uatu.driver.v1.Driver/WaitForIdle"
Driver_Health_FullMethodName = "/uatu.driver.v1.Driver/Health"
Driver_Metrics_FullMethodName = "/uatu.driver.v1.Driver/Metrics"
Driver_Launch_FullMethodName = "/sanderling.driver.v1.Driver/Launch"
Driver_Terminate_FullMethodName = "/sanderling.driver.v1.Driver/Terminate"
Driver_Tap_FullMethodName = "/sanderling.driver.v1.Driver/Tap"
Driver_TapSelector_FullMethodName = "/sanderling.driver.v1.Driver/TapSelector"
Driver_InputText_FullMethodName = "/sanderling.driver.v1.Driver/InputText"
Driver_Swipe_FullMethodName = "/sanderling.driver.v1.Driver/Swipe"
Driver_PressKey_FullMethodName = "/sanderling.driver.v1.Driver/PressKey"
Driver_Screenshot_FullMethodName = "/sanderling.driver.v1.Driver/Screenshot"
Driver_Hierarchy_FullMethodName = "/sanderling.driver.v1.Driver/Hierarchy"
Driver_RecentLogs_FullMethodName = "/sanderling.driver.v1.Driver/RecentLogs"
Driver_WaitForIdle_FullMethodName = "/sanderling.driver.v1.Driver/WaitForIdle"
Driver_Health_FullMethodName = "/sanderling.driver.v1.Driver/Health"
Driver_Metrics_FullMethodName = "/sanderling.driver.v1.Driver/Metrics"
)
// 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,
// and not to be introspected or modified (even as a copy)
var Driver_ServiceDesc = grpc.ServiceDesc{
ServiceName: "uatu.driver.v1.Driver",
ServiceName: "sanderling.driver.v1.Driver",
HandlerType: (*DriverServer)(nil),
Methods: []grpc.MethodDesc{
{
+1 -1
View File
@@ -8,7 +8,7 @@ import (
func TestDriverServiceDescriptor(t *testing.T) {
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)
}
+9 -9
View File
@@ -10,11 +10,11 @@ plugins {
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"
android {
namespace = "dev.uatu.sdk"
namespace = "dev.sanderling.sdk"
compileSdk = 35
defaultConfig {
@@ -61,12 +61,12 @@ mavenPublishing {
)
pom {
name.set("uatu sdk-android")
name.set("sanderling sdk-android")
description.set(
"Android runtime SDK for uatu, a property-based UI fuzzer for mobile apps. " +
"Exposes a content-provider accessibility bridge consumed by the uatu CLI at test time.",
"Android runtime SDK for sanderling, a property-based UI fuzzer for mobile apps. " +
"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 {
license {
@@ -85,9 +85,9 @@ mavenPublishing {
}
scm {
url.set("https://github.com/priyanshujain/uatu")
connection.set("scm:git:git://github.com/priyanshujain/uatu.git")
developerConnection.set("scm:git:ssh://[email protected]/priyanshujain/uatu.git")
url.set("https://github.com/priyanshujain/sanderling")
connection.set("scm:git:git://github.com/priyanshujain/sanderling.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.
@@ -1,4 +1,4 @@
package dev.uatu.sdk
package dev.sanderling.sdk
import android.os.Handler
import android.os.Looper
@@ -1,4 +1,4 @@
package dev.uatu.sdk
package dev.sanderling.sdk
import java.io.PrintWriter
import java.io.StringWriter
@@ -1,4 +1,4 @@
package dev.uatu.sdk
package dev.sanderling.sdk
import android.net.LocalSocket
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.Semaphore
@@ -1,4 +1,4 @@
package dev.uatu.sdk
package dev.sanderling.sdk
import java.io.DataInputStream
import java.io.DataOutputStream
@@ -1,24 +1,24 @@
package dev.uatu.sdk
package dev.sanderling.sdk
import android.app.Application
import android.util.Log
data class Configuration(
val socketName: String = "uatu-agent",
val socketName: String = "sanderling-agent",
val pauseTimeoutMillis: Long = 5_000L,
)
object Uatu {
object Sanderling {
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
@Synchronized
fun start(application: Application, configuration: Configuration = Configuration()) {
if (runtime != null) return
val newRuntime = UatuRuntime(
val newRuntime = SanderlingRuntime(
transport = LocalAbstractTransport(configuration.socketName),
pauser = Pauser(ChoreographerPoster(), configuration.pauseTimeoutMillis),
version = VERSION,
@@ -32,7 +32,7 @@ object Uatu {
fun extract(name: String, function: () -> Any?) {
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)
}
@@ -1,8 +1,8 @@
package dev.uatu.sdk
package dev.sanderling.sdk
import android.util.Log
internal class UatuRuntime(
internal class SanderlingRuntime(
transport: AgentTransport,
private val pauser: Pauser,
private val version: String,
@@ -45,7 +45,7 @@ internal class UatuRuntime(
private inner class AgentHandler : SocketClient.Handler {
override fun onConnected(sender: SocketClient.MessageSender) {
this@UatuRuntime.sender = sender
this@SanderlingRuntime.sender = sender
try {
sender.send(Message.hello(version, platform, appPackage))
} catch (cause: Exception) {
@@ -92,6 +92,6 @@ internal class UatuRuntime(
}
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.InputStream
@@ -30,7 +30,7 @@ class SocketClient(
private val transport: AgentTransport,
private val handler: Handler,
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) },
) {
interface Handler {
@@ -1,4 +1,4 @@
package dev.uatu.sdk
package dev.sanderling.sdk
import org.junit.Assert.assertEquals
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.Executors
@@ -1,4 +1,4 @@
package dev.uatu.sdk
package dev.sanderling.sdk
import java.io.ByteArrayInputStream
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.CountDownLatch
@@ -12,22 +12,22 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
class UatuRuntimeTest {
class SanderlingRuntimeTest {
private lateinit var transport: SocketClientTest.FakeTransport
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()
frameThread = PauserTest.FakeFrameThread()
val pauser = Pauser(frameThread, pauseTimeoutMillis = 2_000L)
return UatuRuntime(
return SanderlingRuntime(
transport = transport,
pauser = pauser,
version = "0.0.1",
platform = "android",
appPackage = "com.example.uatu_test",
appPackage = "com.example.sanderling_test",
).also { runtime = it }
}
@@ -43,7 +43,7 @@ class UatuRuntimeTest {
assertEquals(MessageType.HELLO, hello.type)
assertEquals("0.0.1", hello.version)
assertEquals("android", hello.platform)
assertEquals("com.example.uatu_test", hello.appPackage)
assertEquals("com.example.sanderling_test", hello.appPackage)
}
@Test fun pauseTriggersExtractorsAndReturnsState() {
@@ -1,4 +1,4 @@
package dev.uatu.sdk
package dev.sanderling.sdk
import java.io.IOException
import java.io.PipedInputStream
+1 -1
View File
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
}
}
rootProject.name = "uatu"
rootProject.name = "sanderling"
include(":sidecar")
include(":sdk-android")
+2 -2
View File
@@ -7,7 +7,7 @@ plugins {
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 {
toolchain {
@@ -70,7 +70,7 @@ sourceSets {
}
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/
@@ -1,4 +1,4 @@
package dev.uatu.sidecar
package dev.sanderling.sidecar
interface DriverBackend {
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 dev.uatu.driver.v1.DriverGrpc
import dev.uatu.driver.v1.Duration
import dev.uatu.driver.v1.Empty
import dev.uatu.driver.v1.HealthStatus
import dev.uatu.driver.v1.HierarchyJSON
import dev.uatu.driver.v1.Image
import dev.uatu.driver.v1.LaunchRequest
import dev.uatu.driver.v1.LogEntries
import dev.uatu.driver.v1.LogEntry
import dev.uatu.driver.v1.MetricsRequest
import dev.uatu.driver.v1.MetricsResponse
import dev.uatu.driver.v1.Point
import dev.uatu.driver.v1.PressKeyRequest
import dev.uatu.driver.v1.RecentLogsRequest
import dev.uatu.driver.v1.Selector
import dev.uatu.driver.v1.SwipeRequest
import dev.uatu.driver.v1.Text
import dev.sanderling.driver.v1.DriverGrpc
import dev.sanderling.driver.v1.Duration
import dev.sanderling.driver.v1.Empty
import dev.sanderling.driver.v1.HealthStatus
import dev.sanderling.driver.v1.HierarchyJSON
import dev.sanderling.driver.v1.Image
import dev.sanderling.driver.v1.LaunchRequest
import dev.sanderling.driver.v1.LogEntries
import dev.sanderling.driver.v1.LogEntry
import dev.sanderling.driver.v1.MetricsRequest
import dev.sanderling.driver.v1.MetricsResponse
import dev.sanderling.driver.v1.Point
import dev.sanderling.driver.v1.PressKeyRequest
import dev.sanderling.driver.v1.RecentLogsRequest
import dev.sanderling.driver.v1.Selector
import dev.sanderling.driver.v1.SwipeRequest
import dev.sanderling.driver.v1.Text
import io.grpc.stub.StreamObserver
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