Family compatibility
Verification
Scope
The full PHP test suite, including the table and chart formatters, with the current phibble, phplyr, and phiz releases installed.
References
- Versions
- PHP 8.5.10, phibble 0.5.3, phplyr 0.4.6, phiz 0.5.2, vl-convert vl-convert 1.9.0
Method
Pest runs in a copy of the tested commit with the three packages required from Packagist.
- Check
- End-to-end run with the external program
- Recorded
- 2026-11-01, PHP 8.5.10, macOS 27.0.1 (arm64), commit 46349a3
Tolerances
exact equality
Applies to: Asserted values and output
x: package value; r: reference value.
Results
784 comparisons in 33 cases: 784 pass, 0 fail, 0 skipped.
By case
| Case | Comparisons | Status |
|---|---|---|
| Engine/ChannelTest | 25 | pass |
| Engine/EngineTest | 61 | pass |
| Engine/IdleTest | 3 | pass |
| Engine/ProjectTest | 14 | pass |
| Unit/Console/ApplicationTest | 25 | pass |
| Unit/Console/OptionsTest | 15 | pass |
| Unit/Console/ReferenceTest | 5 | pass |
| Unit/Display/ChartFormatterTest | 22 | pass |
| Unit/Display/DisplayFunctionTest | 7 | pass |
| Unit/Display/DumperTest | 23 | pass |
| Unit/Display/FallbackFormattersTest | 10 | pass |
| Unit/Display/MimeBundleTest | 10 | pass |
| Unit/Display/NoSiblingAtCompileTimeTest | 17 | pass |
| Unit/Display/RegistryTest | 24 | pass |
| Unit/Display/TableFormatterTest | 13 | pass |
| Unit/Display/TableRendererTest | 26 | pass |
| Unit/Display/UserTypesTest | 12 | pass |
| Unit/Engine/CleanerTest | 99 | pass |
| Unit/Engine/ExecutorTest | 70 | pass |
| Unit/Engine/SessionTest | 24 | pass |
| Unit/Engine/TracebackTest | 27 | pass |
| Unit/Install/DoctorCommandTest | 20 | pass |
| Unit/Install/InstallCommandTest | 66 | pass |
| Unit/Install/JupyterPathsTest | 18 | pass |
| Unit/Install/KernelSpecTest | 27 | pass |
| Unit/Install/PharTest | 15 | pass |
| Unit/Install/PythonProbeTest | 23 | pass |
| Unit/Install/QuartoProbeTest | 16 | pass |
| Unit/Install/UninstallCommandTest | 17 | pass |
| Unit/Install/VenvBuilderTest | 15 | pass |
| Unit/Project/AutoloadTest | 16 | pass |
| Unit/Project/ConfigTest | 15 | pass |
| Unit/VersionTest | 4 | pass |
All 784 comparisons
| Case | Quantity | Status |
|---|---|---|
| Engine/ChannelTest | framing → it writes one JSON object per line | pass |
| framing → it reads one line at a time, without the newline | pass | |
| framing → it reports end of input as null | pass | |
| framing → it keeps a newline inside a string from breaking the framing | pass | |
| framing → it leaves slashes and unicode readable on the wire | pass | |
| framing → it substitutes bytes that are not valid UTF-8 rather than failing | pass | |
| writing to a peer that has gone → it gives up quietly rather than throwing | pass | |
| writing to a peer that has gone → it stays quiet for every write after that | pass | |
| writing to a peer that has gone → it reads null once the peer has gone | pass | |
| writing to a peer that has gone → it is closed after close(), and writing then does nothing | pass | |
| a line longer than it will read → it gives up on an over-long line and resumes at the next one | pass | |
| a line longer than it will read → it treats a final line with no newline as the last thing said | pass | |
| connecting → it refuses clearly when there is nothing listening | pass | |
| connecting → it connects to a socket that is listening | pass | |
| the request shape task 6 writes → it reads the five request types | pass | |
| the request shape task 6 writes → it reads execute's fields, with the documented defaults | pass | |
| the request shape task 6 writes → it defaults the cursor to the end of the code, and clamps it | pass | |
| the request shape task 6 writes → it ignores a field of the wrong type rather than trusting it | pass | |
| the request shape task 6 writes → it refuses what is not a request, with a message fit for an evalue | pass | |
| the event shape task 6 reads → it puts the id and type first, then the type's own fields | pass | |
| the event shape task 6 reads → it builds each reply with the fields the contract names | pass | |
| the event shape task 6 reads → it always gives is_complete_reply an indent, even when empty | pass | |
| the event shape task 6 reads → it carries a null id for a complaint about the channel itself | pass | |
| the event shape task 6 reads → it keeps a traceback it was given rather than making one up | pass | |
| the event shape task 6 reads → it reports the running PHP version in kernel_info_reply | pass | |
| Engine/EngineTest | the connection → it connects to the socket named in JUPHYTER_SOCKET | pass |
| the connection → it keeps its own stdout and stderr clear of the protocol | pass | |
| the connection → it exits 0 when the transport closes the channel without asking | pass | |
| every request ends with one terminal event → it ends a cell with nothing to show in a single ok | pass | |
| every request ends with one terminal event → it ends a cell with a value in result then ok | pass | |
| every request ends with one terminal event → it ends a thrown cell in error, with no ok | pass | |
| every request ends with one terminal event → it answers each other request with its matching reply | pass | |
| every request ends with one terminal event → it tags every event with the id of the request it answers | pass | |
| a session across requests → it carries state from one cell to the next | pass | |
| a session across requests → it carries a declared function and an import across cells | pass | |
| a session across requests → it refuses a redeclaration, so one session guards the whole process | pass | |
| streaming → it sends each print as its own event, in order, before the terminal one | pass | |
| streaming → it sends what a cell printed before the error that followed it | pass | |
| streaming → it sends a warning on stderr rather than as an error | pass | |
| the execution count → it advances once per stored cell and matches what the front end shows | pass | |
| the execution count → it does not advance when store_history is false | pass | |
| the execution count → it reports the same count on result and ok | pass | |
| the execution count → it advances for a cell that failed, as a notebook does | pass | |
| the execution count → it labels a traceback with the number the front end shows | pass | |
| silent → it suppresses the result but not the terminal event | pass | |
| silent → it suppresses a display but not an error | pass | |
| silent → it does not suppress stream output, which a cell asked for explicitly | pass | |
| silent → it applies to one request only | pass | |
| the display wiring → it turns a cell value into a result with a text/plain bundle | pass | |
| the display wiring → it publishes nothing extra for a cell that ends in display() | pass | |
| the display wiring → it turns Juphyter\display() into a display event rather than stdout | pass | |
| the display wiring → it publishes several values in the order display() was given them | pass | |
| the display wiring → it gives a display event a metadata field, as the contract says | pass | |
| is_complete → it calls finished source complete | pass | |
| is_complete → it calls source that only wanted a semicolon complete | pass | |
| is_complete → it calls an unclosed brace incomplete, with an indent to continue at | pass | |
| is_complete → it indents one level further after a line that opened a brace | pass | |
| is_complete → it calls a syntax error invalid | pass | |
| is_complete → it does not touch the session, however often a front end asks | pass | |
| is_complete → it does not advance the execution count | pass | |
| kernel_info → it reports the PHP version actually running | pass | |
| kernel_info → it carries a banner naming the version too | pass | |
| completion → it completes a session variable after $ | pass | |
| completion → it completes a declared function by prefix | pass | |
| completion → it completes a declared class by prefix | pass | |
| completion → it completes a member after -> on a variable the session holds | pass | |
| completion → it completes a dynamic property, which a notebook value often has | pass | |
| completion → it completes a static member after :: | pass | |
| completion → it offers nothing for a member of a value it does not hold | pass | |
| completion → it offers nothing rather than guessing where there is no token | pass | |
| malformed input → it answers a line that is not JSON and keeps serving | pass | |
| malformed input → it answers JSON that is not an object | pass | |
| malformed input → it answers a request with no id | pass | |
| malformed input → it answers a request with no type, against its id | pass | |
| malformed input → it answers an unknown type against the id it was given | pass | |
| malformed input → it keeps serving after several bad lines in a row | pass | |
| malformed input → it does not hang on a line with no newline at the end | pass | |
| shutdown → it replies, then exits 0 | pass | |
| shutdown → it echoes the restart flag the transport sent | pass | |
| shutdown → it closes the channel, so there is nothing more to read | pass | |
| shutdown → it does not report the shutdown as a cell having ended the process | pass | |
| a cell that ends the process → it is reported as SystemExit | pass | |
| a cell that ends the process → it still delivers what the cell printed first | pass | |
| interrupts → it reports an interrupt and keeps the session usable | pass | |
| interrupts → it is not swallowed by the cell's own catch (Exception) | pass | |
| interrupts → it ignores a signal that arrives between cells | pass | |
| Engine/IdleTest | it answers a request after sitting idle for longer than a read timeout | pass |
| it exits when the transport closes the socket, timeout or no timeout | pass | |
| it waits out an expired read and still obeys end of input | pass | |
| Engine/ProjectTest | the walk up → it gives a notebook its own project with no configuration at all | pass |
| the walk up → it finds the project from a notebook in a subdirectory | pass | |
| the walk up → it serves perfectly well with no project at all | pass | |
| JUPHYTER_AUTOLOAD → it overrides the walk | pass | |
| JUPHYTER_AUTOLOAD → it is reported as an engine error when it names nothing, rather than hanging | pass | |
| juphyter.json → it names the autoloader, and beats the walk | pass | |
| juphyter.json → it loses to the environment | pass | |
| juphyter.json → it sets the memory limit the cells run under | pass | |
| juphyter.json → it adds to the include path without losing what was there | pass | |
| juphyter.json → it applies its settings even when it names no autoloader | pass | |
| juphyter.json → it is reported as an engine error when it is not valid JSON | pass | |
| the --autoload and --directory options → it takes an autoloader from the command line | pass | |
| the --autoload and --directory options → it takes the notebook directory from the command line | pass | |
| juphyter loading twice → it survives a project that carries its own copy of juphyter | pass | |
| Unit/Console/ApplicationTest | discovery → it names a command from its class name | pass |
| discovery → it finds the concrete commands in its directory, sorted by name | pass | |
| discovery → it scans its directory once, however many times it is asked | pass | |
| discovery → it leaves out abstract classes and files that are not PHP | pass | |
| discovery → it reports an empty directory as no commands | pass | |
| discovery → it treats a missing directory as empty rather than an error | pass | |
| discovery → it reads this package's own command directory without error | pass | |
| dispatch → it runs a registered command and returns its exit code | pass | |
| dispatch → it passes on the arguments after the subcommand name | pass | |
| dispatch → it dispatches a kebab-cased name | pass | |
| construction → it lists a command with a required constructor argument | pass | |
| construction → it dispatches a command with a required constructor argument through make() | pass | |
| construction → it does not construct a command to list it | pass | |
| construction → it reports a command it cannot build, rather than dying | pass | |
| construction → it still lists and dispatches the other commands when one cannot be built | pass | |
| construction → it keeps the unknown-command path working alongside such a command | pass | |
| usage → it lists the commands on stdout and exits 0 with no arguments | pass | |
| usage → it lists the commands for --help and -h too | pass | |
| usage → it says so plainly when there is no command to list | pass | |
| usage → it still rejects an unknown command when nothing is registered | pass | |
| usage → it prints usage to stderr and exits non-zero for an unknown command | pass | |
| usage → it does not run a command when the name is unknown | pass | |
| version → it reports the version on stdout and exits 0 for --version and -V | pass | |
| version → it answers even when no command can be listed | pass | |
| version → it treats a lowercase -v as an unknown command rather than the version flag | pass | |
| Unit/Console/OptionsTest | parsing → it reads an option and its value as two arguments | pass |
| parsing → it reads an option joined to its value by an equals sign | pass | |
| parsing → it reads both forms together, in any order | pass | |
| parsing → it reads nothing from nothing | pass | |
| parsing → it keeps a value that contains an equals sign | pass | |
| parsing → it keeps an empty value given with = | pass | |
| parsing → it lets a later option win over an earlier one | pass | |
| complaints → it refuses a stray argument, and shows the usage | pass | |
| complaints → it refuses an unknown option, and shows the usage | pass | |
| complaints → it refuses an option with no value | pass | |
| complaints → it refuses an option whose value is the next option | pass | |
| complaints → it includes the command's own usage in a complaint | pass | |
| help → it spots --help and -h anywhere in the arguments | pass | |
| help → it spots help even beside something malformed, so help still works | pass | |
| help → it is false when help was not asked for | pass | |
| Unit/Console/ReferenceTest | it documents every option a command prints with data set "('install')" | pass |
| it documents every option a command prints with data set "('uninstall')" | pass | |
| it documents every option a command prints with data set "('doctor')" | pass | |
| it documents every option a command prints with data set "('engine')" | pass | |
| it documents every command the application lists | pass | |
| Unit/Display/ChartFormatterTest | matching → it matches by name, so it claims nothing without phiz installed | pass |
| the bundle → it publishes the four representations in order when vl-convert is there | pass | |
| the bundle → it leaves out image/svg+xml when vl-convert is not there | pass | |
| the bundle → it leaves out image/svg+xml rather than failing when the conversion does | pass | |
| the bundle → it publishes the spec under the v5 MIME type, schema and all | pass | |
| the bundle → it takes text/html from embed() | pass | |
| the one-line summary → it names the class, the mark, the rows and the Vega-Lite version | pass | |
| the one-line summary → it joins the marks of a layered plot | pass | |
| the one-line summary → it says how a composition is arranged with data set "('hconcat', 'side by side')" | pass | |
| the one-line summary → it says how a composition is arranged with data set "('vconcat', 'stacked')" | pass | |
| the one-line summary → it says how a composition is arranged with data set "('concat', 'in a grid')" | pass | |
| the one-line summary → it finds the rows a composition carries in its first dataset | pass | |
| the one-line summary → it reads in the singular for one row | pass | |
| the one-line summary → it leaves out the rows when the data is behind a URL | pass | |
| the one-line summary → it names the class alone when the spec says nothing it can use | pass | |
| a real phiz chart → it matches a Plot and a Compose through their shared base | pass | |
| a real phiz chart → it publishes a Plot with the spec and the embed phiz produced | pass | |
| a real phiz chart → it describes a Compose by how it arranges its plots | pass | |
| a real phiz chart → it reaches a chart through the default registry | pass | |
| a real vl-convert binary → it publishes an SVG that vl-convert actually rendered | pass | |
| a real vl-convert binary → it publishes an SVG for a composition too | pass | |
| a real vl-convert binary → it finds the binary the way phiz does, and misses it when nothing points at one | pass | |
| Unit/Display/DisplayFunctionTest | the exported function → it is loaded by the autoloader, not by an include | pass |
| the exported function → it publishes one bundle for one value | pass | |
| the exported function → it publishes the whole bundle a formatter built, not only its text | pass | |
| the exported function → it publishes several values in the order given | pass | |
| the exported function → it formats with the current registry | pass | |
| with no publisher installed → it writes text/plain to standard output | pass | |
| with no publisher installed → it goes back to that after a publisher is removed | pass | |
| Unit/Display/DumperTest | scalars → it renders null, booleans and integers | pass |
| scalars → it keeps a float reading as a float | pass | |
| scalars → it names the floats that have no digits | pass | |
| scalars → it quotes a string so it cannot be read as a number | pass | |
| scalars → it escapes quotes, backslashes and newlines in a string | pass | |
| scalars → it clips a long string | pass | |
| arrays → it renders a list on one line | pass | |
| arrays → it renders keys for a map | pass | |
| arrays → it nests while it fits | pass | |
| arrays → it stops at the depth limit | pass | |
| arrays → it counts the entries past the item limit | pass | |
| arrays → it wraps a long list of scalars instead of giving each a line | pass | |
| arrays → it still gives a nested entry its own line | pass | |
| arrays → it expands one entry per line when the line would be too long | pass | |
| arrays → it keeps an inner entry on one line while that one fits | pass | |
| arrays → it never widens past the budget by indenting | pass | |
| objects → it names an enum case | pass | |
| objects → it names a closure without trying to describe it | pass | |
| objects → it renders a date as a date | pass | |
| objects → it lists the public properties of an object | pass | |
| objects → it says so when an object has no public properties | pass | |
| objects → it stops at the depth limit | pass | |
| everything else → it names a resource by its type | pass | |
| Unit/Display/FallbackFormattersTest | scalars → it supports null and every scalar, and nothing else | pass |
| scalars → it carries text/plain and nothing else | pass | |
| arrays → it leaves a flat list as plain text alone | pass | |
| arrays → it offers application/json for a nested array | pass | |
| arrays → it leaves out application/json when the array cannot be encoded | pass | |
| objects → it dumps the public properties of an object as text | pass | |
| objects → it offers application/json only when the class opted in | pass | |
| throwables → it prints the class, the message and where it came from | pass | |
| throwables → it leaves out the colon when there is no message | pass | |
| throwables → it supports every throwable, errors included | pass | |
| Unit/Display/MimeBundleTest | construction → it starts empty | pass |
| construction → it carries text/plain from plain() | pass | |
| construction → it answers null for a type it does not carry | pass | |
| immutability → it leaves the original alone when a type is added | pass | |
| immutability → it leaves the original alone when a type is removed | pass | |
| order → it keeps the types in insertion order | pass | |
| order → it keeps a type in place when it is set again | pass | |
| data → it carries a decoded structure, not only strings | pass | |
| data → it keeps metadata per type and only when given | pass | |
| data → it drops the metadata along with the type | pass | |
| Unit/Display/NoSiblingAtCompileTimeTest | it names no package of the family at compile time with data set "dataset "src/Display/Formatter.php"" | pass |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/ArrayFormatter.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/ChartFormatter.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/Dumper.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/HtmlFormatter.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/MimeBundleFormatter.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/ObjectFormatter.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/Reflect.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/ScalarFormatter.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/TableFormatter.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Formatters/ThrowableFormatter.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Html/TableRenderer.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/MimeBundle.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/Display/Registry.php"" | pass | |
| it names no package of the family at compile time with data set "dataset "src/functions.php"" | pass | |
| it has the names it matches on as plain strings | pass | |
| it loads every class in the display layer with none of the family installed | pass | |
| Unit/Display/RegistryTest | dispatch order → it ships the formatters in the documented order | pass |
| dispatch order → it takes the first formatter that supports the value | pass | |
| dispatch order → it stops asking once one has claimed the value | pass | |
| dispatch order → it asks them in order until one claims the value | pass | |
| dispatch order → it does not merge the bundles of two formatters | pass | |
| composition → it puts prepended formatters first | pass | |
| composition → it puts appended formatters last | pass | |
| composition → it leaves the registry it was built from alone | pass | |
| text/plain → it is on every bundle the registry returns with data set "(null)" | pass | |
| text/plain → it is on every bundle the registry returns with data set "(false)" | pass | |
| text/plain → it is on every bundle the registry returns with data set "(0)" | pass | |
| text/plain → it is on every bundle the registry returns with data set "(1.5)" | pass | |
| text/plain → it is on every bundle the registry returns with data set "('')" | pass | |
| text/plain → it is on every bundle the registry returns with data set "([])" | pass | |
| text/plain → it is on every bundle the registry returns with data set "([1, 2, 3])" | pass | |
| text/plain → it is on every bundle the registry returns with data set "([['c']])" | pass | |
| text/plain → it is on every bundle the registry returns with data set "(Closure Object ()) #1" | pass | |
| text/plain → it is on every bundle the registry returns with data set "(Closure Object ()) #2" | pass | |
| text/plain → it is on every bundle the registry returns with data set "(Closure Object ()) #3" | pass | |
| text/plain → it is on every bundle the registry returns with data set "(Closure Object ()) #4" | pass | |
| text/plain → it is filled in when a formatter left it out | pass | |
| text/plain → it is the whole bundle for a value nothing claimed | pass | |
| the current registry → it defaults to the shipped formatters | pass | |
| the current registry → it can be replaced, and restored with null | pass | |
| Unit/Display/TableFormatterTest | matching → it matches by name, so it claims nothing without phibble installed | pass |
| the bundle → it carries text/html first, then text/plain | pass | |
| the bundle → it takes text/plain from the collected frame itself | pass | |
| the bundle → it renders the HTML when the table has none of its own | pass | |
| the bundle → it prefers a toHtml() the table brought itself | pass | |
| the bundle → it collects a lazy table once, for both representations | pass | |
| the bundle → it uses the renderer it was given | pass | |
| a real phibble table → it matches a DataFrame | pass | |
| a real phibble table → it prints a DataFrame exactly as phibble prints it | pass | |
| a real phibble table → it renders a DataFrame as HTML, to its snapshot | pass | |
| a real phibble table → it matches a SqlTable, and prints it as phibble does | pass | |
| a real phibble table → it would match a PolarsTable, which implements the same contract | pass | |
| a real phibble table → it matches a PolarsTable instance when php_polars is loaded | pass | |
| Unit/Display/TableRendererTest | snapshots → it renders a small frame | pass |
| snapshots → it renders a frame with rows and columns held back | pass | |
| snapshots → it renders a grouped frame | pass | |
| snapshots → it renders a frame with no rows | pass | |
| the header → it states the dimensions as phibble does | pass | |
| the header → it separates the thousands in a big dimension | pass | |
| the header → it gives every column its type abbreviation | pass | |
| the header → it names the group columns, and says rowwise when it is | pass | |
| the header → it leaves the grouping line out when the frame is ungrouped | pass | |
| the footer → it counts the rows it held back | pass | |
| the footer → it names the columns it held back, with their types | pass | |
| the footer → it reads in the singular for one row and one column | pass | |
| the footer → it is left out when the whole frame is shown | pass | |
| cells → it marks a null as NA | pass | |
| cells → it prints booleans as phibble prints them | pass | |
| cells → it prints a date without the time | pass | |
| cells → it prints a datetime with the time | pass | |
| cells → it escapes markup in a string | pass | |
| cells → it escapes a newline rather than breaking the row | pass | |
| cells → it gives a list column its length | pass | |
| cells → it clips a long string | pass | |
| cells → it names the floats that have no digits | pass | |
| cells → it right-aligns the numeric columns and leaves the rest alone | pass | |
| cells → it fills in a cell the row is missing | pass | |
| a table from somewhere else → it renders a type it cannot abbreviate | pass | |
| a table from somewhere else → it holds rows back itself when the table offers no head() | pass | |
| Unit/Display/UserTypesTest | toMimeBundle() → it takes precedence over every built-in formatter | pass |
| toMimeBundle() → it is first in the default registry, ahead of toHtml() | pass | |
| toMimeBundle() → it fills in text/plain from __toString() when the bundle left it out | pass | |
| toMimeBundle() → it accepts a MimeBundle as well as a map | pass | |
| toMimeBundle() → it falls back to the dump when the return is not a MIME map | pass | |
| toHtml() → it becomes the text/html of the bundle | pass | |
| toHtml() → it takes text/plain from __toString() when the class has one | pass | |
| toHtml() → it takes text/plain from the dump when the class has no text form | pass | |
| what does not count as a hook → it ignores a static toMimeBundle() and a toHtml() that needs an argument | pass | |
| what does not count as a hook → it asks for a public, static-free, argument-free method | pass | |
| matching by name → it never reports a class name string as an instance | pass | |
| matching by name → it answers false for a class no installed package declares | pass | |
| Unit/Engine/CleanerTest | parse errors → it refuses source that does not parse, with the parser's own message | pass |
| parse errors → it reports the line in the user's cell, not in the prelude | pass | |
| parse errors → it leaves the session untouched when it refuses a cell | pass | |
| parse errors → it does not record a declaration from a cell it refuses | pass | |
| the forgiving semicolon → it accepts a bare trailing expression and captures it | pass | |
| the forgiving semicolon → it accepts a bare variable, which is how a notebook asks what something is | pass | |
| the forgiving semicolon → it accepts a cell ending in a // comment, which the bare semicolon could not | pass | |
| the forgiving semicolon → it accepts a cell ending in a # comment, which is what Quarto writes | pass | |
| the forgiving semicolon → it still evals a cell that ended in a comment | pass | |
| the forgiving semicolon → it says so when it did not have to append anything | pass | |
| the forgiving semicolon → it still refuses an unclosed brace, with the original message | pass | |
| the forgiving semicolon → it reports the original error, not the retry's, when the retry fails differently | pass | |
| the forgiving semicolon → it keeps the original error's line when the retry fails | pass | |
| the forgiving semicolon → it does not retry an error that is not source running out | pass | |
| the forgiving semicolon → it leaves the line mapping untouched, because it only ever appends | pass | |
| the forgiving semicolon → it produces source that really evals, semicolon and all | pass | |
| incomplete versus invalid → it calls an unclosed brace incomplete, not invalid | pass | |
| incomplete versus invalid → it calls an unclosed call incomplete | pass | |
| incomplete versus invalid → it calls an unclosed string incomplete | pass | |
| incomplete versus invalid → it calls a genuine syntax error invalid, not incomplete | pass | |
| incomplete versus invalid → it calls a stray closing brace invalid | pass | |
| incomplete versus invalid → it does not call a refused namespace incomplete, because it parsed | pass | |
| incomplete versus invalid → it does not call a redeclaration incomplete | pass | |
| incomplete versus invalid → it does not call a refused declare incomplete | pass | |
| incomplete versus invalid → it does not call an alias clash incomplete | pass | |
| incomplete versus invalid → it gives the transport all three answers without it reading a parser message | pass | |
| the prelude → it prepends declare(strict_types=1) to every cell | pass | |
| the prelude → it is one line when nothing is imported | pass | |
| the prelude → it enforces strict types, which is the point of prepending it | pass | |
| the prelude → it re-emits every accumulated import ahead of a later cell | pass | |
| the cell's own declare → it strips declare(strict_types=1) the user wrote, which PHP would reject there | pass | |
| the cell's own declare → it does not let a stripped declare shift the lines below it | pass | |
| the cell's own declare → it survives eval with the user's own declare stripped | pass | |
| the cell's own declare → it refuses declare(strict_types=0), which it cannot honour | pass | |
| the cell's own declare → it refuses declare(encoding=...), which can no longer be first | pass | |
| the cell's own declare → it refuses block-mode strict_types | pass | |
| the cell's own declare → it refuses strict_types combined with another directive, rather than dropping it | pass | |
| the cell's own declare → it refuses the combination whichever order it is written in | pass | |
| the cell's own declare → it names every directive it would have discarded | pass | |
| the cell's own declare → it leaves declare(ticks=1) where the user put it | pass | |
| imports → it lifts the cell's use into the session so later cells see it | pass | |
| imports → it removes the use from the body, because the prelude now carries it | pass | |
| imports → it emits the cell's own import exactly once, which PHP requires | pass | |
| imports → it handles use function and use const | pass | |
| imports → it expands a grouped use into one statement per name | pass | |
| imports → it reads the per-name type out of a mixed grouped use | pass | |
| imports → it keeps a use that follows code in the same cell | pass | |
| imports → it refuses one cell binding the same alias to two names | pass | |
| imports → it allows one cell repeating the same import, which is harmless | pass | |
| imports → it lets a later cell rebind an alias, with the new one winning | pass | |
| imports → it does not fatal when a cell rebinds an alias an earlier cell bound | pass | |
| namespace → it refuses a namespace declaration, naming the limitation | pass | |
| namespace → it refuses a braced namespace too | pass | |
| redeclaration → it reports every kind of top-level declaration on the result | pass | |
| redeclaration → it reports nothing for a cell that declares nothing | pass | |
| redeclaration → it does not put the names into the session, which is the executor's job | pass | |
| redeclaration → it refuses a class the session already holds, with the name in the message | pass | |
| redeclaration → it refuses a function the session already holds | pass | |
| redeclaration → it refuses an interface that collides with a class, because PHP shares the namespace | pass | |
| redeclaration → it refuses an enum that collides with a trait, for the same reason | pass | |
| redeclaration → it matches a held name case-insensitively, as PHP does | pass | |
| redeclaration → it allows a function and a class of the same name, which PHP allows | pass | |
| redeclaration → it refuses a name the one cell declares twice | pass | |
| redeclaration → it refuses a function the one cell declares twice | pass | |
| redeclaration → it reports the line the colliding declaration is on | pass | |
| redeclaration → it leaves a conditional declaration alone, because PHP does not hoist it | pass | |
| redeclaration → it accepts the same conditional declaration twice | pass | |
| redeclaration → it does not treat a method of a class as a declaration | pass | |
| redeclaration → it does not treat an anonymous class as a declaration | pass | |
| committing declarations → it reserves nothing for a cell that was cleaned but never run | pass | |
| committing declarations → it does not reserve a name the cell failed to declare, so the typo can be fixed | pass | |
| committing declarations → it does the same for a class whose interface does not exist | pass | |
| committing declarations → it reserves a name the cell declared before it threw, because PHP hoists it | pass | |
| committing declarations → it reserves a function only once it exists | pass | |
| committing declarations → it lets a subclass run again once the base class it needed arrives | pass | |
| committing declarations → it commits nothing for a cell that declares nothing | pass | |
| capturing the cell's value → it assigns a trailing expression into the reserved variable | pass | |
| capturing the cell's value → it captures a call, which is the common notebook case | pass | |
| capturing the cell's value → it captures only the last statement | pass | |
| capturing the cell's value → it does not capture an assignment | pass | |
| capturing the cell's value → it does not capture a compound assignment | pass | |
| capturing the cell's value → it does not capture a reference assignment | pass | |
| capturing the cell's value → it does not capture an increment or a decrement | pass | |
| capturing the cell's value → it does not capture echo, which is a statement rather than an expression | pass | |
| capturing the cell's value → it does not capture print | pass | |
| capturing the cell's value → it does not capture a declaration | pass | |
| capturing the cell's value → it does not capture a control structure | pass | |
| capturing the cell's value → it does not capture a return | pass | |
| capturing the cell's value → it captures a value that eval really does leave in the variable | pass | |
| capturing the cell's value → it does not shift any line when it inserts the capture | pass | |
| cells with nothing in them → it accepts an empty cell | pass | |
| cells with nothing in them → it accepts a cell of only whitespace | pass | |
| cells with nothing in them → it accepts a cell of only a comment, and keeps it | pass | |
| cells with nothing in them → it accepts a Quarto cell option, which is a PHP comment | pass | |
| the line mapping → it maps a source line back to the cell line it came from | pass | |
| the line mapping → it keeps the mapping exact for every line of a cell, however it was edited | pass | |
| the line mapping → it holds for a real traceback line that eval reports | pass | |
| a ten-cell round trip → it keeps the prelude line count correct as the import set grows | pass | |
| a ten-cell round trip → it produces source that evals cleanly at every step of such a session | pass | |
| Unit/Engine/ExecutorTest | terminal events → it ends a cell with nothing to show in a single ok | pass |
| terminal events → it emits the result immediately before ok, carrying the execution count | pass | |
| terminal events → it emits no result for a cell the cleaner found no value in | pass | |
| terminal events → it ends a thrown cell in a single error and no ok | pass | |
| terminal events → it reports a refused cell as an error without running anything | pass | |
| terminal events → it reports a parse error with the cleaner's ename and the cell's line | pass | |
| terminal events → it reports a refusal that has no line without inventing one | pass | |
| variables between cells → it keeps scalars, arrays and objects | pass | |
| variables between cells → it keeps the same object, not a copy of it | pass | |
| variables between cells → it keeps a resource open across cells | pass | |
| variables between cells → it keeps functions and classes a cell declared | pass | |
| variables between cells → it removes a variable a cell unset, rather than merging it back | pass | |
| variables between cells → it keeps the variables a cell bound before it threw | pass | |
| variables between cells → it never leaks the reserved result variable into the session | pass | |
| variables between cells → it never leaks any reserved variable into the session | pass | |
| variables between cells → it does not carry a reference across cells, which no PHP REPL does | pass | |
| variables between cells → it starts a cell with the session already in scope | pass | |
| output → it turns what a cell prints into a stdout stream event | pass | |
| output → it emits each print separately and in order | pass | |
| output → it emits output as it is produced, not when the cell ends | pass | |
| output → it delivers output before the error of a cell that printed and then threw | pass | |
| output → it does not let the cell's output reach the real stdout | pass | |
| output → it closes a buffer the cell left open, without taking any outer one | pass | |
| output → it survives a cell that closes more buffers than it opened | pass | |
| diagnostics → it turns a warning into a stderr stream event, not a result | pass | |
| diagnostics → it turns a deprecation into a stderr stream event | pass | |
| diagnostics → it turns a user notice into a stderr stream event | pass | |
| diagnostics → it drops a diagnostic the cell suppressed with @ | pass | |
| diagnostics → it still ends a warning-producing cell with ok | pass | |
| diagnostics → it names the cell line a warning came from | pass | |
| strict types → it enforces strict types, so a weak argument is a TypeError | pass | |
| strict types → it still accepts a correctly typed argument | pass | |
| committing declarations → it holds a class a cell declared before it threw, and refuses the cell again | pass | |
| committing declarations → it does not hold a class whose parent was missing, and runs the corrected cell | pass | |
| committing declarations → it commits after a cell that was interrupted or failed for any reason | pass | |
| display → it publishes a value mid-cell as a display event | pass | |
| display → it publishes an already-formatted bundle without formatting it again | pass | |
| display → it defaults a published bundle's metadata to empty | pass | |
| display → it routes publish() through publishBundle, so one place builds the event | pass | |
| display → it takes metadata for a published value | pass | |
| display → it uses an injected formatter for a result instead of the fallback | pass | |
| display → it uses an injected formatter for a published value too | pass | |
| display → it describes the common value kinds in its fallback | pass | |
| a value of null → it publishes no result, and still ends in ok | pass | |
| a value of null → it publishes nothing for a void call as the last statement | pass | |
| a value of null → it leaves var_dump with its output and no result | pass | |
| a value of null → it still advances the execution count | pass | |
| a value of null → it publishes every other falsy value, which are values a cell meant | pass | |
| the session object → it uses a session it was given, so the transport can share one | pass | |
| the session object → it carries accumulated imports into a later cell | pass | |
| in a child process → it runs cells and emits the same events there | pass | |
| in a child process → it reports an exhausted memory limit from the shutdown handler | pass | |
| in a child process → it reports an interrupt and leaves the session usable | pass | |
| in a child process → it keeps the variables an interrupted cell had already bound | pass | |
| in a child process → it is not swallowed by a cell's own catch (Exception) | pass | |
| in a child process → it ignores a signal that arrives between cells rather than dying | pass | |
| a cell that ends the process → it reports a bare exit as a terminal error | pass | |
| a cell that ends the process → it reports exit(0) the same way | pass | |
| a cell that ends the process → it reports exit with a non-zero code, and the code still reaches the parent | pass | |
| a cell that ends the process → it delivers what die() printed before the terminal error | pass | |
| a cell that ends the process → it stops the cells after it, since the process is gone | pass | |
| a cell that ends the process → it says nothing extra on an ordinary shutdown | pass | |
| a cell that ends the process → it says nothing when the shutdown is one the engine asked for | pass | |
| a cell that ends the process → it still reports an unmarked exit, so the mark is what makes the difference | pass | |
| a cell that ends the process → it still delivers what a cell printed before an expected shutdown | pass | |
| a cell that ends the process → it still reports a fatal even when a shutdown was expected | pass | |
| a cell that ends the process → it reports a fatal from a cell that had already printed something | pass | |
| a cell that ends the process → it reports a fatal after a cell has printed megabytes | pass | |
| a cell that ends the process → it reports a fatal whose own allocation was a small one | pass | |
| a cell that ends the process → it still reports a fatal as a fatal rather than as an exit | pass | |
| Unit/Engine/SessionTest | imports → it starts with nothing imported | pass |
| imports → it renders a class import, and leaves out a redundant alias | pass | |
| imports → it renders function and constant imports with their keyword | pass | |
| imports → it renders an unnamespaced import | pass | |
| imports → it keeps the statements sorted, so one session always prints the same | pass | |
| imports → it replaces an earlier binding of the same alias | pass | |
| imports → it keeps the three use types apart, as PHP does | pass | |
| imports → it treats a class alias case-insensitively, as PHP does | pass | |
| imports → it treats a function alias case-insensitively, as PHP does | pass | |
| imports → it treats a constant alias case-sensitively, as PHP does | pass | |
| imports → it reports nothing for an alias it does not hold | pass | |
| declared names → it starts with nothing declared | pass | |
| declared names → it remembers a class-like name and keeps the spelling the cell used | pass | |
| declared names → it matches a class-like name case-insensitively, as PHP does | pass | |
| declared names → it matches a function name case-insensitively, as PHP does | pass | |
| declared names → it keeps class-like names and functions apart, because PHP does | pass | |
| declared names → it holds one entry for a name declared twice | pass | |
| committing declarations → it records a name that exists in the runtime | pass | |
| committing declarations → it ignores a name that does not exist, however valid the cell was | pass | |
| committing declarations → it records an interface, a trait and an enum, not only a class | pass | |
| committing declarations → it records a function that exists | pass | |
| committing declarations → it ignores a function that does not exist | pass | |
| committing declarations → it does not count a name only the autoloader could find | pass | |
| committing declarations → it commits nothing for a cell that declares nothing | pass | |
| Unit/Engine/TracebackTest | a throw from cell code → it reports the cell and the line the user wrote | pass |
| a throw from cell code → it subtracts the prelude, however long it has grown | pass | |
| a throw from cell code → it reports the class of the thrown value as the ename line | pass | |
| a throw from cell code → it reports an Error as readily as an Exception | pass | |
| a throw from cell code → it stops at the executor, leaving juphyter's own frames out | pass | |
| a throw from a function an earlier cell declared → it maps the throw to the cell that declared the function | pass | |
| a throw from a function an earlier cell declared → it uses the declaring cell's prelude, not the running cell's | pass | |
| a throw from a function an earlier cell declared → it maps a method of a class an earlier cell declared | pass | |
| a throw from a function an earlier cell declared → it maps a static method too | pass | |
| a throw from a function an earlier cell declared → it maps a chain through two earlier cells | pass | |
| a throw from inside vendor code → it leaves a vendor frame as its own file and line | pass | |
| a throw from inside vendor code → it rewrites the cell frame while leaving the vendor frames alone | pass | |
| a throw from inside vendor code → it names the function a vendor frame was called from | pass | |
| a throw from inside vendor code → it does not report an internal function's location twice | pass | |
| a frame no cell claims → it says what a closure is without claiming to know where | pass | |
| a frame no cell claims → it never shows the closure's raw name, whatever this PHP calls it | pass | |
| a frame no cell claims → it treats a closure declared and called in one cell the same way | pass | |
| a frame no cell claims → it treats an arrow function the same way | pass | |
| a frame no cell claims → it still places the cell frame that called the closure | pass | |
| an interrupt → it reports KeyboardInterrupt rather than the exception class | pass | |
| an interrupt → it does not show the kernel's own signal handler or its path | pass | |
| a refused cell → it reads as the refusal and the line the user wrote | pass | |
| a refused cell → it omits the location when the refusal has no line | pass | |
| describing one location → it returns null for a file that is not evaluated code | pass | |
| describing one location → it recognises evaluated code by its file name | pass | |
| describing one location → it maps a line through the running cell when nothing else claims it | pass | |
| describing one location → it is what a warning's location comes from | pass | |
| Unit/Install/DoctorCommandTest | a healthy machine → it reports every check and says both front ends are ready | pass |
| a healthy machine → it names every usable kernel in the verdict | pass | |
| a healthy machine → it changes nothing it looked at | pass | |
| Quarto → it says it is not installed rather than not ready | pass | |
| Quarto → it fails when the interpreter Quarto will run has no Jupyter, and gives the PATH entry | pass | |
| Quarto → it takes the PATH entry from a working kernel when no interpreter is usable now | pass | |
| a Python that cannot host the kernel → it fails and prints the command that fixes it | pass | |
| a Python that cannot host the kernel → it fails an ipykernel older than the kernel needs | pass | |
| kernels whose recorded paths have moved → it names a PHP that no longer runs | pass | |
| kernels whose recorded paths have moved → it names an engine a deleted project took with it | pass | |
| kernels whose recorded paths have moved → it names a Python wrapper that is gone | pass | |
| kernels whose recorded paths have moved → it names a Python that lost its ipykernel | pass | |
| what it leaves alone → it ignores a kernel juphyter did not write | pass | |
| what it leaves alone → it says so when nothing is installed, and what to run | pass | |
| what it leaves alone → it says to install a Python first when there is not one either | pass | |
| the command surface → it prints its own help for --help and -h | pass | |
| the command surface → it documents --prefix and the exit code | pass | |
| the command surface → it rejects an unknown option | pass | |
| the command surface → it is dispatched as "doctor" and describes itself | pass | |
| it treats a default Python that cannot host a kernel as advisory once kernels work | pass | |
| Unit/Install/InstallCommandTest | writing the kernelspec → it writes one under the Jupyter data directory | pass |
| writing the kernelspec → it writes one under a prefix, as jupyter kernelspec install does | pass | |
| writing the kernelspec → it takes an option as --name=value too | pass | |
| writing the kernelspec → it reports where it went | pass | |
| writing the kernelspec → it creates the kernels directory when it is not there yet | pass | |
| writing the kernelspec → it adds JUPHYTER_AUTOLOAD when --autoload is given | pass | |
| writing the kernelspec → it lowercases the kernel name | pass | |
| writing the kernelspec → it refuses a name it cannot use, before writing anything | pass | |
| several PHP versions side by side → it keeps two kernels apart by --name and --display-name | pass | |
| several PHP versions side by side → it replaces its own kernelspec when the same name is installed again | pass | |
| several PHP versions side by side → it leaves a kernelspec it did not write alone | pass | |
| the Python interpreter → it names the interpreter the probe chose | pass | |
| the Python interpreter → it writes nothing and prints the pip command when ipykernel is missing | pass | |
| the Python interpreter → it writes nothing when ipykernel is too old for the wrapper | pass | |
| the Python interpreter → it writes nothing when the interpreter does not run | pass | |
| the PHP binary → it defaults to the PHP running the command, without starting a process | pass | |
| the PHP binary → it records the default PHP under the PATH symlink that reaches it, as Homebrew needs | pass | |
| the PHP binary → it keeps the binary when no PATH entry reaches it | pass | |
| the PHP binary → it looks only for the same name, so php8.3 is never swapped for php | pass | |
| the PHP binary → it validates --php by running it, and takes the path it reports | pass | |
| the PHP binary → it keeps an absolute --php exactly as it was given | pass | |
| the PHP binary → it resolves a bare name through the binary it reports | pass | |
| the PHP binary → it resolves a relative path through the binary it reports | pass | |
| the PHP binary → it probes the path it was given, and not the one that comes back | pass | |
| the PHP binary → it writes nothing when --php does not run | pass | |
| the PHP binary → it writes nothing when --php runs but reports no version | pass | |
| running from a PHAR → it names the archive as the engine and copies the wrapper out of it | pass | |
| running from a PHAR → it refuses an archive with no wrapper module in it | pass | |
| the missing Python wrapper → it fails and writes nothing when the package has no wrapper module | pass | |
| the missing Python wrapper → it fails when the module directory is there but the module is not | pass | |
| the missing Python wrapper → it asks nothing of Python when the wrapper is missing | pass | |
| the missing Python wrapper → it installs when the module is there | pass | |
| the command surface → it is dispatched as "install" and describes itself without being built | pass | |
| the command surface → it prints its own help for --help and -h | pass | |
| the command surface → it rejects an unknown option and shows the usage | pass | |
| the command surface → it rejects an argument that is not an option | pass | |
| the command surface → it rejects an option with no value | pass | |
| the command surface → it is built by make() without arguments, the way the dispatcher builds it | pass | |
| the command surface → it resolves its package root to this checkout outside a PHAR | pass | |
| the command surface → it writes a kernelspec that uninstall recognizes as juphyter's | pass | |
| the stop button → it says nothing when the target PHP can handle signals | pass | |
| the stop button → it warns and names the binary when it cannot, and still installs | pass | |
| the stop button → it names the path the user gave in the warning, not the one PHP reports | pass | |
| the stop button → it still reports the install on stdout alongside the warning | pass | |
| the stop button → it asks the target PHP the same question Interrupt::available() asks | pass | |
| the stop button → it says nothing when an older probe answers with only two lines | pass | |
| the stop button → it checks the running PHP when no --php is given | pass | |
| the interpreter Quarto will use → it says nothing about Quarto when quarto is not on PATH | pass | |
| the interpreter Quarto will use → it names the interpreter and its jupyter_client when it has one | pass | |
| the interpreter Quarto will use → it reports no Jupyter, warns, and installs anyway | pass | |
| the interpreter Quarto will use → it leaves the kernelspec naming the recorded interpreter, not Quarto's | pass | |
| the interpreter Quarto will use → it takes QUARTO_PYTHON over the python3 on PATH | pass | |
| the interpreter Quarto will use → it reports a QUARTO_PYTHON that does not run, and still installs | pass | |
| the interpreter Quarto will use → it reports that there is no python3 on PATH for Quarto to use | pass | |
| the interpreter Quarto will use → it suggests no PATH entry when the recorded interpreter has no Jupyter either | pass | |
| the interpreter Quarto will use → it asks the resolved interpreter for jupyter_client and nothing else | pass | |
| the Quarto check against a real interpreter → it warns and names the Jupyter-less python3 that is first on PATH | pass | |
| building a Python when none can host the kernel → it offers to build one and installs into it when the answer is yes | pass | |
| building a Python when none can host the kernel → it does not ask when --create-venv names a directory | pass | |
| building a Python when none can host the kernel → it says what it is building, because installing it takes a minute | pass | |
| building a Python when none can host the kernel → it declines without a terminal and names the option that would do it | pass | |
| building a Python when none can host the kernel → it does not offer to build when --python named an interpreter | pass | |
| building a Python when none can host the kernel → it has no default directory to offer when HOME is unset | pass | |
| building a Python when none can host the kernel → it lists --create-venv in its help | pass | |
| shell startup files → it writes to none, and offers no option that would | pass | |
| shell startup files → it rejects --path-file, which it no longer has | pass | |
| Unit/Install/JupyterPathsTest | the data directory → it uses JUPYTER_DATA_DIR when it is set | pass |
| the data directory → it ignores an empty JUPYTER_DATA_DIR | pass | |
| the data directory → it drops a trailing separator | pass | |
| the data directory → it puts it under ~/Library/Jupyter on macOS | pass | |
| the data directory → it puts it under XDG_DATA_HOME on Linux | pass | |
| the data directory → it falls back to ~/.local/share on Linux without XDG_DATA_HOME | pass | |
| the data directory → it treats BSD like Linux | pass | |
| the data directory → it follows JUPYTER_PLATFORM_DIRS to the platformdirs location | pass | |
| the data directory → it reads the words jupyter_core reads as false | pass | |
| the data directory → it refuses Windows, which the kernel cannot run on | pass | |
| the data directory → it says what to do when HOME is not set | pass | |
| the kernels directory → it sits inside the data directory | pass | |
| the kernels directory → it moves under a prefix when one is given | pass | |
| the kernels directory → it ignores an empty prefix | pass | |
| the kernels directory → it does not need HOME or a platform when a prefix is given | pass | |
| the kernels directory → it names one kernelspec after the kernel | pass | |
| the kernels directory → it collapses a doubled separator from a prefix with a trailing slash | pass | |
| it reads the running process in fromEnvironment | pass | |
| Unit/Install/KernelSpecTest | the kernel name → it lowercases a name and trims it | pass |
| the kernel name → it allows dots, dashes, and underscores | pass | |
| the kernel name → it rejects a name Jupyter cannot select with data set "('')" | pass | |
| the kernel name → it rejects a name Jupyter cannot select with data set "(' ')" | pass | |
| the kernel name → it rejects a name Jupyter cannot select with data set "('php 85')" | pass | |
| the kernel name → it rejects a name Jupyter cannot select with data set "('php/85')" | pass | |
| the kernel name → it rejects a name Jupyter cannot select with data set "('../escape')" | pass | |
| the kernel name → it rejects a name Jupyter cannot select with data set "('.hidden')" | pass | |
| the kernel name → it rejects a name Jupyter cannot select with data set "('-leading-dash')" | pass | |
| the kernel name → it rejects a name Jupyter cannot select with data set "('php:85')" | pass | |
| the kernel name → it rejects a name Jupyter cannot select with data set "('php*')" | pass | |
| the kernel name → it keeps a name out of the parent directory | pass | |
| the document → it holds the keys Jupyter reads, in the plan's order | pass | |
| the document → it leaves {connection_file} for Jupyter to substitute | pass | |
| the document → it interrupts by signal, because the wrapper forwards SIGINT | pass | |
| the document → it adds JUPHYTER_AUTOLOAD only when an autoloader is given | pass | |
| the document → it reports its own name | pass | |
| the JSON → it round-trips through json_decode | pass | |
| the JSON → it is indented, unescaped, and ends with a newline | pass | |
| recognizing juphyter's own kernelspec → it recognizes what it wrote | pass | |
| recognizing juphyter's own kernelspec → it does not claim a kernelspec from another kernel with data set "dataset "a python kernel"" | pass | |
| recognizing juphyter's own kernelspec → it does not claim a kernelspec from another kernel with data set "dataset "another PHP kernel"" | pass | |
| recognizing juphyter's own kernelspec → it does not claim a kernelspec from another kernel with data set "dataset "a PHP kernel with an empty engine"" | pass | |
| recognizing juphyter's own kernelspec → it does not claim a kernelspec from another kernel with data set "dataset "a PHP kernel whose env is not a map"" | pass | |
| recognizing juphyter's own kernelspec → it does not claim a kernelspec from another kernel with data set "dataset "nothing at all"" | pass | |
| recognizing juphyter's own kernelspec → it does not claim a kernelspec from another kernel with data set "dataset "a string"" | pass | |
| recognizing juphyter's own kernelspec → it does not claim a kernelspec from another kernel with data set "dataset "an empty document"" | pass | |
| Unit/Install/PharTest | the build → it produces a runnable archive | pass |
| the build → it carries the production dependencies and not the dev ones | pass | |
| the build → it carries the Python wrapper, which a PHAR install copies out of it | pass | |
| the build → it leaves compiled Python bytecode out, however the checkout looks | pass | |
| the build → it drops the shebang of bin/juphyter, which would otherwise be printed | pass | |
| dispatching from the archive → it lists the commands it holds, because scandir() reads phar:// paths | pass | |
| dispatching from the archive → it lists every command the checkout lists | pass | |
| dispatching from the archive → it rejects an unknown command with usage and a non-zero code | pass | |
| dispatching from the archive → it dispatches a command it holds | pass | |
| installing from the archive → it writes a kernelspec that names the archive as the engine | pass | |
| installing from the archive → it copies the Python wrapper out of the archive | pass | |
| installing from the archive → it copies the real wrapper out, so the installed kernel can start | pass | |
| installing from the archive → it prints the pip command when a named interpreter has no ipykernel | pass | |
| installing from the archive → it builds an environment rather than touching an interpreter it was not given | pass | |
| installing from the archive → it uninstalls what it installed | pass | |
| Unit/Install/PythonProbeTest | choosing an interpreter → it asks the interpreter --python names for its own path | pass |
| choosing an interpreter → it reads the shebang of the jupyter on PATH | pass | |
| choosing an interpreter → it follows a /usr/bin/env shebang back to PATH | pass | |
| choosing an interpreter → it skips env options and assignments in a shebang | pass | |
| choosing an interpreter → it reads a quoted shebang path, which pip writes when the path holds a space | pass | |
| choosing an interpreter → it falls back to python3 when the shebang names something else | pass | |
| choosing an interpreter → it falls back to python3 when the shebang points at a missing interpreter | pass | |
| choosing an interpreter → it falls back to python when there is no python3 | pass | |
| choosing an interpreter → it skips a jupyter that is not executable | pass | |
| choosing an interpreter → it says what is missing when PATH holds no jupyter and no python | pass | |
| choosing an interpreter → it reports an interpreter that does not run, with its own message | pass | |
| choosing an interpreter → it reports an interpreter that exits cleanly and says nothing | pass | |
| requiring ipykernel → it returns the version it finds | pass | |
| requiring ipykernel → it asks the interpreter to import it | pass | |
| requiring ipykernel → it reports no version when the import fails | pass | |
| requiring ipykernel → it prints the exact install command when ipykernel is missing | pass | |
| requiring ipykernel → it refuses an ipykernel below the floor the wrapper needs | pass | |
| requiring ipykernel → it accepts the floor itself and anything above it | pass | |
| requiring ipykernel → it names the floor in one place | pass | |
| the default runner → it runs a process and returns its code and both streams | pass | |
| the default runner → it reports a path that is not there rather than throwing | pass | |
| the default runner → it reports an empty command | pass | |
| the default runner → it passes arguments without a shell, so nothing needs quoting | pass | |
| Unit/Install/QuartoProbeTest | whether Quarto is installed → it finds quarto on PATH | pass |
| whether Quarto is installed → it reports none when PATH has no quarto | pass | |
| whether Quarto is installed → it ignores a quarto that is not executable | pass | |
| resolving the interpreter → it takes the first python3 on PATH | pass | |
| resolving the interpreter → it takes QUARTO_PYTHON over the python3 on PATH | pass | |
| resolving the interpreter → it ignores an empty QUARTO_PYTHON | pass | |
| resolving the interpreter → it resolves the candidate to its own sys.executable | pass | |
| resolving the interpreter → it has nothing to resolve when PATH holds no python3 | pass | |
| resolving the interpreter → it does not fall back to python | pass | |
| a candidate that cannot be asked anything → it reports a QUARTO_PYTHON that is not there | pass | |
| a candidate that cannot be asked anything → it reports an exit code when the interpreter said nothing | pass | |
| a candidate that cannot be asked anything → it reports an interpreter that ran and printed no path | pass | |
| the jupyter_client check → it reports the version when it imports | pass | |
| the jupyter_client check → it reports none when it does not import | pass | |
| the jupyter_client check → it asks the resolved interpreter, and asks only about jupyter_client | pass | |
| the jupyter_client check → it can be asked of any interpreter | pass | |
| Unit/Install/UninstallCommandTest | removing a kernelspec → it removes one juphyter wrote, under a prefix | pass |
| removing a kernelspec → it removes one under the Jupyter data directory | pass | |
| removing a kernelspec → it removes the copy of the Python wrapper a PHAR install left inside it | pass | |
| removing a kernelspec → it removes only the kernel named | pass | |
| removing a kernelspec → it lowercases the name it is given | pass | |
| removing a kernelspec → it unlinks a symlink inside the kernelspec rather than following it | pass | |
| refusing to remove → it leaves a kernelspec juphyter did not write | pass | |
| refusing to remove → it leaves a Python kernelspec that happens to share the name | pass | |
| refusing to remove → it leaves a kernel.json it cannot parse | pass | |
| refusing to remove → it says there is nothing there when the kernel is not installed | pass | |
| refusing to remove → it says there is nothing there when the directory holds no kernel.json | pass | |
| refusing to remove → it refuses a name it cannot use | pass | |
| the command surface → it is dispatched as "uninstall" and describes itself without being built | pass | |
| the command surface → it prints its own help for --help and -h | pass | |
| the command surface → it rejects an unknown option, a stray argument, and a missing value | pass | |
| the command surface → it does not take install's options | pass | |
| the command surface → it is built by make() without arguments, the way the dispatcher builds it | pass | |
| Unit/Install/VenvBuilderTest | building an environment → it creates one and installs the packages into its own interpreter | pass |
| building an environment → it adopts an environment that already runs rather than rebuilding it | pass | |
| building an environment → it installs exactly the documented packages | pass | |
| building an environment → it drops a trailing slash from the directory it was given | pass | |
| refusing to build → it refuses a directory that exists and holds something | pass | |
| refusing to build → it builds into an empty directory that is already there | pass | |
| refusing to build → it refuses an empty directory name | pass | |
| refusing to build → it says so when PATH holds no Python to build with | pass | |
| reporting a failure → it reports why venv failed | pass | |
| reporting a failure → it reports a venv that claimed success and left nothing that runs | pass | |
| reporting a failure → it reports why pip failed, keeping the last lines of it | pass | |
| reporting a failure → it falls back to the exit code when a failure said nothing | pass | |
| the default directory → it is under HOME | pass | |
| the default directory → it collapses a trailing slash on HOME | pass | |
| the default directory → it is null when HOME is unset, so the caller asks instead of guessing | pass | |
| Unit/Project/AutoloadTest | searching upwards → it finds an autoloader beside the notebook | pass |
| searching upwards → it walks up until it finds one | pass | |
| searching upwards → it takes the nearest one, because a package inside a monorepo means the package | pass | |
| searching upwards → it finds nothing when there is nothing, which is not an error | pass | |
| searching upwards → it ignores a vendor directory with no autoloader in it | pass | |
| searching upwards → it does not fall over on a directory that is not there | pass | |
| precedence → it prefers the environment over everything | pass | |
| precedence → it prefers the config over the search | pass | |
| precedence → it falls back to the search when nothing names one | pass | |
| precedence → it reports finding nothing, rather than failing | pass | |
| precedence → it treats an empty environment variable as unset | pass | |
| an autoloader that was named but is not there → it refuses a missing path from the environment, naming the variable | pass | |
| an autoloader that was named but is not there → it refuses a missing path from the config, naming the file | pass | |
| an autoloader that was named but is not there → it says where it looked | pass | |
| loading → it requires the file it found | pass | |
| loading → it does nothing when it found nothing | pass | |
| Unit/Project/ConfigTest | finding the file → it reads a juphyter.json beside the notebook | pass |
| finding the file → it walks up, so one file at the top of a project serves its subdirectories | pass | |
| finding the file → it takes the nearest one | pass | |
| finding the file → it finds nothing when there is none | pass | |
| finding the file → it does not fall over on a directory that is not there | pass | |
| reading the file → it reads all three settings | pass | |
| reading the file → it leaves out what the file does not say | pass | |
| reading the file → it ignores a setting of the wrong type or an empty one | pass | |
| reading the file → it resolves a relative autoload path against the file, not the working directory | pass | |
| reading the file → it leaves an absolute autoload path alone | pass | |
| reading the file → it refuses invalid JSON, rather than ignoring a file the user wrote on purpose | pass | |
| reading the file → it refuses a file that is not a JSON object | pass | |
| applying the settings → it sets the memory limit | pass | |
| applying the settings → it prepends the include path rather than replacing it | pass | |
| applying the settings → it changes nothing it was not told to change | pass | |
| Unit/VersionTest | the reported version → it reads this installation out of the Composer record | pass |
| the reported version → it identifies a branch install by its commit | pass | |
| the reported version → it names the tool and the PHP running it, on one line | pass | |
| the reported version → it leaves off the archive path when no archive is running | pass |
Limitations
Exclusions
- The case for syntax newer than the running PHP applies below PHP 8.5; the run uses PHP 8.5.
Reproduction
From the repository root
composer install
composer require --dev --prefer-source "phpstats/phibble:^0.5" "phpstats/phplyr:^0.4" "phpstats/phiz:^0.5"
php -d phar.readonly=0 -d extension=<path to php_polars> vendor/bin/pest
- Raw record
- site/verifications/family-compatibility.json
- Full results
- .github/workflows/family.yml