diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..029a43acf1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# AGENTS.md + +# Tlon Messenger backend +The backend of the Tlon Messenger app is hosted on the Urbit platform. + +All the backend code is located in the desk/ directory, which +is deployed to an urbit ship. + +## Development +Interface with a running urbit ship through a tmux session +running an urbit ship. Do not switch to that session, but interface +with it using tmux input and capture commands. +A typical command to verify connection is working is `%`, which will +display current identity, desk and time. + +## Backend tests + +There are two kinds of backend tests in groups. The first kind uses the +`/lib/test-agent.hoon` library, which provides a monadic framework for +implementing gall agent tests. It works by simulating rudimentary gall +functionality, which allows testing of the agent core (which is a pure function of +agent state) under the variety of circumstances. + +test-agent tests are located in `/tests` directory, under the +corresponding desk entry. For example, tests for `/app/groups.hoon` +agent would be located at `/tests/app/groups.hoon`. + +The second kind of tests uses aqua-based ship virtualization. +Using aqua, a virtual fleet of ships can be run directly on a +ship with little resource cost. While these ships are not +fully-featured and do not support every urbit runtime event, they +nonetheless allow testing of gall agents running on virtualized ship. + +Aqua tests are located in `/tests/ph` directory. + +For details on how to work with aqua tests see documentation in `/docs/aqua` + + diff --git a/backend/run-aqua-tests.sh b/backend/run-aqua-tests.sh index a0136d205c..9712230a9c 100755 --- a/backend/run-aqua-tests.sh +++ b/backend/run-aqua-tests.sh @@ -39,7 +39,7 @@ esac echo $urbit_bin_url -echo "Running backend unit tests" +echo "Running aqua tests" #download_url=`jq -r ".[\"$ship\"][\"downloadUrl\"]" < $ship_manifest` download_url="https://bootstrap.urbit.org/zod-aqua-tests-409k.xst" @@ -209,7 +209,7 @@ then exit 1 fi -echo "Starting %aqua..." +echo "Starting aqua..." ${run_click} $pier "/lib/pill/hoon"<(%ok)) EOF -# Run the unit tests +echo "Preparing aqua snapshot..." +result=$( $run_click -t 1200 $pier <(\`start-args:spider\`[\`tid.bowl \`tid byk.bowl(q %groups) %ph-fleet !>(\`args)]) +;< ~ bind:m (watch-our /awaiting/[tid] %spider /thread-result/[tid]) +;< ~ bind:m (poke-our %spider %spider-start poke-vase) +;< =cage bind:m (take-fact /awaiting/[tid]) +;< ~ bind:m (take-kick /awaiting/[tid]) +=/ thread-result=(each vase [term tang]) + ?+ p.cage ~|([%strange-thread-result p.cage %ph-test tid] !!) + %thread-done [%& q.cage] + %thread-fail [%| !<([term tang] q.cage)] + == +?: ?=(%| -.thread-result) + %- (slog %thread-fail p.thread-result) + (pure:m !>(|)) +(pure:m !>(&)) +EOF +) + +result_code=`echo $result | sed 's/\[0 %avow 0 %noun \(.*\)\]/\1/'` + +if [[ $result_code != "0" ]] +then + echo "Failed to generate aqua snapshot ❌" + kill -TERM $vere_pid + exit 1 +fi + +# Run aqua tests +# +# Update to use the generated test snapshot echo "Running tests..." result=$( $run_click -t 1200 $pier <(\`start-args:spider\`[\`tid.bowl \`tid byk.bowl(q %groups) %ph-test !>(\`ph-tests)]) +=/ args + [\`ph-tests %ci-aqua-tests] +=+ tid=~.ci-ph-test +=/ poke-vase !>(\`start-args:spider\`[\`tid.bowl \`tid byk.bowl(q %groups) %ph-test !>(\`args)]) ;< ~ bind:m (watch-our /awaiting/[tid] %spider /thread-result/[tid]) ;< ~ bind:m (poke-our %spider %spider-start poke-vase) ;< =cage bind:m (take-fact /awaiting/[tid]) diff --git a/desk/app/contacts.hoon b/desk/app/contacts.hoon index 2b9192d626..31298e5b18 100644 --- a/desk/app/contacts.hoon +++ b/desk/app/contacts.hoon @@ -1,3 +1,12 @@ +:: contacts: user profile and contact book +:: +:: the contacts agent manages the user's public profile, as well +:: as track profiles of network peers, creating an implicit social graph. +:: it also manages the contact book, which contains the profiles of +:: network peers marked as contacts, together with user-defined +:: metadata and profile data overlays. +:: +:: /- activity-ver /+ default-agent, dbug, verb, neg=negotiate /+ *contacts, kol diff --git a/desk/app/expose.hoon b/desk/app/expose.hoon index 3c55f6b7e8..870b834ecb 100644 --- a/desk/app/expose.hoon +++ b/desk/app/expose.hoon @@ -12,6 +12,7 @@ /= page /app/expose/page /= widget /app/expose/widget :: +:: |% +$ state-2 $: %2 @@ -104,6 +105,7 @@ ^- (list [term value:co]) :: then look at our state and inject as appropriate :: + ?: =(~ open) ~ :_ ~ :- %expose-cites :- %set diff --git a/desk/lib/ph/test.hoon b/desk/lib/ph/test.hoon index 4635679403..e22b914d2d 100644 --- a/desk/lib/ph/test.hoon +++ b/desk/lib/ph/test.hoon @@ -10,6 +10,7 @@ :: ++ ph-test-shut (leave-our /effect/unto %aqua) +:: +take-effect: receive aqua effect on a .wire :: ++ take-effect |= =wire @@ -19,6 +20,14 @@ ?> ?=(%aqua-effect p.res) =+ !<(=aqua-effect q.res) (pure:m aqua-effect) +:: +poke-app: poke a gall agent on a virtual ship +:: +:: .dock: target virtual ship and agent +:: .page: poke payload +:: +:: note that the poke is a $page, not a $vase. +:: this is because pokes to virtual ships are injected +:: into arvo, and thus need to pass through an untyped interface. :: ++ poke-app |= [=dock =page] @@ -38,6 +47,10 @@ ?^ p.sign (strand-fail %poke-ack u.p.sign) (pure:m ~) +:: +watch-app: watch a gall subscription to a virtual ship +:: +:: the resulting facts are received as aqua effects. +:: see +wait-for-app-fact. :: ++ watch-app |= [=wire =dock =path] @@ -60,6 +73,7 @@ ?^ p.sign (strand-fail %watch-ack u.p.sign) (pure:m ~) +:: +leave-app: leave a gall subscription to a virtual ship :: ++ leave-app |= [=wire =dock] @@ -71,6 +85,7 @@ [%event p.dock [%g wire] task] ;< ~ bind:m (send-events ~[aqua-event]) (pure:m ~) +:: +wait-for-app-fact: receive a gall fact from a virtual ship :: ++ wait-for-app-fact |= [=wire [our=ship dap=term]] @@ -89,6 +104,7 @@ =+ .^(=dais:clay %cb /(scot %p our.bowl)/groups/(scot %da now.bowl)/[mark]) =/ =vase (vale:dais noun.p.q.unix-effect) (pure:m [mark vase]) +:: +ex-equal: expect .actual to be equal to .expected :: ++ ex-equal |= [actual=vase expected=vase] @@ -99,6 +115,7 @@ ?~ tang `[%done ~] `[%fail %ex-equal tang] +:: +ex-not-equal: expect .actual not to be equal to .expected :: ++ ex-not-equal |= [actual=vase expected=vase] diff --git a/desk/lib/verb.hoon b/desk/lib/verb.hoon index 620e9abf8e..1b003de976 100644 --- a/desk/lib/verb.hoon +++ b/desk/lib/verb.hoon @@ -133,7 +133,7 @@ same :: ++ print-logs - |= =cage + |= [=bowl:gall =cage] |* etc=* ?. ?=(%log-action p.cage) etc =+ !<(=a-log:logs q.cage) @@ -152,9 +152,10 @@ =/ =echo:logs ?- -.event %fail - [leaf+"fail {}" trace.event] + [leaf+"[{}/{}] fail {}" trace.event] :: - %tell echo.event + %tell + [leaf+"[{}/{}]" echo.event] == %- %- %*(. slog pri val) echo @@ -182,7 +183,7 @@ :: intercept logging cards and print :: %- ?: &(=(%logs name) =(ship our.bowl)) - (print-logs cage) + (print-logs bowl cage) same [%poke p.card [ship name] p.cage `@`(mug q.q.cage)] :: diff --git a/desk/ted/ph/fleet.hoon b/desk/ted/ph/fleet.hoon new file mode 100644 index 0000000000..661bf5d624 --- /dev/null +++ b/desk/ted/ph/fleet.hoon @@ -0,0 +1,91 @@ +:: prepare aqua fleet snapshot +:: +:: snap-id=(unit @t) +:: fleet=(list ship) +:: sync=? +:: +/- spider, aquarium +/+ *strandio, ph-io, ph-test +=, strand=strand:spider +:: +sync-desk: sync aqua ship desk to host desk +:: +|% +++ sync-desk + |= [her=ship desk=@tas] + =/ m (strand ,~) + ^- form:m + ;< =bowl:strand bind:m get-bowl + =/ sab=path + /(scot %p our.bowl)/[desk]/(scot %da now.bowl) + =| =path + =| raw-files=(list [^path page:clay]) + =. raw-files + |- + =* loop $ + =+ .^(=arch %cy (weld sab path)) + =. raw-files + %+ roll ~(tap in ~(key by dir.arch)) + |= [dir=@ta =_raw-files] + (welp loop(path (snoc path dir)) raw-files) + ?~ fil.arch raw-files + =+ .^(=page:clay %cs (weld sab /blob/(scot %uv u.fil.arch))) + :_ raw-files + [path page] + =/ files + %+ turn raw-files + |= [=^path =page:clay] + =+ .^(=dais:clay %cb (snoc sab p.page)) + =+ .^(=tube:clay %cc (weld sab /[p.page]/mime)) + =+ !<(=mime (tube (vale:dais q.page))) + [path ~ mime] + =/ =beam [[her desk ud+1] /] + ;< ~ bind:m (send-events:ph-io [%event her /c/mount/0v1abc [%mont desk beam]]~) + =/ =task:clay + [%into desk & files] + ;< ~ bind:m (send-events:ph-io [%event her /c/sync/0v1abc task]~) + ;< ~ bind:m (sleep ~s0) + (pure:m ~) +-- +^- thread:spider +|= args=vase +=/ m (strand ,vase) +^- form:m +=+ !<(args=(unit [snap-id=(unit @t) fleet=(list ship) sync=?]) args) +=/ [snap-id=(unit @t) fleet=(list ship) sync=?] + ?~ args ~|(%no-args-found !!) + [snap-id fleet sync]:u.args +;< =bowl:spider bind:m get-bowl +:: remove duplicates +=. fleet ~(tap in (silt fleet)) +~> %slog.1^(crip "Booting fleet {}") +;< vane-tids=(map term tid:spider) bind:m start-simple:ph-io +;< ~ bind:m + =/ n (strand ,~) + |- + ?~ fleet (pure:n ~) + ;< ~ bind:n (init-ship:ph-io i.fleet &) + $(fleet t.fleet) +:: +;< ~ bind:m + =/ n (strand ,~) + ?. sync (pure:n ~) + ~> %slog.1^(crip "Syncing %groups desk to ships...") + |- + ?~ fleet (pure:n ~) + ;< ~ bind:n (sync-desk i.fleet %groups) + $(fleet t.fleet) +:: allow agents time to cool down. it takes about a minute +:: for ames connections to be established. +:: +~> %slog.1^(crip "Cooling down %groups agents...") +;< ~ bind:m (sleep ~m1) +;< ~ bind:m (end-test:ph-io vane-tids) +;< =bowl:spider bind:m get-bowl +=/ snap-id=@t + ?~ snap-id + =+ eny=(end 3^4 (sham eny.bowl)) + (cat 3 'aqua-tests-' (scot %uv eny)) + u.snap-id +~> %slog.1^(crip "Taking snapshot {}...") +;< ~ bind:m (send-events:ph-io [%snap-ships snap-id fleet]~) +(pure:m !>(snap-id)) diff --git a/desk/ted/ph/test-ls.hoon b/desk/ted/ph/test-ls.hoon new file mode 100644 index 0000000000..206dc29b01 --- /dev/null +++ b/desk/ted/ph/test-ls.hoon @@ -0,0 +1,153 @@ +:: list aqua tests +:: +:: arguments: +:: pax=(unit path) optional test path +:: +:: returns & if all tests build successfully, | otherwise. +:: +/- spider, aquarium +/+ *strandio, ph-io, ph-test +=, strand=strand:spider +|% ++$ test [=path strand=test-strand] ++$ test-arm [name=term strand=test-strand] ++$ test-strand _*form:(strand ,~) +-- +:: +|% +++ find-test-files + |= [byk=beak pax=path] + =/ m (strand ,(list path)) + ^- form:m + =/ dir=path + ;: weld + /(scot %p p.byk)/[q.byk]/(scot %da +.r.byk) + pax + == + %- pure:m + %+ skim .^((list ^path) %ct dir) + |=(=path =(%hoon (rear path))) +:: +build-test-files: build supplied test files +:: +++ build-test-files + |= [byk=beak files=(list path)] + =| out=(list (pair path vase)) + =| fail=(list path) + =/ m (strand ,[_out _fail]) + ^- form:m + |- + ?~ files (pure:m [(flop out) (flop fail)]) + =* file i.files + ;< build=(unit vase) bind:m (build-file byk file) + =* filename (spud file) + ?~ build + ~> %slog.3^leaf+"FAILED BUILD {filename}" + $(files t.files, fail [file fail]) + $(files t.files, out [[file u.build] out]) +:: +get-test-arms: get test arms contained in a core +:: +++ get-test-arms + |= [=path [typ=type cor=*]] + ^- (list test-arm) + =/ arms=(list @tas) (sloe typ) + %+ roll (skim arms has-test-prefix) + |= [name=term test-arms=(list test-arm)] + =/ fire-arm=(unit (pair type nock)) + =; res + ?: ?=(%| -.res) + %- %- %*(. slog pri 3) p.res + ~>(%slog.3^leaf+"FAILED MINT {<(snoc (snip path) name)>}" ~) + `p.res + %- mule + ^- (trap (pair type nock)) + |.((~(mint ut typ) -:!>(*test-strand) [%limb name])) + ?~ fire-arm + test-arms + =+ !<(=test-strand [p.u.fire-arm .*(cor q.u.fire-arm)]) + :_ test-arms + [name test-strand] +:: +has-test-prefix: does the arm define a test we should run? +:: +++ has-test-prefix + |= a=term ^- ? + =((end [3 8] a) 'ph-test-') +:: +resolve-test-paths: add test names to paths to form full test identifiers +:: +++ resolve-test-paths + |= paths-to-test=(list [path (list test-arm)]) + ^- (list test) + %- sort :_ |=([a=test b=test] (aor path.a path.b)) + ^- (list test) + %- zing + %+ turn paths-to-test + |= [=path test-arms=(list test-arm)] + ^- (list test) + :: for each test, add the test's name to .path + :: + %+ turn test-arms + |= =test-arm + ^- test + [(weld path /[name.test-arm]) strand.test-arm] +-- +^- thread:spider +|= args=vase +=/ m (strand ,vase) +^- form:m +=+ !<(pax=(unit path) args) +;< =bowl:spider bind:m get-bowl +=/ [byk=beak pax=path] + ?~ pax + [byk.bowl ~] + ?> ?=([ship=@ desk=@ date=@ rest=*] u.pax) + =+ ship=(slav %p i.u.pax) + =+ desk=i.t.u.pax + =+ date=(slav %da i.t.t.u.pax) + :_ t.t.t.u.pax + [ship desk da+date] +=? pax ?=(~ pax) + /tests +:: if the path does not point into a directory/file, assume last component +:: is a pattern. +:: +=/ =arch .^(arch %cy (weld /(scot %p p.byk)/[q.byk]/(scot %da +.r.byk) pax)) +=/ [arm-pat=(unit @ta) pax=path] + ?: ?=(^ dir.arch) + [~ pax] + :_ (snip pax) + `(rear pax) +;< test-files=(list path) bind:m (find-test-files byk pax) +=. test-files + %+ sort test-files + |=([a=path b=path] (aor (spat a) (spat b))) +;< [test-cores=(list (pair path vase)) failed-builds=(list path)] + bind:m (build-test-files byk test-files) +=/ test-sets=(list [path (list test-arm)]) + %+ turn test-cores + ?~ arm-pat + |=([=path =vase] [path (get-test-arms path vase)]) + :: filter based on arm pattern + :: + =+ len=(met 3 u.arm-pat) + |= [=path =vase] + :- path + %+ skim (get-test-arms path vase) + |= =test-arm + =((cut 3 [0 len] name.test-arm) u.arm-pat) +|- +?~ test-sets + ?. ?=(~ failed-builds) + (pure:m !>(|)) + (pure:m !>(&)) +=* path -.i.test-sets +=* test-arms +.i.test-sets +=+ num-arms=(lent test-arms) +?: =(0 num-arms) $(test-sets t.test-sets) +=/ test-num + ?: (gth num-arms 1) + "{} tests" + "{} test" +~> %slog.0^leaf+"{} ({test-num})" +|- +?~ test-arms ^$(test-sets t.test-sets) +~> %slog.0^leaf+" - {}" +$(test-arms t.test-arms) diff --git a/desk/ted/ph/test.hoon b/desk/ted/ph/test.hoon index 3794f74153..6e4dd308a8 100644 --- a/desk/ted/ph/test.hoon +++ b/desk/ted/ph/test.hoon @@ -1,4 +1,9 @@ -:: ph test runner +:: run aqua tests +:: +:: pax=(unit path) optional test path +:: snap=@t aqua fleet snapshot +:: +:: returns & if all tests passed, | otherwise. :: /- spider, aquarium /+ *strandio, ph-io, ph-test @@ -76,8 +81,7 @@ ^- test [(weld path /[name.test-arm]) strand.test-arm] :: +await-test-thread: run and return result of an aqua test thread -::TODO implement timeout, and if it is exceeded kill the thread and -:: report timeout failure. +:: ++ await-test-thread |= [file=path =test-strand] =/ m (strand ,thread-result) @@ -99,57 +103,20 @@ == ;< ~ bind:m (watch-our /awaiting/[tid] %spider /thread-result/[tid]) ;< ~ bind:m (poke-our %spider %spider-inline !>(inline-args)) + ::TODO implement timeout ;< =cage bind:m (take-fact /awaiting/[tid]) ;< ~ bind:m (take-kick /awaiting/[tid]) ?+ p.cage ~|([%strange-thread-result p.cage file tid] !!) %thread-done (pure:m %& q.cage) %thread-fail (pure:m %| !<([term tang] q.cage)) == -:: +sync-desk: sync aqua ship desk to host -:: host ship -> sync desk to virtual ship -:: %groups -> aqua %groups -++ sync-desk - |= [her=ship desk=@tas] - =/ m (strand ,~) - ^- form:m - ;< =bowl:strand bind:m get-bowl - =/ sab=path - /(scot %p our.bowl)/[desk]/(scot %da now.bowl) - =| =path - =| raw-files=(list [^path page:clay]) - =. raw-files - |- - =* loop $ - =+ .^(=arch %cy (weld sab path)) - =. raw-files - %+ roll ~(tap in ~(key by dir.arch)) - |= [dir=@ta =_raw-files] - (welp loop(path (snoc path dir)) raw-files) - ?~ fil.arch raw-files - =+ .^(=page:clay %cs (weld sab /blob/(scot %uv u.fil.arch))) - :_ raw-files - [path page] - =/ files - %+ turn raw-files - |= [=^path =page:clay] - =+ .^(=dais:clay %cb (snoc sab p.page)) - =+ .^(=tube:clay %cc (weld sab /[p.page]/mime)) - =+ !<(=mime (tube (vale:dais q.page))) - [path ~ mime] - =/ =beam [[her desk ud+1] /] - ;< ~ bind:m (send-events:ph-io [%event her /c/mount/0v1abc [%mont desk beam]]~) - =/ =task:clay - [%into desk & files] - ;< ~ bind:m (send-events:ph-io [%event her /c/sync/0v1abc task]~) - ;< ~ bind:m (sleep ~s0) - (pure:m ~) -- -:: ^- thread:spider |= args=vase =/ m (strand ,vase) ^- form:m -=+ !<(pax=(unit path) args) +=+ ~| %no-args-found + (need !<((unit [pax=(unit path) snap=@t]) args)) ;< =bowl:spider bind:m get-bowl =/ [byk=beak pax=path] ?~ pax @@ -162,9 +129,10 @@ [ship desk da+date] =? pax ?=(~ pax) /tests -=/ =arch .^(arch %cy (weld /(scot %p p.byk)/[q.byk]/(scot %da +.r.byk) pax)) :: if the path does not point into a directory, assume last component :: is a pattern. +:: +=/ =arch .^(arch %cy (weld /(scot %p p.byk)/[q.byk]/(scot %da +.r.byk) pax)) =/ [arm-pat=(unit @ta) pax=path] ?: ?=(^ dir.arch) [~ pax] @@ -193,46 +161,8 @@ =+ num=(lent tests) ?: =(num 0) ~> %slog.2^leaf+"No suitable aqua tests found" - (pure:m !>(~)) + (pure:m !>(|)) ~> %slog.1^leaf+"{} test {?:((gth num 1) "threads" "thread")} built" -~> %slog.1^'Booting ships...' -;< vane-tids=(map term tid:spider) bind:m start-simple:ph-io -:: test ships -:: -;< ~ bind:m (init-ship:ph-io ~zod &) -;< ~ bind:m (init-ship:ph-io ~bud &) -;< ~ bind:m (init-ship:ph-io ~nec &) -:: provider ships -:: -:: bait provider -;< ~ bind:m (init-ship:ph-io ~fen &) -:: notify provider -;< ~ bind:m (init-ship:ph-io ~dem &) -:: -~> %slog.1^(crip "Syncing {} desk to ships...") -;< ~ bind:m (sync-desk ~zod %groups) -;< ~ bind:m (sync-desk ~bud %groups) -;< ~ bind:m (sync-desk ~nec %groups) -;< ~ bind:m (sync-desk ~fen %groups) -;< ~ bind:m (sync-desk ~dem %groups) -:: setup bait provider -:: -~> %slog.1^(crip "Setting ~fen as lure provider...") -;< ~ bind:m ph-test-init:ph-test -;< ~ bind:m (poke-app:ph-test [~zod %reel] reel-command+[%set-ship ~fen]) -;< ~ bind:m (poke-app:ph-test [~bud %reel] reel-command+[%set-ship ~fen]) -;< ~ bind:m (poke-app:ph-test [~nec %reel] reel-command+[%set-ship ~fen]) -;< ~ bind:m (poke-app:ph-test [~fen %reel] reel-command+[%set-ship ~fen]) -;< ~ bind:m (poke-app:ph-test [~dem %reel] reel-command+[%set-ship ~fen]) -;< ~ bind:m (sleep ~s0) -;< ~ bind:m ph-test-shut:ph-test -;< ~ bind:m (end:ph-io vane-tids) -:: TODO notify agent does not support swapping a provider -;< =bowl:spider bind:m get-bowl -=+ snap-id=(end 3^4 (sham eny.bowl)) -=+ snap=(cat 3 'aqua-tests-' (scot %uv snap-id)) -~> %slog.1^(crip "Taking snapshot...") -;< ~ bind:m (send-events:ph-io [%snap-ships snap ~[~zod ~bud ~nec ~fen ~dem]]~) ~> %slog.1^'Running tests...' =/ n (strand (list (pair path thread-result))) ;< results=(list (pair path thread-result)) bind:m @@ -256,8 +186,6 @@ [(div diff s) (div (mod diff s) ms)] ~> %slog.1^leaf+"{(trip name)} took {}.{((d-co:co 3) took-ms)}s" $(tests t.tests, results [[path.test thread-result] results]) -::TODO fix aqua to only clear a particular snap -;< ~ bind:m (poke-our %aqua noun+!>([%clear-snap snap])) =+ ok=& |- ?~ results (pure:m !>(ok)) diff --git a/desk/tests/ph/app/contacts.hoon b/desk/tests/ph/app/contacts.hoon new file mode 100644 index 0000000000..7dc9b6a929 --- /dev/null +++ b/desk/tests/ph/app/contacts.hoon @@ -0,0 +1,92 @@ +/- spider, c=contacts +/+ *ph-io, *ph-test +=, strand=strand:spider +|% +++ my-broadcast-profile + ^- contact:c + %- ~(gas by *(map @tas value:c)) + :~ [%nickname text+'Zod Test'] + [%bio text+'A test profile broadcast over aqua'] + [%status text+'testing contacts'] + [%color tint+0xff.0000] + == +++ my-typed-profile + ^- contact:c + %- ~(gas by *(map @tas value:c)) + :~ [%nickname text+'Zod Typed'] + [%bio text+'Testing every contact value type'] + [%status text+'typing contacts'] + [%color tint+0xff.0000] + [%avatar look+'https://example.com/avatar.png'] + [%cover look+'https://example.com/cover.png'] + [%favorite-number numb+42] + [%birthday date+~2026.1.1] + [%friend ship+~bud] + [%home-group flag+~zod^%home-group] + [%favourite-colors set+(sy tint+0xff.0000 tint+0x0 ~)] + == +:: +ph-test-profile-broadcast: test profile updates broadcast to met peers +:: +:: scenario +:: +:: ~bud meets ~zod and subscribes to ~zod's contact profile. +:: ~zod updates his profile with several fields, then ~bud receives a +:: peer update containing the new profile. +:: +++ ph-test-profile-broadcast + =/ m (strand ,~) + ^- form:m + ;< ~ bind:m (watch-app /~bud/contacts/v1/news [~bud %contacts] /v1/news) + :: ~bud meets ~zod and receives ~zod's initial empty profile. + :: + ;< ~ bind:m (poke-app [~bud %contacts] contact-action-1+[%meet ~[~zod]]) + ;< pay=cage bind:m (wait-for-app-fact /~bud/contacts/v1/news [~bud %contacts]) + ?> =(%contact-response-0 p.pay) + =+ !<(res=response:c q.pay) + ?> ?=(%peer -.res) + ;< ~ bind:m (ex-equal !>(who.res) !>(~zod)) + :: ~zod updates his public profile, then ~bud receives the broadcast. + :: + ;< ~ bind:m (poke-app [~zod %contacts] contact-action-1+[%self my-broadcast-profile]) + ;< pay=cage bind:m (wait-for-app-fact /~bud/contacts/v1/news [~bud %contacts]) + ?> =(%contact-response-0 p.pay) + =+ !<(res=response:c q.pay) + ?> ?=(%peer -.res) + ;< ~ bind:m (ex-equal !>(who.res) !>(~zod)) + ;< ~ bind:m (ex-equal !>(con.res) !>(my-broadcast-profile)) + (pure:m ~) +:: +ph-test-profile-field-types: test profile field value types +:: +:: scenario +:: +:: ~zod starts with an empty contact profile. +:: ~zod updates his profile with fields covering every contact value type, +:: then publishes the updated profile to local subscribers. +:: +++ ph-test-profile-field-types + =/ m (strand ,~) + ^- form:m + ;< ~ bind:m (watch-app /~zod/contacts/v1/contact [~zod %contacts] /v1/contact) + ;< pay=cage bind:m (wait-for-app-fact /~zod/contacts/v1/contact [~zod %contacts]) + ?> =(%contact-update-1 p.pay) + =+ !<(upd=update:c q.pay) + ?> ?=(%full -.upd) + ;< ~ bind:m (ex-equal !>(con.upd) !>(*contact:c)) + ;< ~ bind:m (watch-app /~zod/contacts/v1/news [~zod %contacts] /v1/news) + :: ~zod sets every contact value type and receives the local response. + :: + ;< ~ bind:m (poke-app [~zod %contacts] contact-action-1+[%self my-typed-profile]) + ;< pay=cage bind:m (wait-for-app-fact /~zod/contacts/v1/news [~zod %contacts]) + ?> =(%contact-response-0 p.pay) + =+ !<(res=response:c q.pay) + ?> ?=(%self -.res) + ;< ~ bind:m (ex-equal !>(con.res) !>(my-typed-profile)) + :: ~zod then publishes his full updated profile to contact subscribers. + :: + ;< pay=cage bind:m (wait-for-app-fact /~zod/contacts/v1/contact [~zod %contacts]) + ?> =(%contact-update-1 p.pay) + =+ !<(upd=update:c q.pay) + ?> ?=(%full -.upd) + ;< ~ bind:m (ex-equal !>(con.upd) !>(my-typed-profile)) + (pure:m ~) +-- diff --git a/desk/tests/ph/app/groups.hoon b/desk/tests/ph/app/groups.hoon index 7a2113060c..a49203c431 100644 --- a/desk/tests/ph/app/groups.hoon +++ b/desk/tests/ph/app/groups.hoon @@ -31,14 +31,13 @@ :: :: scenario :: -:: ~zod hosts a group. ~bud joins the group. we verify -:: that the subscription lifecycle follows through %watch, and then %done. -:: finally, the group creation response is received. +:: ~zod hosts a group and sends an invitation to ~bud. +:: ~bud receives the invitation and joins the group successfully, +:: receiving the group creation fact. :: ++ ph-test-group-join =/ m (strand ,~) ^- form:m - ;< ~ bind:m (watch-app /~zod/groups/v1/groups [~zod %groups] /v1/groups) ;< ~ bind:m (watch-app /~bud/groups/v1/groups [~bud %groups] /v1/groups) ;< ~ bind:m (watch-app /~bud/groups/v1/foreigns [~bud %groups] /v1/foreigns) :: ~zod hosts a group and invites ~bud @@ -62,7 +61,5 @@ [%foreign my-test-flag %join token.i.invites.foreign] ;< ~ bind:m (poke-app [~bud %groups] group-foreign-2+a-foreigns) ;< ~ bind:m (ex-r-groups-fact ~bud ~zod^%my-test-group %create) - ;< =bowl:strand bind:m get-bowl (pure:m ~) -- - diff --git a/desk/tests/ph/for/lure.hoon b/desk/tests/ph/platform/lure.hoon similarity index 94% rename from desk/tests/ph/for/lure.hoon rename to desk/tests/ph/platform/lure.hoon index 3b091629fb..334e1d5b4a 100644 --- a/desk/tests/ph/for/lure.hoon +++ b/desk/tests/ph/platform/lure.hoon @@ -11,7 +11,7 @@ :: ~bud is the invitee. he receives the invite :: and onboards to the network, joining the group. :: -:: ~fen is the bait provider +:: ~loshut-lonreg is the bait provider :: |% ++ my-test-flag ~zod^%test-group @@ -194,16 +194,16 @@ ipv4+.127.0.0.1 request == - :: ~fen the bait provider receives the onboarding request + :: ~loshut-lonreg the bait provider receives the onboarding request :: =/ =aqua-event - [%event ~fen /e/aqua/eyre/request task] + [%event ~loshut-lonreg /e/aqua/eyre/request task] (send-events ~[aqua-event]) ++ ph-test-lure-group =/ m (strand ,~) ^- form:m :: - ;< ~ bind:m (poke-app [~fen %bait] verb+[%volume %info]) + ;< ~ bind:m (poke-app [~loshut-lonreg %bait] verb+[%volume %info]) :: host a group on ~zod and enable lure links :: ;< ~ bind:m create-test-group @@ -211,7 +211,7 @@ ;< lure-invite=@t bind:m (generate-lure-invite lure-group-metadata) ;< ~ bind:m (watch-app /~bud/groups/v1/foreigns [~bud %groups] /v1/foreigns) ;< ~ bind:m (watch-app /~bud/chat/v4 [~bud %chat] /v4) - ;< cookie=(unit @t) bind:m (eyre-authenticate ~fen) + ;< cookie=(unit @t) bind:m (eyre-authenticate ~loshut-lonreg) ?> ?=(^ cookie) :: ~bud onboards from hosting through the lure invite. :: @@ -240,10 +240,10 @@ =/ m (strand ,~) ^- form:m :: - ;< ~ bind:m (poke-app [~fen %bait] verb+[%volume %info]) + ;< ~ bind:m (poke-app [~loshut-lonreg %bait] verb+[%volume %info]) ;< lure-invite=@t bind:m (generate-lure-invite lure-personal-metadata) ;< ~ bind:m (watch-app /~bud/chat/v4 [~bud %chat] /v4) - ;< cookie=(unit @t) bind:m (eyre-authenticate ~fen) + ;< cookie=(unit @t) bind:m (eyre-authenticate ~loshut-lonreg) ?> ?=(^ cookie) :: ~bud onboards from hosting through the lure invite. :: diff --git a/docs/aqua/development.md b/docs/aqua/development.md new file mode 100644 index 0000000000..e28b97a316 --- /dev/null +++ b/docs/aqua/development.md @@ -0,0 +1,225 @@ +# Aqua test development + +Aqua tests allow a test to execute in a reproducible, virtual urbit environment. +When a test runs, it is presented with: +1. A clean slate fleet of virtual ships, usually galaxies. +2. A connection to the aqua virtual runtime, which allows it to + interface with running virtual ships. + +A single test file can define any number tests. Between the run of each test, +the test runner thread `-ph-test` takes care of resetting the test environment. + +## The Structure of Aqua Tests + +An aqua test is a core with test arms, where each +test arm has the prefix `ph-test` and resolves to a strand with the signature `(strand ,~)`. + +The thread runner builds a test file, extracts and builds all matching test arms. +To run a test, the runner will restore the specified aqua snapshot, and run the defined +test thread. If the thread returns a null, it means the execution was successful. +A thread failure indicates test failure. The test runner then displays the error. + +Here is an example test file with a single test case `+ph-test-sleep`. +It waits for `~s5` and always return successfully. +```hoon +/- spider +/+ *ph-io, *ph-test +=, strand=strand:spider +:: +ph-test-sleep: sleep test +:: +++ ph-test-sleep + =/ m (strand ,~) + ^- form:m + ;< ~ bind:m (sleep ~s5) + (pure:m ~) +-- +``` + +If the test thread were to hang and never return, the test runner would eventually timeout +the test and report a failure. + +### Libraries + +Let's look at the imports in the above example. The `spider` sur file is the thread interface. +Since each test case is a thread, we generally use it, primarily to define the strand type. + +Next, we have `ph-io`. This is the library for interacting with the aqua runtime from threads, and defines +strands to send events to aqua and other utilities. + +Finally, `ph-test` is a dedicated library for writing aqua tests. It currently contains many useful utilities +missing from `ph-io`, such as strands interfacing with virtual ships: poking, watching, receiving facts and scrying. +It also contains test assertions, such as `+ex-equal`, `+ex-not-equal`. + +## Developing Aqua Tests + +When developing a new aqua test, we start by specifying the scope of the test. The scope could encompass +a single system component, such as a single gall agent, or target multiple system components involved +in the process under testing. + +Once we have identified the components we then prepare appropriate snapshot with a fleet size big enough +to accommodate test scenarios. + +While we develop the test, we can use a snapshot targeted to our use case. +However, when the new test ships to production, we must make sure that the snapshot used in production +has a big enough fleet size. If that is not the case, the production +test runner must be updated. + +### Test scenarios + +Each test arm should correspond to a single test scenario. +When developing a new test, we start out by describing the scenario +in the comment directly above the test arm. The test scenario should be +be brief and written from third-person perspective. It should not +overwhelm the reader with too much detail, but should contain essential +information about assertion made during the test. +Here is an example test arm for the group join process: +```hoon +:: +ph-test-group-join: test group joins +:: +:: scenario +:: +:: ~zod hosts a group and sends an invitation to ~bud. +:: ~bud receives the invitation and joins the group successfully, +:: receiving the group creation fact. +:: +++ ph-test-group-join + =/ m (strand ,~) + ^- form:m +::... +``` + +Important: when naming arms anywhere in the outer core, avoid names +starting with the `test` prefix. This would cause the `-test` unit test +runner to attempt to resolve the arm as a unit test, which would break +the unit test suite. + +## Comment style guide + +Following a long maritime tradition, Urbit ships generally use feminine pronouns. +However for galaxies, which hold authority over the network, use masculine pronouns. + +In comments describing a sequence of assertions, try to use sequential +language. For instance, prefer "then" rather than "and" to describe a +sequence of events happening one after another. This is rule is not absolute, +sometimes, especially when talking about events concerning the same +ship, we might profitably employ "and". + +Prefer "and" to "then" especially if two events logically follow one another +in the context of the test. For example, we prefer to use "and" in "~zod hosts a +group and invites ~bud", since in the context of the test, there is no +decision on `~zod`'s part involved: the group was created _so that_ +`~bud` could join it. + +### Test assertions + +Aqua tests do not have direct access to the underlying gall agent. The +test can only interface with the virtual ship through arvo tasks. +The perspective is that of a client integrating with a particular system component. + +When testing gall agents, we have essentially two ways to approach test +assertions. The first one is to establish a subscription and assert on +facts. The second is to directly query an agent state with a scry. + +Here is the full group join test +```hoon +++ ph-test-group-join + =/ m (strand ,~) + ^- form:m + ;< ~ bind:m (watch-app /~bud/groups/v1/groups [~bud %groups] /v1/groups) + ;< ~ bind:m (watch-app /~bud/groups/v1/foreigns [~bud %groups] /v1/foreigns) + :: ~zod hosts a group and invites ~bud + :: + =/ =create-group:g + :* %my-test-group + ['My Test Group' 'My testing group' '' ''] + %secret + [~ ~] + (my ~bud^~ ~) + == + ;< ~ bind:m (poke-app [~zod %groups] group-command+[%create create-group]) + :: ~bud joins the group using received invite token + :: + ;< kag=cage bind:m (wait-for-app-fact /~bud/groups/v1/foreigns [~bud %groups]) + ?> =(%foreigns-1 p.kag) + =+ !<(=foreigns:v8:gv q.kag) + =+ foreign=(~(got by foreigns) my-test-flag) + ?> ?=(^ invites.foreign) + =/ =a-foreigns:v8:gv + [%foreign my-test-flag %join token.i.invites.foreign] + ;< ~ bind:m (poke-app [~bud %groups] group-foreign-2+a-foreigns) + ;< ~ bind:m (ex-r-groups-fact ~bud ~zod^%my-test-group %create) + (pure:m ~) +``` +We first establish two app subscriptions to `%groups` on `~bud`. +After `~zod` created the group, we expect `~bud` to first receive the +group invitation on the foreigns subscription. After we have verified +that an invitation has indeed been received, we poke `~bud` to join the +group. We verify that the group has been successfully joined by +expecting a group creation response on the groups subscription. + +### Test assertions and Hoon assertions + +When asserting on a value expected by the test, we have two choices. +We can aqua assertion, such as `+ex-equal`. This will terminate the +thread with an appropriate error message, describing the discrepancy +between expected and actual values. Alternatively, we can use any of the +Hoon assertions such as `?>` or `?<`, which will simply crash the thread +without a specific error message. The obvious downside of Hoon +assertions is that they don't carry any information about the way +assertion has failed. + +We should generally use aqua assertions. However, when asserting on +things that are considered unchengable parts of an interface, and do not in +themselves implement any logic which could be broken, using +Hoon assertions is permissible. One example is asserting on marks of +received facts. Since these are generally considered fixed, crashing +with a Hoon assertions is more ergonomic. + +Sometimes we might be tempted to use Hoon assertions out of pure convenience, +such using crashing map getter `+get:by`, or unpacking a unit using `?>`. +However, taking such shortcuts will simply make for a later inconvenience when debugging a +broken test. + +When investigating a test failing on a Hoon assertion, which does not +display any values, using the debug print rune `~&` can be a good way to +investigate the problem. If the test failure does not stem from an +originally wrong implementation, it is likely the case that it could be +broken in the future and we should consider to convert it to an aqua +assertion. + +### Interfacing with gall agents + +The only interface exposed by aqua is that of an Urbit runtime, with its +4 standard arms. Aqua exposes an interface to poke and scry virtual +arvo, as well as receive effects. +It is therefore not possible to interface directly through gall API with apps running inside virtual ships. +Instead, we use arvo tasks to pass messages to vanes running on a +virtual ship. To receive effects, we can subscribe to a generic aqua +endpoint, and also target a specific type of arvo effects by using a specific subscription path. + +### Implementing a test + +We have so far talked about the structure of an aqua test. It is +composed of a prose description, followed by the test strand +implementation, which essentially is a sequence of events send to the aqua runtime +or assertions on various effects received. + +We are now going to focus on the test implementation. Once we have +specified the test scenario, how do we go about implementing it? +In some cases, it might be enough to look at relevant portions of the code to +be able to specify the sequence of events and assertions. However, +discerning the exact test process and data involved is not always easy just +by reading the code. Sometimes, the functionality might be spread across many libraries +or agents. + +Fortunately, we can observe the running system by enabling debugging mode. +Any agent that integrates the logging library, together with the verb wrapper, will +display debugging messages when enabled. To enable debugging mode, we adjust the logging +volume +``` +> :agent &verb [%volume volume] +``` +where volume can be any of `%dbug`, `%info`, `%warn` and `%crit`. +Adjusting the volume to `%dbug` will enable printing of all log messages at that priority or above. + + diff --git a/docs/aqua/introduction.md b/docs/aqua/introduction.md new file mode 100644 index 0000000000..91d61ae44b --- /dev/null +++ b/docs/aqua/introduction.md @@ -0,0 +1,190 @@ +# Aqua + +Aqua is a virtual urbit runtime implemented in Hoon. +It virtualizes arvo instances and allows a virtual urbit ship to be +hosted inside the host arvo at a fraction of the memory cost. + +## Aqua tests + +Aqua is an ideal urbit testing environment. +It allows us to run fleets of virtual ships in a reproducible manner. +There is no need to implement stubs for any of the kernel components, given +that virtual arvo is just a copy of the real arvo running on the host. + +The only deficiency is that aqua currently does not implement all of the +runtime effects. However, while this might pose some difficulties when running +a virtual ship indented to interface with the urbit livenet, +all the functionality needed to perform tests on a self-contained +fleet of virtual ships is there. + +## Preparing aqua pill + +Before aqua can run virtual ships, the aqua pill must be prepared.This +pill is used to boot each virtual ship. Once aqua has been initialized with the pill +there is no need to reinitialize it unless: +1. Aqua state has been reset. +2. We want the pill to incorporate some changes, such as an updated desk or additional files. + +In the latter case, we also have a choice to prepare an aqua snapshot with desk sync enabled, +that will contain the updated desk irrespective of the version frozen in the pill. +This is usually preferrable, unless we plan to generate multiple snapshots. In that case, +generating a new pill containing updated desk will usually be faster than the accumulated +cost of syncing virtual desks for each ship in a snapshot. + +To initialize aqua with a fresh pill, we poke aqua with an assembled pill. This +can be done in one go by using the brass pill generator. +``` +> :aqua &pill +pill/brass %base desk1 desk2 +``` + +The brass pill takes a list of desks as an argument. The `%base` desk is required. + +Brass pill generation takes about 5 minutes or longer to complete. + +## Preparing aqua snapshot + +Each aqua test requires a reproducible environment consisting of a +fleet of virtual ships. It would be too expensive to boot the fleet +each time a test runs. Fortunately, aqua provides a way to snapshot a booted +set of ships. A snapshot can then be restored each time a test runs, providing +a fast way to initialize a test environment. + +To prepare a snapshot, decide upon a number of virtual ships required. +Since aqua tests use a hardcoded set of ships, by convention we use galaxies +in enumeration order as test ships. Thus, for a test involving 4 ships we would have +`~zod`, `~nec`, `~bud` and `~wes`. If more ships are required, discover the n-th galaxy id by using +``` +> `@p`n +``` + +Prepare the snapshot by running +``` +> -desk!ph-fleet fleet sync +``` +where `fleet` is the list of ships and `sync` is the desk sync flag. +Default the sync flag to `|`, unless there has been changes to the desk. +Currently, syncing desk to virtual ships takes quite some time. + +The thread will prepare an aqua snapshot, and return the snapshot id. +You can save it to a dojo variable with +``` +> =snap-id -desk!ph-fleet fleet sync +``` +to avoid having to retype each time tests are run. + +In addition to galaxy test ships, a test may require a number of _provider_ +ships to be running, to allow components of the system to properly integrate +with remote services. + +In the groups desk, we need `%bait` and `%notify` providers, running on `~loshut-lonreg` +and `~rivfur-livmet` respectively. In addition, virtual ames/mesa networking requires +the parent galaxies to be present. In this case, we must add `~fen` and `~dem` to the +fleet. This is handled by the `-ph-fleet` thread + +When a host ship accumulated enough snapshots to be a memory burden, we +can delete all existing snapshots using an aqua poke: +``` +> :aqua &noun [%clear-snap snap-id] +``` + +**Important**: at present aqua does not handle the snap-id argument, and will delete +all snapshots upon receiving this poke. + +## Updating the desk + +When changes have been made in the repository and are ready to be tested +on a running ship, use `rsync` or equivalent command to copy the `desk/` in the repository +to an appropriate pier. This will put the files into the ship's desk unix mount point directory. +To make the changes visible on the ship, you must also issue `|commit` command, which +will detect any changes in the unix mount point directory. `|commit` can display `sync` spinner, +which indicates the commit is still ongoing. + +Important: do not delete files not present in the repository at the destination. +Running urbit desks are usually supplemented with other neccessary files not present +in the repository. + +## Checking tests + +While developing tests, it is useful to have a way to verify any compilation errors +separately before triggering the actual test run. This can be done using +`-ph-test-ls path` command, which will find all aqua tests available at the +path and build them. For example, to list all aqua files in the groups desk, we would run +``` +> -groups!ph-test-ls /=groups=/tests/ph +``` +, and to list tests for a particular agent we would use +``` +> -groups!ph-test-ls /=groups=/tests/ph/app/contacts +``` + +There are two kind of errors that can occur at compilation time. +A `FAILED BUILD` error indicates the test file did not build. +The compiler error is shown directly above, because it is coming from clay. +A `FAILED MINT` error indicates that a test arm in a successfully compiled test +file does not resolve properly. Usually this indicates that the arm resolved +to a type that does not match the test signature, which should be a form of the strand `(strand ,~)`. +The possible compiler error is also displayed right the error line, for consistency +with file build errors. + +## Running tests + +To run aqua tests use the `-ph-test` test runner supplied with the desk. +``` +> -desk!ph-test `path snap +``` +The `path` is a directory containing aqua tests (remember to include the backtick). Supply `~` to run from the default directory. +It will be scanned recursively, and any tests found will be build and scheduled to be run. +`snap` is the aqua snapshot on which tests are going to run. + +The standard location for aqua tests is in the desk's `/tests/ph` directory. +The path can be extended to target a particular component. For instance, to +run only aqua tests for the group agent, use `/tests/ph/app/groups`. +You can also target a particular test by appending the test name, such as +`/tests/ph/app/groups/ph-test-group-join` to run only the groups group join test. + +The current virtual ames driver is inefficient. This make a single test run take +about `~20s`, or more, depending on the number of networked interactions between +virtual ships. + +As a general note, arguments for threads in dojo can be listed one after another. + +### Understanding test output +While tests run, information will be displayed from a variety of sources. +The test runner will display test result and time taken after run of each test case. +The aqua virtual runtime is currently quite verbose, and will display a lot of noise, +primarily about unhandled effects. + +Running virtual ships will also display logging messages. +These messages are associated with a priority: + +| prefix | priority | +|-----------|------------------------------| +| no prefix | `%dbug` debug priority | +| `>` | `%info` information priority | +| `>>` | `%warn` warning priority | +| `>>>` | `%crit` critical priority | + +Each logging message is prefix by the virtual source ship and agent names. + +An example error message +``` +>>> [~zod/groups] Critical failure in +se-u-groups +``` +means the `%groups` agent running on `~zod` is reporting an error generated +in `+se-u-groups` arm. + +Since the logging messages are not associated with a particular location +in the source code, to find the originating location we can either start with +the location indicated in the message. In the above example, that would +be the `+se-u-groups` arm somewhere in the agent's source code or its libraries. +If that does not yield results, searching for constant parts of the messsage in relevant files usually. + +## Cancelling a test run + +The test runner is a thread, so in order to stop an ongoing test run you must +cancel the runner thread. In dojo, this is simply done by pressing a backspace. + +## Developing aqua tests + +To learn how to develop aqua tests, see ./docs/aqua/development.md +