Osy#betaRun it on Osyrin: osyrin.com

Reference / Testing

Dev seeds: a fixture you can load into the running app

[TestFixture, DevSeed("A tower of bells")] void ATowerOfBells() { … } osy launch --dev · osy seed ATowerOfBells

Mark a [TestFixture] with [DevSeed("label")] and the local dev page (`osy launch --dev`) offers it as a button that loads that fixture's world into the running app — the same world its tests fork from. The page lists the accounts the app holds, with their roles, and signs you in as any of them. `osy seed <name>` is the same load from a terminal.

preview2 examples compiled by CItestinglocal

Summary#

A dev seed is a [TestFixture] you also mark [DevSeed("label")]. You write the world once: its tests fork from it, and the local dev page loads it into the app you are clicking through.

[Role] enum TowerRole { Authenticator, Ringer, Captain }

[Principal]
entity Ringer {
  [Required, Unique, MaxLength(200)] string Email;
  [MaxLength(200)] string? PasswordHash;
  security {
    allow read when IsAuthenticated || IsAuthenticator;
    allow create when IsAuthenticator;
    deny read PasswordHash when !IsAuthenticator;
  }
}

[RoleGrant]
entity RoleGrant {
  [Required] Ringer Member;
  [Required] TowerRole Role;
  security { allow read when IsAuthenticated; }
}

policy IsAuthenticator => RoleGrant.Any(g => g.Member == user && g.Role == TowerRole.Authenticator);
policy IsCaptain       => RoleGrant.Any(g => g.Member == user && g.Role == TowerRole.Captain);

entity Bell {
  [Required, MaxLength(40)] string Name;
  security { allow read when IsAuthenticated; allow create, update when IsCaptain; }
}
[TestFixture, DevSeed("A tower of bells")]
void ATowerOfBells() {
  var captain = new Ringer { Email = "[email protected]", PasswordHash = Security.HashPassword("a-long-captain-password") };
  new RoleGrant { Member = captain, Role = TowerRole.Captain };
  new Ringer { Email = "[email protected]", PasswordHash = Security.HashPassword("a-long-ringer-password") };
  new Bell { Name = "Treble" };
  new Bell { Name = "Tenor" };
}

principal Captain => Ringer.Single(r => r.Email == "[email protected]");

// A test body runs as its `[runas]` principal — with none it is anonymous, and a Bell is readable only when signed in.
[Test(ATowerOfBells)]
[runas(Captain)]
void The_tower_has_two_bells() {
  Assert.Equal(2, Bell.Count());
}

Signature#

[TestFixture, DevSeed("<label>")]          // offered on the dev page under <label>
void <Name>() { … }

[TestFixture(<Parent>), DevSeed]            // builds on <Parent>; with no label, the name in words
void <Layer>() { … }
osy launch --dev           open the app's dev page
osy seed                   list the seeds the tests declare
osy seed <name|label>      load one into the running app
osy seed --json            the same answers as one JSON line, for a tool: the seeds, the one loaded last, the accounts and their roles

Description#

What does loading a seed do?#

It replaces the app's data with the seed's world. The fixture is built exactly as osy test builds it — from an empty app, past every security { } block, with runas, stubs and its parent fixtures — and the rows it leaves (and the files it wrote) become the running app's data. What was there before is removed first, so loading a seed twice gives you the same world twice, and Reset on the page empties the app and loads the last seed again.

The platform's own Admin is the one exception. Its data is the platform's registry — the installation, the operator, the system apps — so a seed loaded in admin/ goes in beside those rows instead of replacing them, and the platform keeps running. Loading again replaces only the previous seed, and Reset removes only the seed's rows (and any row that points at one). osy seed says how many of the platform's own rows it kept.

A row whose field a workflow tracks arrives at the state the seed left it in, with a run of the app's own — an order the seed left waiting for payment is waiting for payment, and nothing the seed's workflows already did runs again. A field a Counter numbers arrives with the number the seed gave it, and the counter carries on from the seed's last number — so the next order the app makes after loading a seed with 28 orders is the 29th, however many times you load it.

What if a seed does not load?#

The message says where it left the app, because the two failures leave it in different places:

  • the seed's world could not be built (its fixture threw, an assertion failed) — the app was not touched: "The app's data was not changed."
  • the world was built and could not be moved in — what got in is removed and the app's own data is written back: "What the app held before was put back." Only if putting it back fails too is the app left empty, and the message says so: "The app is empty now."

osy seed <name> --json prints that as one JSON line and exits 1: {"error": "…", "appUnchanged": true}.

What is on the dev page?#

osy launch --dev opens it at http://<app>.localhost:<port>/_osy/dev:

  • Seeds — one button per [DevSeed], with the osy seed line that does the same;
  • Accounts — every account the app holds, with its roles, and Sign in as beside each: you land on the app signed in as that person, with no password asked;
  • Secrets — every secret the app declares, set or not set. A value you type is saved to the project's .secrets (kept out of git) and applied to the running app, exactly as osy secret set does; it is never shown again;
  • Open the app — the app itself.

osy launch (without --dev) opens the app and prints one line saying the dev page is there.

Where does the dev page exist?#

Only on your local platform (osyrin dev). A seed writes past security and "sign in as" asks for no password, so on any deployed platform the page and everything behind it do not exist at all — the address answers 404. On your machine it opens only through osy launch --dev, which holds your instance's own key.

Why a fixture, and not a separate seed file?#

Because the world you click through should be the world your tests check. A seed written apart from the tests drifts from them the first time either changes; a fixture marked [DevSeed] is one method doing both jobs, and a test that forks from it fails the moment the seed stops building what the app needs.

See also#

Related

Fixtures: built once, cloned for every test, and built on each other

A [TestFixture] is built once per run, and every test that names it runs on its own copy of what it left — rows, files…

[Test] / [TestFixture]

A test is an ordinary function marked [Test]. It runs against a throwaway clone of the app, so it may create rows and…

Adding an account

Adds a user to your app on the local platform, with the roles you say it should have. Use it to give yourself an…

role grants (and the first admin)

A role is granted by an ordinary entity you mark `[RoleGrant]` — one that holds a reference to your `[Principal]` and a…

Running tests locally

Runs your app's tests against a Platform on your own machine — no account, no network, no setup beyond a running local…