From 3fa47d75b34d083f649b1fe9e225db008a5b58ab Mon Sep 17 00:00:00 2001 From: Richard Ore Date: Fri, 16 Jun 2023 21:30:23 +0100 Subject: [PATCH 1/5] Moved vendor to apps --- apps/nyssa/app/commands/init.b | 5 ----- apps/nyssa/app/setup.b | 1 - src/config.h.in | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/nyssa/app/commands/init.b b/apps/nyssa/app/commands/init.b index 75889d62..c52523ae 100644 --- a/apps/nyssa/app/commands/init.b +++ b/apps/nyssa/app/commands/init.b @@ -29,10 +29,8 @@ def run(value, options, success, error) { # Declare locations. var here = os.cwd(), test_dir = os.join_paths(here, setup.TEST_DIR), - ex_dir = os.join_paths(here, setup.EXAMPLES_DIR), app_dir = os.join_paths(here, setup.APP_DIR) var test_ignore = test_dir + os.path_separator + '.gitignore', - ex_ignore = ex_dir + os.path_separator + '.gitignore', index = here + os.path_separator + setup.INDEX_FILE, app_index = app_dir + os.path_separator + setup.INDEX_FILE, readme = here + os.path_separator + setup.README_FILE, @@ -58,15 +56,12 @@ def run(value, options, success, error) { # Create tests and examples directory if !os.dir_exists(test_dir) os.create_dir(test_dir) - if !os.dir_exists(ex_dir) os.create_dir(ex_dir) # Create .gitignore files in tests and examples directory for # git compartibility. log.info('Creating required git files') var tf = file(test_ignore, 'w+') tf.open(); tf.close() - var ef = file(ex_ignore, 'w+') - ef.open(); ef.close() # increase Blade visibility by setting the attribute file properties # to allow Github identify it as a Blade project. diff --git a/apps/nyssa/app/setup.b b/apps/nyssa/app/setup.b index 4bd52d8a..28f7df5e 100644 --- a/apps/nyssa/app/setup.b +++ b/apps/nyssa/app/setup.b @@ -7,7 +7,6 @@ var NYSSA_VERSION = '0.0.0' # directories var APP_DIR = 'app' var TEST_DIR = 'tests' -var EXAMPLES_DIR = 'examples' var STATIC_DIR = 'public' var TEMPLATES_DIR = 'templates' var STORAGE_DIR = 'storage' diff --git a/src/config.h.in b/src/config.h.in index 65bb2693..27bd1ba2 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -6,7 +6,7 @@ #define BVM_VERSION "0.1.1" #define LIBRARY_DIRECTORY "libs" #define LIBRARY_DIRECTORY_INDEX "index" -#define PACKAGES_DIRECTORY "vendor" +#define PACKAGES_DIRECTORY "apps" #define LOCAL_PACKAGES_DIRECTORY ".blade" #define LOCAL_EXT_DIRECTORY "/bin" #define LOCAL_SRC_DIRECTORY "/libs" From a8ad5ef842a7fcdcf4d84b206b511f7e331119b7 Mon Sep 17 00:00:00 2001 From: Richard Ore Date: Fri, 16 Jun 2023 22:49:48 +0100 Subject: [PATCH 2/5] Initial commit to add qi to nyssa --- apps/nyssa/app/commands/account.b | 6 +- apps/nyssa/app/commands/clean.b | 2 +- apps/nyssa/app/commands/publish.b | 2 +- apps/nyssa/app/commands/test.b | 38 ++ apps/nyssa/app/index.b | 3 +- apps/nyssa/app/server/views.b | 5 +- .../docs/hosting-a-private-repository.md | 22 +- apps/nyssa/docs/package-layout.md | 2 - apps/nyssa/docs/test-assertions.md | 491 ++++++++++++++++++ apps/nyssa/docs/test-globals.md | 223 ++++++++ apps/nyssa/docs/testing.md | 52 ++ apps/nyssa/public/css/style.css | 2 +- apps/qi/.gitattributes | 2 + apps/qi/.gitignore | 18 + apps/qi/README.md | 70 +++ apps/qi/app/index.b | 10 + apps/qi/app/lib.b | 426 +++++++++++++++ apps/qi/index.b | 1 + apps/qi/nyssa.json | 18 + apps/qi/tests/.gitignore | 0 apps/qi/tests/global.b | 42 ++ apps/qi/tests/sample.spec.b | 12 + apps/qi/tests/sample2.spec.b | 11 + apps/qi/tests/setup.spec.b | 10 + apps/qi/tests/test.b | 105 ++++ qi | 10 + 26 files changed, 1571 insertions(+), 12 deletions(-) create mode 100644 apps/nyssa/app/commands/test.b create mode 100644 apps/nyssa/docs/test-assertions.md create mode 100644 apps/nyssa/docs/test-globals.md create mode 100644 apps/nyssa/docs/testing.md create mode 100644 apps/qi/.gitattributes create mode 100644 apps/qi/.gitignore create mode 100644 apps/qi/README.md create mode 100644 apps/qi/app/index.b create mode 100644 apps/qi/app/lib.b create mode 100644 apps/qi/index.b create mode 100644 apps/qi/nyssa.json create mode 100644 apps/qi/tests/.gitignore create mode 100644 apps/qi/tests/global.b create mode 100644 apps/qi/tests/sample.spec.b create mode 100644 apps/qi/tests/sample2.spec.b create mode 100644 apps/qi/tests/setup.spec.b create mode 100644 apps/qi/tests/test.b create mode 100755 qi diff --git a/apps/nyssa/app/commands/account.b b/apps/nyssa/app/commands/account.b index 1278dc7a..a71e4501 100644 --- a/apps/nyssa/app/commands/account.b +++ b/apps/nyssa/app/commands/account.b @@ -19,9 +19,9 @@ def parse(parser) { type: args.CHOICE, # choices: ['create', 'login', 'logout'], choices: { - create: 'creates a new publisher account', - login: 'login to a publisher account', - logout: 'log out of a publisher account', + create: 'Creates a new publisher account', + login: 'Login to a publisher account', + logout: 'Log out of a publisher account', } } ).add_option( diff --git a/apps/nyssa/app/commands/clean.b b/apps/nyssa/app/commands/clean.b index 3b27961d..8d8d685b 100644 --- a/apps/nyssa/app/commands/clean.b +++ b/apps/nyssa/app/commands/clean.b @@ -6,7 +6,7 @@ import ..log def parse(parser) { parser.add_command( 'clean', - 'Clear Nyssa storage' + 'Clear Nyssa storage and cache' ).add_option( 'cache', 'clean packages cache', diff --git a/apps/nyssa/app/commands/publish.b b/apps/nyssa/app/commands/publish.b index b01f106e..2b8796aa 100644 --- a/apps/nyssa/app/commands/publish.b +++ b/apps/nyssa/app/commands/publish.b @@ -18,7 +18,7 @@ def italics(t) { def parse(parser) { parser.add_command( 'publish', - 'Publishes a repository' + 'Publishes a Blade package to a repository' ).add_option( 'repo', 'repository url', diff --git a/apps/nyssa/app/commands/test.b b/apps/nyssa/app/commands/test.b new file mode 100644 index 00000000..f8bd6eed --- /dev/null +++ b/apps/nyssa/app/commands/test.b @@ -0,0 +1,38 @@ +import os +import io +import iters +import args +import qi +import ..setup + +def parse(parser) { + parser.add_command( + 'test', + 'Run the tests' + ) +} + +def run_test_files(files) { + for f in files { + f = os.join_paths(setup.TEST_DIR, f) + qi.run(f) + } + return qi.show_tests_results() +} + +def run(value, options, success, error) { + if os.dir_exists(setup.TEST_DIR) { + var files = iters.filter(os.read_dir(setup.TEST_DIR), | x | { + return x != '.' and x != '..' and x.ends_with('.b') + }) + if files { + if !run_test_files(files) { + os.exit(1) + } + } else { + error('No test files found.') + } + } else { + error('"${setup.TEST_DIR}" directory not found.') + } +} diff --git a/apps/nyssa/app/index.b b/apps/nyssa/app/index.b index b9f2a175..c8342c83 100644 --- a/apps/nyssa/app/index.b +++ b/apps/nyssa/app/index.b @@ -29,9 +29,9 @@ import .commands.install import .commands.restore # import .commands.doc import .commands.uninstall -# import .commands.update import .commands.publish import .commands.serve +import .commands.test # Import options... import .options.version @@ -47,6 +47,7 @@ var commands = { publish, restore, serve, + test, uninstall, } diff --git a/apps/nyssa/app/server/views.b b/apps/nyssa/app/server/views.b index 5d13ff7e..d8b158bb 100644 --- a/apps/nyssa/app/server/views.b +++ b/apps/nyssa/app/server/views.b @@ -12,12 +12,15 @@ var doc_files = [ 'publishing-packages.md', 'managing-dependencies.md', 'package-layout.md', + 'testing.md', + 'test-globals.md', + 'test-assertions.md', 'install-and-uninstall-actions.md', 'getting-project-info.md', 'hosting-a-private-repository.md', 'publishers-account.md', 'cleaning-cache-and-logs.md', - 'commands.md' + 'commands.md', ] var docs_dir = os.join_paths(os.args[1], setup.DOCS_DIR) diff --git a/apps/nyssa/docs/hosting-a-private-repository.md b/apps/nyssa/docs/hosting-a-private-repository.md index 4fe1a9f6..89c54a3f 100644 --- a/apps/nyssa/docs/hosting-a-private-repository.md +++ b/apps/nyssa/docs/hosting-a-private-repository.md @@ -6,7 +6,7 @@ This means that you can install and configure your own private or custom instanc ### Starting the respository server -Once you have Nyssa installed on a device, you can start the repository server by running the command `nyssa serve`. This starts a local repository server that listens to the port `3000`. You should see a log similar to the following: +Once you have Nyssa installed on a device, you can start the repository server by running the command `nyssa serve`. This starts a local repository server that listens to the port `3000` by default. You should see a log similar to the following: ``` $ nyssa serve @@ -18,9 +18,27 @@ You can visit `http://127.0.0.1:3000` in your browser to see your own clone of t Replace IP address with the address/domain of the server if you are trying to access it outside of the server. In fact, if you'll be accessing it ourside the local machine, it is advisable to change the configured host from `127.0.0.1` to `0.0.0.0`. +You can choose to run your server on another port or change the bind address using the `--port` (or `-p`) and `--host` (or `-n`) flag respectively. + +For example: + +``` +$ nyssa serve --port 4000 +Nyssa repository server started. +Repository URL: http://127.0.0.1:4000 +``` + +The following example binds to `IP Any` (i.e. `0.0.0.0`) and listens on port `8000`. + +``` +$ nyssa serve --host 0.0.0.0 --port 8000 +Nyssa repository server started. +Repository URL: http://localhost:8000 +``` + ### Customizing your installation -You fully customize your own repository setup by modifying the `app/setup.b` file. For exmple, you can change the host and port number by modifying the value of `REPOSITORY_HOST` and `REPOSITORY_PORT` respectively in the file `app/setup.b`. +You fully customize your own repository setup by modifying the `apps/nyssa/app/setup.b` file in your Blade installation. For example, you can change the host and port number from code by modifying the value of `REPOSITORY_HOST` and `REPOSITORY_PORT` respectively in the file `setup.b`. ### Production considerations diff --git a/apps/nyssa/docs/package-layout.md b/apps/nyssa/docs/package-layout.md index f9ac66dd..8c5cc37c 100644 --- a/apps/nyssa/docs/package-layout.md +++ b/apps/nyssa/docs/package-layout.md @@ -11,8 +11,6 @@ follows: installed packages and will only be available when at least one or more package is installed. - `app`: The directory contains the main application/package code. -- `examples`: Examples accompanying your package goes here and _may_ - be shown to users under the `Examples` section of the Nyssa website. - `index.b`: The default Blade package index file. - `README.md`: The README.md file will be contain the main documentation for your package. diff --git a/apps/nyssa/docs/test-assertions.md b/apps/nyssa/docs/test-assertions.md new file mode 100644 index 00000000..4161fa34 --- /dev/null +++ b/apps/nyssa/docs/test-assertions.md @@ -0,0 +1,491 @@ +# Test Assertions + +When writing tests you often need to check that a value meets certain criterias. The `expect()` function gives you access to an array of assertions that lets you test against different conditions. This page describes assertions, how to use them and all possible assertions in Qi. + +## Reference + +- [Test Assertions](#test-assertions) + - [Reference](#reference) + - [expect()](#expect) + - [Modifiers](#modifiers) + - [not()](#not) + - [Matchers](#matchers) + - [to\_be(value)](#to_bevalue) + - [to\_be\_nil()](#to_be_nil) + - [to\_be\_defined()](#to_be_defined) + - [to\_be\_truthy()](#to_be_truthy) + - [to\_be\_falsy()](#to_be_falsy) + - [to\_be\_greater\_than(number)](#to_be_greater_thannumber) + - [to\_be\_greater\_than\_or\_equal(number)](#to_be_greater_than_or_equalnumber) + - [to\_be\_less\_than(number)](#to_be_less_thannumber) + - [to\_be\_less\_than\_or\_equal(number)](#to_be_less_than_or_equalnumber) + - [to\_match(value)](#to_matchvalue) + - [to\_contain(item)](#to_containitem) + - [to\_throw(error)](#to_throwerror) + - [to\_have\_length(number)](#to_have_lengthnumber) + - [to\_be\_instance\_of(class)](#to_be_instance_ofclass) + - [to\_be\_function(value)](#to_be_functionvalue) + - [to\_have\_property(name, value?)](#to_have_propertyname-value) + - [to\_have\_method(name)](#to_have_methodname) + - [to\_have\_decorator(name)](#to_have_decoratorname) + - [to\_be\_boolean()](#to_be_boolean) + - [to\_be\_number()](#to_be_number) + - [to\_be\_string()](#to_be_string) + - [to\_be\_list()](#to_be_list) + - [to\_be\_dict()](#to_be_dict) + - [to\_be\_class()](#to_be_class) + - [to\_be\_iterable()](#to_be_iterable) + - [to\_be\_file()](#to_be_file) + - [to\_be\_bytes()](#to_be_bytes) + + +## expect() + +The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. Instead, you will use `expect` along with a "matcher" function to assert something about a value. Let's say you have a method `name_of_app()` which is supposed to return the string `'qi'`. Here's how you would test that: + +```py +describe('Name of app test', || { + it('should be qi', || { + expect(name_of_app()).to_be('qi') + }) +}) +``` + +In this case, `to_be` is the matcher function. There are a lot of different matcher functions, documented below, to help you test different things. + +The argument to `expect` should be the value that your code produces, and any argument to the matcher should be the correct value. If you mix them up, your tests will still work, but the reporting messages show after running tests will look strange. + +## Modifiers + +### not() + +If you know how to test something, `.not()` lets you test its opposite. For example, this code tests that the name of the application is `not` `'qi'`. + +```py +describe('Name of app test', || { + it('should be qi', || { + expect(name_of_app()).not().to_be('qi') + }) +}) +``` + +## Matchers + +> **NOTE:** +> +> Matchers can be nested. For example, +> +> ```py +> expect(10.5).to_be_number().to_be_less_than(20) +> ``` + + +### to_be(value) + +Use `.to_be` to compare primitive values or to check referential identity of object instances. For example, this code will validate some properties of the `can` object: + +```py +var can = { + name: 'pamplemousse', + ounces: 12, +} + +describe('the can', || { + it('has 12 ounces', || { + expect(can.ounces).to_be(12) + }) + + it('has a sophisticated name', || { + expect(can.name).to_be('pamplemousse') + }) +}) +``` + +### to_be_nil() + +`.to_be_nil()` is the same as `.to_be(nil)` but the error messages are a bit nicer. So use `.to_be_nil()` when you want to check that something is nil. + +```py +def bloop() { + return nil +} + +it('should return nil', || { + expect(bloop()).to_be_nil() +}) +``` + +### to_be_defined() + +Use `.to_be_defined` to check that a variable is not `nil`. For example, if you want to check that a function `fetch_new_flavor_idea()` returns something, you can write: + +```py +expect(fetch_new_flavor_idea()).to_be_defined() +``` + +You could also write `expect(fetch_new_flavor_idea()).not().to_be_nil()` as they are identical, but it's better practice to use the direct method. + +### to_be_truthy() + +Use `.to_be_truthy` when you don't care what a value is and you want to ensure a value is true in a boolean context. For example, let's say you have some application code that looks like: + +```py +drink_some_lacroix() +if (thirsty()) { + drink_more_lacroix() +} +``` + +You may not care what `get_errors` returns, specifically - it might return `true`, `[1]`, or anything that's true in Blade, and your code would still work. So if you want to test you are thirsty before drinking some La Croix, you could write: + +```py +it('should be thirsty before drinking La Croix', || { + drink_some_lacroix() + expect(thirsty()).to_be_truthy() +}) +``` + +### to_be_falsy() + +Use `.to_be_falsy` when you don't care what a value is and you want to ensure a value is false in a boolean context. For example, let's say you have some application code that looks like: + +```py +drink_some_lacroix() +if (!get_errors()) { + drink_more_lacroix() +} +``` + +You may not care what `get_errors` returns, specifically - it might return `false`, `nil`, or `-1`, and your code would still work. So if you want to test there are no errors after drinking some La Croix, you could write: + +```py +it('does not lead to errors when drinking La Croix', || { + drink_some_lacroix() + expect(get_errors()).to_be_falsy() +}) +``` + +### to_be_greater_than(number) + +Use `.to_be_greater_than` to compare `received > expected` for number or `received.length() > expected` for string. For example, test that `ounces_per_can()` returns a value of more than 10 ounces: + +```py +it('is more than 10 ounces per can', || { + expect(ounces_per_can()).to_be_greater_than(10) +}) +``` + +### to_be_greater_than_or_equal(number) + +Use `.to_be_greater_than_or_equal` to compare `received >= expected` for number or `received.length() >= expected` for string. For example, test that `ounces_per_can()` returns a value of more than or equal to 10 ounces: + +```py +it('is more than or equal to 10 ounces per can', || { + expect(ounces_per_can()).to_be_greater_than_or_equal(10) +}) +``` + +### to_be_less_than(number) + +Use `.to_be_less_than` to compare `received < expected` for number or `received.length() < expected` for string. For example, test that `ounces_per_can()` returns a value of less than 10 ounces: + +```py +it('is less than 10 ounces per can', || { + expect(ounces_per_can()).to_be_less_than(10) +}) +``` + +### to_be_less_than_or_equal(number) + +Use `.to_be_less_than_or_equal` to compare `received <= expected` for number or `received.length() <= expected` for string. For example, test that `ounces_per_can()` returns a value of less than or equal to 10 ounces: + +```py +it('is less than or equal to 10 ounces per can', || { + expect(ounces_per_can()).to_be_less_than_or_equal(10) +}) +``` + +### to_match(value) + +Use `.to_match` to check that a string matches a regular expression. + +For example, you might not know what exactly `essay_on_the_best_flavor()` returns, but you know it's a really long string, and the substring grapefruit should be in there somewhere. You can test this with: + +```py +describe('an essay on the best flavor', || { + it('mentions grapefruit', || { + expect(essay_on_the_best_flavor()).to_match('/grapefruit/i') + }) +}) +``` + +This matcher also accepts a string, which it will try to match: + +```py +describe('grapefruits', || { + it('should be a grape', || { + expect('grapefruits').to_match('grape') + }) +}) +``` + +### to_contain(item) + +Use `.to_contain` when you want to check that an item is in an list or dictionary or whether a string is a substring of another string. + +For example, if `get_all_flavors()` returns an list of flavors and you want to be sure that lime is in there, you can write: + +```py +it('should contain lime', || { + expect(get_all_flavors()).to_contain('lime') +}) +``` + +### to_throw(error) + +Use `.to_throw` to test that a function throws when it is called. For example, if we want to test that `drink_flavor('octopus')` throws, because octopus flavor is too disgusting to drink, we could write: + +```py +it('throws on octopus', || { + expect(|| { + drink_flavor('octopus') + }).to_throw() +}) +``` + +> **NOTE:** +> +> You must wrap the code in a function, otherwise the error will not be caught and the assertion will fail. + +You can provide an optional argument to test that a specific error is thrown: + +- string: error message **includes** the substring +- regular expression: error message **matches** the pattern +- error object: error message is **equal to** the message property of the object +- error class: error object is **instance of** class + +For example, let's say `drink_flavor()` looks like this: + +```py +def drink_flavor(flavor) { + if flavor == 'octopus' { + die DisgustingFlavorError('yuck, octopus flavor') + } + # Do some other stuff +} +``` + +We could test the error thrown in several ways: + +```py +it('throws on octopus', || { + def drink_octopus() { + drink_flavor('octopus') + } + + # Test that the error message says "yuck" somewhere: these are equivalent + expect(drink_octopus).to_throw('/yuck/') + expect(drink_octopus).to_throw('yuck') + + # Test the exact error message + expect(drink_octopus).to_throw('/^yuck, octopus flavor$/') + expect(drink_octopus).to_throw(Exception('yuck, octopus flavor')) + + # Test that we get a DisgustingFlavorError + expect(drink_octopus).to_throw(DisgustingFlavorError) +}) +``` + +### to_have_length(number) + +Use `.to_have_length` to check that an object has a .length property and it is set to a certain numeric value. For example: + +```py +expect([1, 2, 3]).to_have_length(3) +expect('abc').to_have_length(3) +expect('').not().to_have_length(5) +``` + +### to_be_instance_of(class) + +Use `.to_be_instance_of(class)` to check that an object is an instance of a class. This matcher uses `instance_of` underneath. + +```py +class A {} + +expect(A()).to_be_instance_of(A) +expect(A()).to_be_instance_of(Exception) # fails +``` + +### to_be_function(value) + +Use `.to_be_function` when you want to check if a value is a function or a closure. For example, if `do_something()` is a function looking like this: + +```py +def do_something(id) { + if id == 1 return || { do_another_thing() } + else return || { do_something_else() } +} +``` + +We can test that `do_something()` correctly returns a function. + +```py +expect(do_something(1)).to_be_function() +``` + + +### to_have_property(name, value?) + +Use `.to_have_property` to check if an object has a given property. You can provide an optional value argument to compare the received property value against an expected value. + +```py +class A { + var name = 'something' +} + +expect(A()).to_have_property('name') +expect(A()).to_have_property('name', 'something') +``` + +> It's important to note that when value is given, value must not be `nil`. + +### to_have_method(name) + +Use the `.to_have_method` to check if an object is an instance of a class having a particular method. For example, let's say you have a class `A` and `B` defined as follows: + +```py +class A { + testing() {} +} + +class B { + testing() {} +} +``` + +and you have a function `return_class()` that could return an instance of any of `A` or `B`, you can test the output of that method like, + +```py +expect(return_class()).to_have_method('testing') +``` + +### to_have_decorator(name) + +Use the `.to_have_decorator` to check if an object is an instance of a class having a particular decorator. For example, let's say you have a class `A` and `B` defined as follows: + +```py +class A { + @testing() {} +} + +class B { + @testing() {} +} +``` + +and you have a function `return_class()` that could return an instance of any of `A` or `B`, you can test the output of that method like, + +```py +expect(return_class()).to_have_decorator('testing') +``` + +### to_be_boolean() + +Use `.to_be_boolean` to check for `true` or `false` values. For example, test that `user_is_admin()` returns a value of `true` or `false`: + +```py +it('should be true or false', || { + expect(user_is_admin()).to_be_boolean() +}) +``` + +### to_be_number() + +Use `.to_be_number` to check that a value is a number without requiring any specific number. For example, test that `number_of_cans()` returns a valid number: + +```py +it('should be a number', || { + expect(number_of_cans()).to_be_number() +}) +``` + +### to_be_string() + +Use `.to_be_string` to check that a value is a string without requiring any specific content. For example, test that `name_of_king()` returns a valid string: + +```py +it('should be a string', || { + expect(name_of_king()).to_be_string() +}) +``` + +### to_be_list() + +Use `.to_be_list` to check that a value is a list without requiring any specific content. For example, test that `fruits()` returns a valid list: + +```py +it('should be a string', || { + expect(fruits()).to_be_list() +}) +``` + +### to_be_dict() + +Use `.to_be_dict` to check that a value is a dictionary without requiring any specific content. For example, test that `{age: 10}` returns a valid dictionary: + +```py +it('should be a dictionary', || { + expect({age: 10}).to_be_dict() +}) +``` + +### to_be_class() + +Use `.to_be_class` to check that a value is a class and not an instance. For example, test that `Exception` is actually a class: + +```py +it('should be a list', || { + expect(Exception).to_be_class() +}) +``` + +### to_be_iterable() + +Use `.to_be_iterable` to check that a value is an iterable whether its of basic types (e.g. String, List etc.) or an iterable class. For example, suppose we have a class `Set` defined ass follows: + +```py +class Set { + @iter() {} + @itern() {} +} +``` + +The following test will show that it's as much an iterable as a list or dictionary can be. + +```py +it('should be enumerable', || { + expect([]).to_be_iterable() + expect({}).to_be_iterable() + expect(Set()).to_be_iterable() +}) +``` + +### to_be_file() + +Use `.to_be_file` to check that a value is a file object. For example, you can test that an handle `fh` returned by the function `get_config()` is actually a file like this: + +```py +var fh = get_config() + +expect(fh).to_be_file() +``` + +### to_be_bytes() + +Use `.to_be_bytes` to check that a value is an array of bytes. For example, + +```py +expect(bytes(0)).to_be_bytes() +``` + diff --git a/apps/nyssa/docs/test-globals.md b/apps/nyssa/docs/test-globals.md new file mode 100644 index 00000000..e025fd38 --- /dev/null +++ b/apps/nyssa/docs/test-globals.md @@ -0,0 +1,223 @@ +# Test Globals + +Qi exposes a set of global functions for use in your test files by putting each of these methods and objects into the global environment. You don't have to require or import anything to use them from your test files. + +## Reference + +- [Test Globals](#test-globals) + - [Reference](#reference) + - [Methods](#methods) + - [describe(name, fn)](#describename-fn) + - [it(name, fn)](#itname-fn) + - [Hooks](#hooks) + - [before\_all(fn)](#before_allfn) + - [after\_all(fn)](#after_allfn) + - [before\_each(fn)](#before_eachfn) + - [after\_each(fn)](#after_eachfn) + +## Methods + +### describe(name, fn) + +`describe(name, fn)` creates a block that groups together several related tests. It is the Test Suite. For example, if you have a `myBeverage` object that is supposed to be delicious but not sour, you could test it with: + +```py +var myBeverage = { + delicious: true, + sour: false, +} + +describe('my beverage', || { + it('should be delicious', || { + expect(myBeverage.delicious).to_be_truthy() + }); + + it('should be sour', || { + expect(myBeverage.sour).to_be_falsy() + }) +}) +``` + +You can also nest `describe` blocks if you have a hierarchy of tests: + +```py +var binay_string_to_number = | bin_string | { + if !bin_string.match('/^[01]+$/') { + die CustomError('Not a binary number.') + } + + return to_number('0b' + bin_string) +} + +describe('binay string to number', || { + describe('given an invalid binary string', || { + it('throws CustomError when composed of non-numbers', || { + expect(|| { binay_string_to_number('abc') }).to_throw(CustomError) + }) + + it('throws CustomError when having extra whitespace', || { + expect(|| { binay_string_to_number(' 100') }).to_throw(CustomError) + }) + }) + + describe('given a valid binary string', || { + it('returns the correct number', || { + expect(binay_string_to_number('100')).to_be(4) + }) + }) +}) +``` + +### it(name, fn) + +The `it(name, fn)` function is the entry point for tests in a test suite. For example, let's say there's a function `inches_of_rain()` that should be zero. Your whole test could be: + +```py +it('did not rain', || { + expect(inches_of_rain()).to_be(0) +}) +``` + +The first argument is the test name; the second argument is a function that contains the expectations to test. + +## Hooks + +Qi provides a few set of hooks that allows us to meet different requirements at different stages of our test. There are essentially four stages that can be hooked into in a Qi test. And they are as expected: + +- Before any test in a suite is run, +- After every test in a suite is run, +- Before a test is run, +- After a test is run. + +### before_all(fn) + +Runs a function before each of the tests in the test suite run. This is often useful if you want to set up some global state that will be used by many tests. + +For example: + +```py +var global_db = make_global_db() + +before_all(|| { + # Clears the database and adds some testing data. + return globalDatabase.clear(|| { + return globalDatabase.insert({testData: 'foo'}) + }) +}) + +# Since we only set up the database once in this example, it's important +# that our tests don't modify it. +describe('Before all', || { + it('can find things', || { + return global_db.find('thing', {}, |results| { + expect(results.length()).to_be_greater_than(0) + }) + }) +}) +``` + +Here the `before_all` ensures that the database is set up before tests run. If you want to run something before every test instead of before any test runs, use `before_each` instead. + +### after_all(fn) + +Runs a function after all the tests in a test suite have completed. This is often useful if you want to clean up some global setup state that is shared across tests. + +For example: + +```py +var global_db = make_global_db() + +def clean_up_db(db) { + db.clean_up() +} + +after_all(|| { + clean_up_db(global_db) +}); + +describe('confirming after_all works', || { + it('can find things', || { + return global_db.find('thing', {}, |results| { + expect(results.length()).to_be_greater_than(0) + }) + }) + + it('can insert a thing', || { + return global_db.insert('thing', make_thing(), |response| { + expect(response.success).to_be_truthy() + }) + }) +}) +``` + +Here the `after_all` ensures that `clean_up_db` is called after all tests in the describe run. + +If you want to run some cleanup after every test instead of after all tests, use `after_each` instead. + +### before_each(fn) + +Runs a function before each of the tests in the test suite runs. This is often useful if you want to reset some global state that will be used by many tests. + +For example: + +```py +var global_db = make_global_db() + +before_each(|| { + # Clears the database and adds some testing data. + global_db.clear() + global_db.insert({testData: 'foo'}); +}) + +describe('confirming before_each works', || { + it('can find things', || { + return global_db.find('thing', {}, |results| { + expect(results.length()).to_be_greater_than(0) + }) + }) + + it('can insert a thing', || { + return global_db.insert('thing', make_thing(), |response| { + expect(response.success).to_be_truthy() + }) + }) +}) +``` + +Here the `before_each` ensures that the database is reset for each test. If you only need to run some setup code once, before any tests run, use `before_all` instead. + +### after_each(fn) + +Runs a function after each one of the tests in this file completes. This is often useful if you want to clean up some temporary state that is created by each test. + +For example: + +```py +var global_db = make_global_db() + +def clean_up_db(db) { + db.clean_up() +} + +after_each(|| { + clean_up_db(global_db) +}) + +describe('confirming after_each works', || { + it('can find things', || { + return global_db.find('thing', {}, |results| { + expect(results.length()).to_be_greater_than(0) + }) + }) + + it('can insert a thing', || { + return global_db.insert('thing', make_thing(), |response| { + expect(response.success).to_be_truthy() + }) + }) +}) +``` + +Here the `after_each` ensures that `clean_up_db` is called after each test runs. + +If you want to run some cleanup just once, after all of the tests run, use `after_all` instead. diff --git a/apps/nyssa/docs/testing.md b/apps/nyssa/docs/testing.md new file mode 100644 index 00000000..07a7607d --- /dev/null +++ b/apps/nyssa/docs/testing.md @@ -0,0 +1,52 @@ +# Testing + +Blade comes shipped with a test runner called `qi` designed to run tests are out the directory `tests` and for this reason. Nyssa provides the default interface to the test runner via the `test` command allowing you to write and run tests for your Blade applications out of the box. + +Both Nyssa and `Qi` ship with Blade allowing you write comprehensive tests without any third-party package. + +### Writing a simple test + +Let's write a test for a hypothetical function that returns the product of two numbers. First, we'll create a file `prod.b` that contains the following code: + +```py +def prod(x, y) { + return x * y +} +``` + +Now, let's create a test for it by creating a file `prod.test.b` in the `tests` directory and add the following code to it. + +```py +import ..prod + +describe('Product test suite', || { + it('should return 6 for 2 and 3', || { + expect(prod(2, 3)).to_be(6) + }) +}) +``` + +### Running your tests + +Now let's run the test. If you have installed Qi using `nyssa` (which is recommended), then you can run the following command at the root directory (the directory that contains the `tests` folder). + +```sh +.blade/qi +``` + +You should get an output similar to this: + +```sh + PASS tests/prod.test.b + Product test suite + ✔ should return 6 for 2 and 3 (1.09ms) + ✔ expect "6" to be "6" + +Test suites: 1 passed, 0 failed, 1 total +Tests: 1 passed, 0 failed, 1 total +Assertions: 1 passed, 0 failed, 1 total +Time: 1.092ms +Ran all test suites. +``` + +**You have successfully created your first Qi test!** diff --git a/apps/nyssa/public/css/style.css b/apps/nyssa/public/css/style.css index c6cf9bde..0219f95b 100644 --- a/apps/nyssa/public/css/style.css +++ b/apps/nyssa/public/css/style.css @@ -265,7 +265,7 @@ pre.has-icon .icon { line-height: 1.5; } -.content.doc h3 { +.content.doc h2 { border-bottom: 1px solid rgba(0, 0, 0, .1); padding-bottom: .5em; } diff --git a/apps/qi/.gitattributes b/apps/qi/.gitattributes new file mode 100644 index 00000000..f7da81aa --- /dev/null +++ b/apps/qi/.gitattributes @@ -0,0 +1,2 @@ +*.b linguist-detectable +*.b linguist-language=Blade diff --git a/apps/qi/.gitignore b/apps/qi/.gitignore new file mode 100644 index 00000000..01426527 --- /dev/null +++ b/apps/qi/.gitignore @@ -0,0 +1,18 @@ +# blade packages directory and files +.blade/ +*.nyp + +# popular editors +.vscode/ +.idea/ +.vs/ + +# c object files (for C extensions) +*.o +*.ko +*.obj +*.elf + +# log files +*.log +/qi diff --git a/apps/qi/README.md b/apps/qi/README.md new file mode 100644 index 00000000..78f6e840 --- /dev/null +++ b/apps/qi/README.md @@ -0,0 +1,70 @@ +# Qi + +A testing framework for Blade programming language. + +### Package Information + +- **Name:** qi +- **Version:** 1.0.0 +- **Homepage:** https://github.com/mcfriend99/qi.git +- **Tags:** test, blade, testing, tdd, test-driven +- **Author:** Richard Ore +- **License:** MIT +- **Requires:** Nyssa v0.1.6+ + +### Getting started + +Qi is a Nyssa package and can be installed using the command: + +``` +nyssa install qi +``` + +Qi is designed to run tests are out the directory `tests` and for this reason, all tests files must reside inside the `tests` directory. + +### Writing a simple test + +Let's write a test for a hypothetical function that returns the product of two numbers. First, we'll create a file `prod.b` that contains the following code: + +```py +def prod(x, y) { + return x * y +} +``` + +Now, let's create a test for it by creating a file `prod.test.b` in the `tests` directory and add the following code to it. + +```py +import ..prod + +describe('Product test suite', || { + it('should return 6 for 2 and 3', || { + expect(prod(2, 3)).to_be(6) + }) +}) +``` + +Now let's run the test. If you have installed Qi using `nyssa` (which is recommended), then you can run the following command at the root directory (the directory that contains the `tests` folder). + +``` +.blade/qi +``` + +You should get an output similar to this: + +``` + PASS tests/prod.test.b + Product test suite + ✔ should return 6 for 2 and 3 (1.09ms) + ✔ expect "6" to be "6" + +Test suites: 1 passed, 0 failed, 1 total +Tests: 1 passed, 0 failed, 1 total +Assertions: 1 passed, 0 failed, 1 total +Time: 1.092ms +Ran all test suites. +``` + +**You have successfully created your first Qi test!** + +Read more documentation at: [mcfriend99.github.io/qi](https://mcfriend99.github.io/qi/) diff --git a/apps/qi/app/index.b b/apps/qi/app/index.b new file mode 100644 index 00000000..a788bd96 --- /dev/null +++ b/apps/qi/app/index.b @@ -0,0 +1,10 @@ +import .lib { * } +import reflect + +reflect.set_global(describe) +reflect.set_global(it) +reflect.set_global(expect) +reflect.set_global(after_each) +reflect.set_global(before_each) +reflect.set_global(after_all) +reflect.set_global(before_all) diff --git a/apps/qi/app/lib.b b/apps/qi/app/lib.b new file mode 100644 index 00000000..a017330e --- /dev/null +++ b/apps/qi/app/lib.b @@ -0,0 +1,426 @@ +import reflect +import colors +import iters +import io + + +var _before_eachs = [] +var _before_alls = [] +var _after_eachs = [] +var _after_alls = [] +var _total_tests = 0 +var _total_suites = 0 +var _passed_tests = 0 +var _failed_tests = 0 +var _file + +def file() { + return _file +} + +def set_file(f) { + _file = f +} + +var _stats = [] +var _curr_desc = { + it: [], + file: file(), + time: 0, +} +var _curr_it = {} + +def before_each(fn) { + _before_eachs.append(fn) +} + +def after_each(fn) { + _after_eachs.append(fn) +} + +def before_all(fn) { + _before_alls.append(fn) +} + +def after_all(fn) { + _after_alls.append(fn) +} + +class expect { + var value + var _is_not = false + + expect(value) { + self.value = value + } + + _run(name, expected, fn) { + if self._is_not name = 'not ${name}' + if !fn fn = |x, y| { return x == y } + + var v = to_string(self.value).replace('\r', '\\r').replace('\n', '\\n') + var w = expected != nil ? to_string(expected).replace('\r', '\\r').replace('\n', '\\n') : nil + #if v.length() > 20 v = v[,17] + '...' + #if w.length() > 20 w = v[,17] + '...' + + var state = { + name: w != nil ? 'expect "${v}" ${name} "${w}"' : 'expect "${v}" ${name}', + status: false, + } + + try { + if !self._is_not and fn(self.value, expected) { + _passed_tests++ + state.status = true + } else if self._is_not and !fn(self.value, expected) { + _passed_tests++ + state.status = true + } else { + _failed_tests++ + } + } catch Exception e { + _failed_tests++ + io.stderr.write(e.message + '\r\n') + io.stderr.write(e.stacktrace + '\r\n') + } finally { + _curr_it.expects.append(state) + } + } + + not() { + self._is_not = true + return self + } + + to_be(e) { + self._run('to be', e) + return self + } + + to_be_nil() { + self._run('to be nil', nil) + return self + } + + to_be_defined() { + self._run('to be defined', nil, |x, y| { return x != nil }) + return self + } + + to_be_truthy() { + self._run('to be truthy', nil, |x, y| { return !!x }) + return self + } + + to_be_falsy() { + self._run('to be falsy', nil, |x, y| { return !x }) + return self + } + + to_be_greater_than(e) { + self._run('to be greather than', e, |x, y| { + if is_string(x) return x.length() > y + return x > y + }) + return self + } + + to_be_greater_than_or_equal(e) { + self._run('to be greather than or equal to', e, |x, y| { + if is_string(x) return x.length() >= y + return x >= y + }) + return self + } + + to_be_less_than(e) { + self._run('to be less than', e, |x, y| { + if is_string(x) return x.length() < y + return x < y + }) + return self + } + + to_be_less_than_or_equal(e) { + self._run('to be less than or equal to', e, |x, y| { + if is_string(x) return x.length() <= y + return x <= y + }) + return self + } + + to_match(e) { + self._run('to match', e, |x, y| { return x.match(y) }) + return self + } + + to_contain(e) { + self._run('to contain', e, |x, y| { + if is_dict(x) return x.contains(y) + return x.count(y) > 0 + }) + return self + } + + to_throw(e) { + if !e e = Exception + + if is_function(self.value) { + self._run('to throw', e, |x, y| { + try { + x() + return false + } catch Exception ex { + if is_string(e) return ex.message.match(e) + if is_class(e) return instance_of(ex, e) + if instance_of(e, Exception) and ex == e return true + return true + } + }) + } else { + self._run('to throw', e, @(x, y) { return false }) + } + + return self + } + + to_have_length(e) { + self._run('to have length', e, |x, y| { return x.length() == y }) + return self + } + + to_be_instance_of(e) { + self._run('to be an instance of', e, |x, y| { return instance_of(x, y) }) + return self + } + + to_have_property(e, value) { + var name = 'to have a property' + if value != nil name = 'to have value "${to_string(value)}" in property' + + self._run(name, e, |x, y| { + var res = is_instance(x) and reflect.has_prop(x, y) + if res and value != nil { + return reflect.get_prop(x, y) == value + } + return res + }) + + return self + } + + to_have_method(e) { + self._run('to have a method', e, |x, y| { + return is_instance(x) and reflect.has_method(x, y) + }) + return self + } + + to_have_decorator(e) { + self._run('to have a decorator', e, |x, y| { + return is_instance(x) and reflect.has_decorator(x, y) + }) + return self + } + + to_be_boolean() { + self._run('to be a boolean', nil, |x, y| { return is_bool(x) }) + return self + } + + to_be_number() { + self._run('to be a number', nil, |x, y| { return is_number(x) }) + return self + } + + to_be_string() { + self._run('to be a string', nil, |x, y| { return is_string(x) }) + return self + } + + to_be_list() { + self._run('to be a list', nil, |x, y| { return is_list(x) }) + return self + } + + to_be_dict() { + self._run('to be a dict', nil, |x, y| { return is_dict(x) }) + return self + } + + to_be_function() { + self._run('to be a function', nil, |x, y| { return is_function(x) }) + return self + } + + to_be_class() { + self._run('to be a class', nil, |x, y| { return is_class(x) }) + return self + } + + to_be_iterable() { + self._run('to be an iterable', nil, |x, y| { return is_iterable(x) }) + return self + } + + to_be_file() { + self._run('to be a file', nil, |x, y| { return is_file(x) }) + return self + } + + to_be_bytes() { + self._run('to be bytes', nil, |x, y| { return is_bytes(x) }) + return self + } +} + +def it(desc, fn) { + _total_tests++ + var start = microtime() + for be in _before_eachs { + be() + } + + _curr_it = { + name: desc, + expects: [], + time: 0, + } + + try { + fn() + } catch Exception e { + io.stderr.write(e.message + '\r\n') + io.stderr.write(e.stacktrace + '\r\n') + } + + for ae in _after_eachs { + ae() + } + + _curr_it.time = microtime() - start + + _curr_desc.it.append(_curr_it) +} + +def describe(desc, fn) { + try { + _curr_desc = { + it: [], + file: file(), + time: 0, + } + + var start = microtime() + + for ba in _before_alls { + ba() + } + + _curr_desc.name = desc + fn() + + for aa in _after_alls { + aa() + } + + _curr_desc.time = microtime() - start + _stats.append(_curr_desc) + } catch Exception e { + io.stderr.write(e.message + '\r\n') + io.stderr.write(e.stacktrace + '\r\n') + } +} + +def _get_mark(state) { + return state ? + colors.text('\u2714', colors.text_color.green) : + colors.text('\u2715', colors.text_color.red) +} + +def _print(text, state) { + return state ? + colors.text('\u2714 ' + text, colors.text_color.green) : + colors.text('\u2715 ' + text, colors.text_color.red) +} + +def _gray(txt) { + return colors.text(txt, colors.text_color.dark_grey) +} + +def _report(text, state) { + return state ? + colors.text(text, colors.text_color.green) : + colors.text(text, colors.text_color.red) +} + +def _time(time) { + if time < 1000 { + return time + 'µs' + } else if time < 1000000 { + return (time / 1000) + 'ms' + } + + return (time / 1000000) + 's' +} + +def run(f) { + set_file(f) + reflect.run_script(f) +} + +def show_tests_results() { + echo '' + + var failed_suites = 0 + var failed_tests = 0 + var total_time = 0 + + iter var index = 0; index < _stats.length(); index++ { + var e = _stats[index] + + total_time += e.time + + var fails = iters.filter(e.it, |x| { + return iters.filter(x.expects, |y|{ return !y.status }).length() > 0 + }).length() > 0 + if fails failed_suites++ + + echo colors.text( + !fails ? ' PASS ' : ' FAIL ', + fails ? + colors.background.red : + colors.background.green + ) + ' ' + e.file + + echo ' ${e.name}' + iter var i = 0; i < e.it.length(); i++ { + var _e = e.it[i] + var it_fails = iters.filter(_e.expects, |x|{ return !x.status }).length() > 0 + if it_fails failed_tests++ + + echo ' ' + _print('${_e.name} (${_time(_e.time)})', !it_fails) + + iter var j = 0; j < _e.expects.length(); j++ { + var expect = _e.expects[j] + echo ' ${_get_mark(expect.status)} ${_gray(expect.name)}' + } + } + echo '' + } + + var passed_suites = _report((_stats.length() - failed_suites) + ' passed', true) + var passed_tests = _report((_total_tests - failed_tests) + ' passed', true) + var passes = _passed_tests > 0 ? _report(_passed_tests + ' passed', true) : '0 passed' + var suite_fails = failed_suites > 0 ? _report(failed_suites + ' failed', false) : '0 failed' + var test_fails = failed_tests > 0 ? _report(failed_tests + ' failed', false) : '0 failed' + var assert_fails = _failed_tests > 0 ? _report(_failed_tests + ' failed', false) : '0 failed' + + echo colors.text('Test suites: ${passed_suites}, ${suite_fails}, ${_stats.length()} total', colors.style.bold) + echo colors.text('Tests: ${passed_tests}, ${test_fails}, ${_total_tests} total', colors.style.bold) + echo colors.text('Assertions: ${passes}, ${assert_fails}, ${_passed_tests + _failed_tests} total', colors.style.bold) + echo colors.text('Time: ${_time(total_time)}', colors.style.bold) + echo colors.text(_gray('Ran all test suites.'), colors.style.bold) + + return _failed_tests == 0 +} diff --git a/apps/qi/index.b b/apps/qi/index.b new file mode 100644 index 00000000..269e9765 --- /dev/null +++ b/apps/qi/index.b @@ -0,0 +1 @@ +import .app { * } \ No newline at end of file diff --git a/apps/qi/nyssa.json b/apps/qi/nyssa.json new file mode 100644 index 00000000..95e99422 --- /dev/null +++ b/apps/qi/nyssa.json @@ -0,0 +1,18 @@ +{ + "name": "qi", + "version": "1.0.1", + "description": "A testing framework for Blade programming language.", + "homepage": "https://github.com/mcfriend99/qi.git", + "tags": [ + "test", + "blade", + "testing", + "tdd", + "test-driven" + ], + "author": "Richard Ore ", + "license": "MIT", + "sources": [ + "https://nyssa.bladelang.com" + ] +} \ No newline at end of file diff --git a/apps/qi/tests/.gitignore b/apps/qi/tests/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/apps/qi/tests/global.b b/apps/qi/tests/global.b new file mode 100644 index 00000000..49dcb3b7 --- /dev/null +++ b/apps/qi/tests/global.b @@ -0,0 +1,42 @@ +var myBeverage = { + delicious: true, + sour: false, +} + +describe('my beverage', || { + it('should be delicious', || { + expect(myBeverage.delicious).to_be_truthy() + }); + + it('should be sour', || { + expect(myBeverage.sour).to_be_falsy() + }) +}) + +class CustomError < Exception {} + +var binay_string_to_number = | bin_string | { + if !bin_string.match('/^[01]+$/') { + die CustomError('Not a binary number.') + } + + return to_number('0b' + bin_string) +} + +describe('binay string to number', || { + describe('given an invalid binary string', || { + it('throws CustomError when composed of non-numbers', || { + expect(|| { binay_string_to_number('abc') }).to_throw(CustomError) + }) + + it('throws CustomError when having extra whitespace', || { + expect(|| { binay_string_to_number(' 100') }).to_throw(CustomError) + }) + }) + + describe('given a valid binary string', || { + it('returns the correct number', || { + expect(binay_string_to_number('100')).to_be(4) + }) + }) +}) diff --git a/apps/qi/tests/sample.spec.b b/apps/qi/tests/sample.spec.b new file mode 100644 index 00000000..38241363 --- /dev/null +++ b/apps/qi/tests/sample.spec.b @@ -0,0 +1,12 @@ +describe('Some testing', || { + it('should match exactly!', || { + expect(3).to_be(3) + expect(5).to_be(5) + expect(5).not().to_be(56) + + class X {} + + var b = [] + expect(|| { return b[5] }).not().to_throw(X) + }) +}) \ No newline at end of file diff --git a/apps/qi/tests/sample2.spec.b b/apps/qi/tests/sample2.spec.b new file mode 100644 index 00000000..cfadc926 --- /dev/null +++ b/apps/qi/tests/sample2.spec.b @@ -0,0 +1,11 @@ +describe('Some testing', || { + it('should match exactly!', || { + expect(3).to_be(3) + expect(5).to_be(5) + + class X {} + + var b = [] + expect(|| { return b[5] }).to_throw() + }) +}) \ No newline at end of file diff --git a/apps/qi/tests/setup.spec.b b/apps/qi/tests/setup.spec.b new file mode 100644 index 00000000..08ef5100 --- /dev/null +++ b/apps/qi/tests/setup.spec.b @@ -0,0 +1,10 @@ +import ..setup + +describe('Importing', || { + it('should have a valid command', || { + expect(setup.cmd).to_be_string() + }) + it('should have a correct file path', || { + expect(setup.path).to_contain('qi') + }) +}) diff --git a/apps/qi/tests/test.b b/apps/qi/tests/test.b new file mode 100644 index 00000000..673e6dee --- /dev/null +++ b/apps/qi/tests/test.b @@ -0,0 +1,105 @@ +var can = { + name: 'pamplemousse', + ounces: 12, +} + +describe('the can', || { + it('has 12 ounces', || { + expect(can.ounces).to_be(12) + + class A { + var name = 'something' + } + + expect(A()).to_have_property('name') + expect(A()).to_have_property('name', 'something') + + expect(10.5).to_be_number().to_be_less_than(20) + }) + + it('has a sophisticated name', || { + expect(can.name).to_be('pamplemousse') + + expect([1, 2, 3]).to_have_length(3) + expect('abc').to_have_length(3) + expect('').not().to_have_length(5) + }) + + it('should pass this tests', || { + class A { + testing() {} + @testing() {} + } + + expect(A()).to_be_instance_of(A) + # expect(A()).to_be_instance_of(Exception) # fails + expect(A()).to_have_method('testing') + + def bloop() { + return nil + } + + expect(bloop()).to_be_nil() + + expect(A()).to_have_decorator('testing') + }) +}) + +def do_something(id) { + if id == 1 return || { do_another_thing() } + else return || { do_something_else() } +} + +class Set { + @iter() {} + @itern() {} +} + +describe('grapefruits', || { + it('should be a grape', || { + expect('grapefruits').to_match('grape') + expect(do_something(1)).to_be_function() + + expect('Hosana').to_be_string() + expect(Exception).to_be_class() + }) + + it('should be valid type', || { + expect({age: 10}).to_be_dict() + expect(bytes(0)).to_be_bytes() + }) + + it('should be enumerable', || { + expect([]).to_be_iterable() + expect({}).to_be_iterable() + expect(Set()).to_be_iterable() + }) +}) + +class DisgustingFlavorError < Exception {} + +def drink_flavor(flavor) { + if flavor == 'octopus' { + die DisgustingFlavorError('yuck, octopus flavor') + } + # Do some other stuff +} + +describe('testing to throw', || { + it('throws on octopus', || { + def drink_octopus() { + drink_flavor('octopus') + } + + # Test that the error message says "yuck" somewhere: these are equivalent + expect(drink_octopus).to_throw('/yuck/') + expect(drink_octopus).to_throw('yuck') + + # Test the exact error message + expect(drink_octopus).to_throw('/^yuck, octopus flavor$/') + expect(drink_octopus).to_throw(Exception('yuck, octopus flavor')) + + # Test that we get a DisgustingFlavorError + expect(drink_octopus).to_throw(DisgustingFlavorError) + }) +}) diff --git a/qi b/qi new file mode 100755 index 00000000..dc0ff7dc --- /dev/null +++ b/qi @@ -0,0 +1,10 @@ +#!/bin/bash + +if [[ ! $(command -v blade) ]]; then + echo "Blade is not installed or not in path." + exit 1 +fi + +BLADE_EXE=`which blade` + +exec $BLADE_EXE "/Users/mcfriendsy/c/blade/blade/apps/qi/cli.b" "$@" From 359746d6569a63e593f141e05330d6a9e4aab2d8 Mon Sep 17 00:00:00 2001 From: Richard Ore Date: Sun, 18 Jun 2023 19:28:22 +0100 Subject: [PATCH 3/5] updated nyssa with qi --- apps/nyssa/.blade/libs/highlight.b | 76 ++++++++ .../{ => nyssa/.blade/libs}/qi/.gitattributes | 0 apps/{ => nyssa/.blade/libs}/qi/.gitignore | 0 apps/{ => nyssa/.blade/libs}/qi/README.md | 14 +- apps/{ => nyssa/.blade/libs}/qi/app/index.b | 0 apps/{ => nyssa/.blade/libs}/qi/app/lib.b | 58 +++--- apps/{ => nyssa/.blade/libs}/qi/index.b | 0 apps/{ => nyssa/.blade/libs}/qi/nyssa.json | 0 .../.blade/libs}/qi/tests/.gitignore | 0 apps/nyssa/.blade/libs/qi/tests/global.b | 42 ++++ apps/nyssa/.blade/libs/qi/tests/sample.spec.b | 12 ++ .../nyssa/.blade/libs/qi/tests/sample2.spec.b | 11 ++ apps/nyssa/.blade/libs/qi/tests/setup.spec.b | 10 + apps/{ => nyssa/.blade/libs}/qi/tests/test.b | 24 +-- apps/nyssa/app/commands/test.b | 2 +- apps/nyssa/app/server/router.b | 2 +- apps/nyssa/app/server/template_ext.b | 5 +- apps/nyssa/app/server/views.b | 6 +- apps/nyssa/docs/commands.md | 41 ++-- ...installing-nyssa.md => getting-started.md} | 11 +- .../docs/install-and-uninstall-actions.md | 2 +- apps/nyssa/docs/test-assertions.md | 180 +++++++----------- apps/nyssa/docs/test-globals.md | 101 +++++----- apps/nyssa/docs/testing.md | 10 +- apps/nyssa/public/css/style.css | 14 +- apps/nyssa/templates/doc.html | 34 ++-- apps/qi/tests/global.b | 42 ---- apps/qi/tests/sample.spec.b | 12 -- apps/qi/tests/sample2.spec.b | 11 -- apps/qi/tests/setup.spec.b | 10 - libs/markdown/block/list.b | 4 +- libs/markdown/index.b | 14 +- libs/markdown/renderer.b | 2 +- libs/markdown/ruler.b | 8 +- 34 files changed, 402 insertions(+), 356 deletions(-) create mode 100644 apps/nyssa/.blade/libs/highlight.b rename apps/{ => nyssa/.blade/libs}/qi/.gitattributes (100%) rename apps/{ => nyssa/.blade/libs}/qi/.gitignore (100%) rename apps/{ => nyssa/.blade/libs}/qi/README.md (82%) rename apps/{ => nyssa/.blade/libs}/qi/app/index.b (100%) rename apps/{ => nyssa/.blade/libs}/qi/app/lib.b (81%) rename apps/{ => nyssa/.blade/libs}/qi/index.b (100%) rename apps/{ => nyssa/.blade/libs}/qi/nyssa.json (100%) rename apps/{ => nyssa/.blade/libs}/qi/tests/.gitignore (100%) create mode 100644 apps/nyssa/.blade/libs/qi/tests/global.b create mode 100644 apps/nyssa/.blade/libs/qi/tests/sample.spec.b create mode 100644 apps/nyssa/.blade/libs/qi/tests/sample2.spec.b create mode 100644 apps/nyssa/.blade/libs/qi/tests/setup.spec.b rename apps/{ => nyssa/.blade/libs}/qi/tests/test.b (82%) rename apps/nyssa/docs/{installing-nyssa.md => getting-started.md} (88%) delete mode 100644 apps/qi/tests/global.b delete mode 100644 apps/qi/tests/sample.spec.b delete mode 100644 apps/qi/tests/sample2.spec.b delete mode 100644 apps/qi/tests/setup.spec.b diff --git a/apps/nyssa/.blade/libs/highlight.b b/apps/nyssa/.blade/libs/highlight.b new file mode 100644 index 00000000..031055ff --- /dev/null +++ b/apps/nyssa/.blade/libs/highlight.b @@ -0,0 +1,76 @@ +var blade_keywords = '|'.join([ + 'as', 'assert', 'break', 'catch', 'class', 'continue', + 'def', 'default', 'die', 'do', 'echo', 'else', 'finally', 'for', + 'if', 'import', 'in', 'iter', 'return', 'static', 'try', + 'using', 'var', 'when', 'while', +]) + +var constant_keywords = '|'.join([ + 'nil', 'parent', 'self', 'true', 'false', 'and', 'or' +]) + +def highlight_blade(text) { + text = text. + # operators + replace('/([+\-*=/%!<>@]|\.\.)/', '<_o>$1'). + replace('/\\b(and|or)\\b/', '<_o>$1'). + # quotes + replace('/((\'(?:[^\'\\\\]|\\.)*\')|("(?:[^"\\\\]|\\.)*"))/', '<_q>$1'). + # constant keywords + replace('/\\b(${constant_keywords})\\b/', '<_c>$1'). + + # numbers + replace('/(([0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?)|(0x[0-9a-fA-F]+)|(0c[0-7][0-7]*)|(0b[01][01]*)|([0-9]+))/', '<_n>$1'). + + # functions + # property/method call and access + replace('/(?<=\.)[ ]*([a-zA-Z_][a-zA-Z0-9_]*)[ ]*(?=[(])/', '<_m>$1'). + # definition and call + replace('/(?$1'). + + # keywords + replace('/\\b(${blade_keywords})\\b/', '<_k>$1'). + # comments + replace('/(#[^\\n]*|\/(?!\\\\)\*[\s\S]*?\*(?!\\\\)\/)/', '<_w>$1') + + # clean up comments + var comments = text.matches('/<_w>(.*?)<\/_w>/') + if comments { + for comment in comments[1] { + text = text.replace(comment, comment.replace('/<\/?_([^>]+)>/', ''), false) + } + } + + # clean up quotes + var quotes = text.matches('/<_q>(.*?)<\/_q>/') + if quotes { + for quote in quotes[1] { + text = text.replace( + quote, + quote.replace('/<\/?_([^>]+)>/', ''). + # interpolation + replace('/(\\$\{[^}]+\})/', '<_i>$1'), + false + ) + } + } + + # expand styles. + return text.replace('/<_q>(.*?)<\/_q>/', '$1'). + replace('/<_i>(.*?)<\/_i>/', '$1'). + replace('/<_c>(.*?)<\/_c>/', '$1'). + replace('/<_m>(.*?)<\/_m>/', '$1'). + replace('/<_f>(.*?)<\/_f>/', '$1'). + replace('/<_k>(.*?)<\/_k>/', '$1'). + replace('/<_w>(.*?)<\/_w>/', '$1'). + replace('/<_o>(.*?)<\/_o>/', '$1'). + replace('/<_n>(.*?)<\/_n>/', '$1') +} + +def highlight(text, lang) { + if lang == 'blade' { + return highlight_blade(text) + } + return text +} + diff --git a/apps/qi/.gitattributes b/apps/nyssa/.blade/libs/qi/.gitattributes similarity index 100% rename from apps/qi/.gitattributes rename to apps/nyssa/.blade/libs/qi/.gitattributes diff --git a/apps/qi/.gitignore b/apps/nyssa/.blade/libs/qi/.gitignore similarity index 100% rename from apps/qi/.gitignore rename to apps/nyssa/.blade/libs/qi/.gitignore diff --git a/apps/qi/README.md b/apps/nyssa/.blade/libs/qi/README.md similarity index 82% rename from apps/qi/README.md rename to apps/nyssa/.blade/libs/qi/README.md index 78f6e840..ebb60b25 100644 --- a/apps/qi/README.md +++ b/apps/nyssa/.blade/libs/qi/README.md @@ -14,13 +14,7 @@ A testing framework for Blade programming language. ### Getting started -Qi is a Nyssa package and can be installed using the command: - -``` -nyssa install qi -``` - -Qi is designed to run tests are out the directory `tests` and for this reason, all tests files must reside inside the `tests` directory. +Qi is a Nyssa package that ships along with it and is designed to run tests are out the directory `tests` and for this reason, all tests files must reside inside the `tests` directory. ### Writing a simple test @@ -37,8 +31,8 @@ Now, let's create a test for it by creating a file `prod.test.b` in the `tests` ```py import ..prod -describe('Product test suite', || { - it('should return 6 for 2 and 3', || { +describe('Product test suite', @() { + it('should return 6 for 2 and 3', @() { expect(prod(2, 3)).to_be(6) }) }) @@ -47,7 +41,7 @@ describe('Product test suite', || { Now let's run the test. If you have installed Qi using `nyssa` (which is recommended), then you can run the following command at the root directory (the directory that contains the `tests` folder). ``` -.blade/qi +nyssa test ``` You should get an output similar to this: diff --git a/apps/qi/app/index.b b/apps/nyssa/.blade/libs/qi/app/index.b similarity index 100% rename from apps/qi/app/index.b rename to apps/nyssa/.blade/libs/qi/app/index.b diff --git a/apps/qi/app/lib.b b/apps/nyssa/.blade/libs/qi/app/lib.b similarity index 81% rename from apps/qi/app/lib.b rename to apps/nyssa/.blade/libs/qi/app/lib.b index a017330e..239e146e 100644 --- a/apps/qi/app/lib.b +++ b/apps/nyssa/.blade/libs/qi/app/lib.b @@ -56,7 +56,7 @@ class expect { _run(name, expected, fn) { if self._is_not name = 'not ${name}' - if !fn fn = |x, y| { return x == y } + if !fn fn = @(x, y) { return x == y } var v = to_string(self.value).replace('\r', '\\r').replace('\n', '\\n') var w = expected != nil ? to_string(expected).replace('\r', '\\r').replace('\n', '\\n') : nil @@ -103,22 +103,22 @@ class expect { } to_be_defined() { - self._run('to be defined', nil, |x, y| { return x != nil }) + self._run('to be defined', nil, @(x, y) { return x != nil }) return self } to_be_truthy() { - self._run('to be truthy', nil, |x, y| { return !!x }) + self._run('to be truthy', nil, @(x, y) { return !!x }) return self } to_be_falsy() { - self._run('to be falsy', nil, |x, y| { return !x }) + self._run('to be falsy', nil, @(x, y) { return !x }) return self } to_be_greater_than(e) { - self._run('to be greather than', e, |x, y| { + self._run('to be greather than', e, @(x, y) { if is_string(x) return x.length() > y return x > y }) @@ -126,7 +126,7 @@ class expect { } to_be_greater_than_or_equal(e) { - self._run('to be greather than or equal to', e, |x, y| { + self._run('to be greather than or equal to', e, @(x, y) { if is_string(x) return x.length() >= y return x >= y }) @@ -134,7 +134,7 @@ class expect { } to_be_less_than(e) { - self._run('to be less than', e, |x, y| { + self._run('to be less than', e, @(x, y) { if is_string(x) return x.length() < y return x < y }) @@ -142,7 +142,7 @@ class expect { } to_be_less_than_or_equal(e) { - self._run('to be less than or equal to', e, |x, y| { + self._run('to be less than or equal to', e, @(x, y) { if is_string(x) return x.length() <= y return x <= y }) @@ -150,12 +150,12 @@ class expect { } to_match(e) { - self._run('to match', e, |x, y| { return x.match(y) }) + self._run('to match', e, @(x, y) { return x.match(y) }) return self } to_contain(e) { - self._run('to contain', e, |x, y| { + self._run('to contain', e, @(x, y) { if is_dict(x) return x.contains(y) return x.count(y) > 0 }) @@ -166,7 +166,7 @@ class expect { if !e e = Exception if is_function(self.value) { - self._run('to throw', e, |x, y| { + self._run('to throw', e, @(x, y) { try { x() return false @@ -185,12 +185,12 @@ class expect { } to_have_length(e) { - self._run('to have length', e, |x, y| { return x.length() == y }) + self._run('to have length', e, @(x, y) { return x.length() == y }) return self } to_be_instance_of(e) { - self._run('to be an instance of', e, |x, y| { return instance_of(x, y) }) + self._run('to be an instance of', e, @(x, y) { return instance_of(x, y) }) return self } @@ -198,7 +198,7 @@ class expect { var name = 'to have a property' if value != nil name = 'to have value "${to_string(value)}" in property' - self._run(name, e, |x, y| { + self._run(name, e, @(x, y) { var res = is_instance(x) and reflect.has_prop(x, y) if res and value != nil { return reflect.get_prop(x, y) == value @@ -210,66 +210,66 @@ class expect { } to_have_method(e) { - self._run('to have a method', e, |x, y| { + self._run('to have a method', e, @(x, y) { return is_instance(x) and reflect.has_method(x, y) }) return self } to_have_decorator(e) { - self._run('to have a decorator', e, |x, y| { + self._run('to have a decorator', e, @(x, y) { return is_instance(x) and reflect.has_decorator(x, y) }) return self } to_be_boolean() { - self._run('to be a boolean', nil, |x, y| { return is_bool(x) }) + self._run('to be a boolean', nil, @(x, y) { return is_bool(x) }) return self } to_be_number() { - self._run('to be a number', nil, |x, y| { return is_number(x) }) + self._run('to be a number', nil, @(x, y) { return is_number(x) }) return self } to_be_string() { - self._run('to be a string', nil, |x, y| { return is_string(x) }) + self._run('to be a string', nil, @(x, y) { return is_string(x) }) return self } to_be_list() { - self._run('to be a list', nil, |x, y| { return is_list(x) }) + self._run('to be a list', nil, @(x, y) { return is_list(x) }) return self } to_be_dict() { - self._run('to be a dict', nil, |x, y| { return is_dict(x) }) + self._run('to be a dict', nil, @(x, y) { return is_dict(x) }) return self } to_be_function() { - self._run('to be a function', nil, |x, y| { return is_function(x) }) + self._run('to be a function', nil, @(x, y) { return is_function(x) }) return self } to_be_class() { - self._run('to be a class', nil, |x, y| { return is_class(x) }) + self._run('to be a class', nil, @(x, y) { return is_class(x) }) return self } to_be_iterable() { - self._run('to be an iterable', nil, |x, y| { return is_iterable(x) }) + self._run('to be an iterable', nil, @(x, y) { return is_iterable(x) }) return self } to_be_file() { - self._run('to be a file', nil, |x, y| { return is_file(x) }) + self._run('to be a file', nil, @(x, y) { return is_file(x) }) return self } to_be_bytes() { - self._run('to be bytes', nil, |x, y| { return is_bytes(x) }) + self._run('to be bytes', nil, @(x, y) { return is_bytes(x) }) return self } } @@ -381,8 +381,8 @@ def show_tests_results() { total_time += e.time - var fails = iters.filter(e.it, |x| { - return iters.filter(x.expects, |y|{ return !y.status }).length() > 0 + var fails = iters.filter(e.it, @(x) { + return iters.filter(x.expects, @(y) { return !y.status }).length() > 0 }).length() > 0 if fails failed_suites++ @@ -396,7 +396,7 @@ def show_tests_results() { echo ' ${e.name}' iter var i = 0; i < e.it.length(); i++ { var _e = e.it[i] - var it_fails = iters.filter(_e.expects, |x|{ return !x.status }).length() > 0 + var it_fails = iters.filter(_e.expects, @(x) { return !x.status }).length() > 0 if it_fails failed_tests++ echo ' ' + _print('${_e.name} (${_time(_e.time)})', !it_fails) diff --git a/apps/qi/index.b b/apps/nyssa/.blade/libs/qi/index.b similarity index 100% rename from apps/qi/index.b rename to apps/nyssa/.blade/libs/qi/index.b diff --git a/apps/qi/nyssa.json b/apps/nyssa/.blade/libs/qi/nyssa.json similarity index 100% rename from apps/qi/nyssa.json rename to apps/nyssa/.blade/libs/qi/nyssa.json diff --git a/apps/qi/tests/.gitignore b/apps/nyssa/.blade/libs/qi/tests/.gitignore similarity index 100% rename from apps/qi/tests/.gitignore rename to apps/nyssa/.blade/libs/qi/tests/.gitignore diff --git a/apps/nyssa/.blade/libs/qi/tests/global.b b/apps/nyssa/.blade/libs/qi/tests/global.b new file mode 100644 index 00000000..8f68c5d9 --- /dev/null +++ b/apps/nyssa/.blade/libs/qi/tests/global.b @@ -0,0 +1,42 @@ +var myBeverage = { + delicious: true, + sour: false, +} + +describe('my beverage', @() { + it('should be delicious', @() { + expect(myBeverage.delicious).to_be_truthy() + }); + + it('should be sour', @() { + expect(myBeverage.sour).to_be_falsy() + }) +}) + +class CustomError < Exception {} + +var binay_string_to_number = @( bin_string ) { + if !bin_string.match('/^[01]+$/') { + die CustomError('Not a binary number.') + } + + return to_number('0b' + bin_string) +} + +describe('binay string to number', @() { + describe('given an invalid binary string', @() { + it('throws CustomError when composed of non-numbers', @() { + expect(@() { binay_string_to_number('abc') }).to_throw(CustomError) + }) + + it('throws CustomError when having extra whitespace', @() { + expect(@() { binay_string_to_number(' 100') }).to_throw(CustomError) + }) + }) + + describe('given a valid binary string', @() { + it('returns the correct number', @() { + expect(binay_string_to_number('100')).to_be(4) + }) + }) +}) diff --git a/apps/nyssa/.blade/libs/qi/tests/sample.spec.b b/apps/nyssa/.blade/libs/qi/tests/sample.spec.b new file mode 100644 index 00000000..48fd4749 --- /dev/null +++ b/apps/nyssa/.blade/libs/qi/tests/sample.spec.b @@ -0,0 +1,12 @@ +describe('Some testing', @() { + it('should match exactly!', @() { + expect(3).to_be(3) + expect(5).to_be(5) + expect(5).not().to_be(56) + + class X {} + + var b = [] + expect(@() { return b[5] }).not().to_throw(X) + }) +}) \ No newline at end of file diff --git a/apps/nyssa/.blade/libs/qi/tests/sample2.spec.b b/apps/nyssa/.blade/libs/qi/tests/sample2.spec.b new file mode 100644 index 00000000..19191ce2 --- /dev/null +++ b/apps/nyssa/.blade/libs/qi/tests/sample2.spec.b @@ -0,0 +1,11 @@ +describe('Some testing', @() { + it('should match exactly!', @() { + expect(3).to_be(3) + expect(5).to_be(5) + + class X {} + + var b = [] + expect(@() { return b[5] }).to_throw() + }) +}) \ No newline at end of file diff --git a/apps/nyssa/.blade/libs/qi/tests/setup.spec.b b/apps/nyssa/.blade/libs/qi/tests/setup.spec.b new file mode 100644 index 00000000..3b056e95 --- /dev/null +++ b/apps/nyssa/.blade/libs/qi/tests/setup.spec.b @@ -0,0 +1,10 @@ +import ..setup + +describe('Importing', @() { + it('should have a valid command', @() { + expect(setup.cmd).to_be_string() + }) + it('should have a correct file path', @() { + expect(setup.path).to_contain('qi') + }) +}) diff --git a/apps/qi/tests/test.b b/apps/nyssa/.blade/libs/qi/tests/test.b similarity index 82% rename from apps/qi/tests/test.b rename to apps/nyssa/.blade/libs/qi/tests/test.b index 673e6dee..8fc2e96b 100644 --- a/apps/qi/tests/test.b +++ b/apps/nyssa/.blade/libs/qi/tests/test.b @@ -3,8 +3,8 @@ var can = { ounces: 12, } -describe('the can', || { - it('has 12 ounces', || { +describe('the can', @() { + it('has 12 ounces', @() { expect(can.ounces).to_be(12) class A { @@ -17,7 +17,7 @@ describe('the can', || { expect(10.5).to_be_number().to_be_less_than(20) }) - it('has a sophisticated name', || { + it('has a sophisticated name', @() { expect(can.name).to_be('pamplemousse') expect([1, 2, 3]).to_have_length(3) @@ -25,7 +25,7 @@ describe('the can', || { expect('').not().to_have_length(5) }) - it('should pass this tests', || { + it('should pass this tests', @() { class A { testing() {} @testing() {} @@ -46,8 +46,8 @@ describe('the can', || { }) def do_something(id) { - if id == 1 return || { do_another_thing() } - else return || { do_something_else() } + if id == 1 return @() { do_another_thing() } + else return @() { do_something_else() } } class Set { @@ -55,8 +55,8 @@ class Set { @itern() {} } -describe('grapefruits', || { - it('should be a grape', || { +describe('grapefruits', @() { + it('should be a grape', @() { expect('grapefruits').to_match('grape') expect(do_something(1)).to_be_function() @@ -64,12 +64,12 @@ describe('grapefruits', || { expect(Exception).to_be_class() }) - it('should be valid type', || { + it('should be valid type', @() { expect({age: 10}).to_be_dict() expect(bytes(0)).to_be_bytes() }) - it('should be enumerable', || { + it('should be enumerable', @() { expect([]).to_be_iterable() expect({}).to_be_iterable() expect(Set()).to_be_iterable() @@ -85,8 +85,8 @@ def drink_flavor(flavor) { # Do some other stuff } -describe('testing to throw', || { - it('throws on octopus', || { +describe('testing to throw', @() { + it('throws on octopus', @() { def drink_octopus() { drink_flavor('octopus') } diff --git a/apps/nyssa/app/commands/test.b b/apps/nyssa/app/commands/test.b index f8bd6eed..935a24e1 100644 --- a/apps/nyssa/app/commands/test.b +++ b/apps/nyssa/app/commands/test.b @@ -22,7 +22,7 @@ def run_test_files(files) { def run(value, options, success, error) { if os.dir_exists(setup.TEST_DIR) { - var files = iters.filter(os.read_dir(setup.TEST_DIR), | x | { + var files = iters.filter(os.read_dir(setup.TEST_DIR), @( x ) { return x != '.' and x != '..' and x.ends_with('.b') }) if files { diff --git a/apps/nyssa/app/server/router.b b/apps/nyssa/app/server/router.b index b694cc5d..ed6ee27c 100644 --- a/apps/nyssa/app/server/router.b +++ b/apps/nyssa/app/server/router.b @@ -63,7 +63,7 @@ def _setup_session(req, res) { } # bind '.clear_session()' to response - res.clear_session = || { + res.clear_session = @() { res.session = {} # just for simplicity diff --git a/apps/nyssa/app/server/template_ext.b b/apps/nyssa/app/server/template_ext.b index a48107fa..ef2952a6 100644 --- a/apps/nyssa/app/server/template_ext.b +++ b/apps/nyssa/app/server/template_ext.b @@ -1,9 +1,12 @@ import markdown +import highlight { highlight } import .util + + var md = markdown({ - # linkify: true, html: true, + highlight, }) def template_ext() { diff --git a/apps/nyssa/app/server/views.b b/apps/nyssa/app/server/views.b index d8b158bb..4e7b03b6 100644 --- a/apps/nyssa/app/server/views.b +++ b/apps/nyssa/app/server/views.b @@ -7,15 +7,15 @@ import .util import ..setup var doc_files = [ - 'installing-nyssa.md', + 'getting-started.md', 'creating-projects.md', - 'publishing-packages.md', - 'managing-dependencies.md', 'package-layout.md', + 'managing-dependencies.md', 'testing.md', 'test-globals.md', 'test-assertions.md', 'install-and-uninstall-actions.md', + 'publishing-packages.md', 'getting-project-info.md', 'hosting-a-private-repository.md', 'publishers-account.md', diff --git a/apps/nyssa/docs/commands.md b/apps/nyssa/docs/commands.md index e396d47f..56a1e2de 100644 --- a/apps/nyssa/docs/commands.md +++ b/apps/nyssa/docs/commands.md @@ -3,32 +3,33 @@ Usage: `nyssa` [ [-h] | [-v] ] [COMMAND] ### OPTIONS -- `-h`, `--help` Show this help message and exit -- `-v`, `--version` Show Nyssa version +- `-h`, `--help` Show this help message and exit +- `-v`, `--version` Show Nyssa version ### COMMANDS -- **account** <_choice_> Manages a Nyssa publisher account - - `create` creates a new publisher account - - `login` login to a publisher account - - `logout` log out of a publisher account - - *`-r`*, *`--repo`* <_value_> the repo where the account is located +- **account** <_choice_> Manages a Nyssa publisher account + - `create` creates a new publisher account + - `login` login to a publisher account + - `logout` log out of a publisher account + - *`-r`*, *`--repo`* <_value_> the repo where the account is located - **clean** Clear Nyssa storage - - *`-c`*, *`--cache`* clean packages cache - - *`-l`*, *`--logs`* clean logs - - *`-a`*, *`--all`* clean everything + - *`-c`*, *`--cache`* clean packages cache + - *`-l`*, *`--logs`* clean logs + - *`-a`*, *`--all`* clean everything - **info** Shows current project information - **init** Creates a new package in current directory - - *`-n`*, *`--name`* <_value_> the name of the package + - *`-n`*, *`--name`* <_value_> the name of the package - **install** <_value_> Installs a Blade package - - *`-g`*, *`--global`* installs the package globally - - *`-c`*, *`--use-cache`* enables the cache - - *`-r`*, *`--repo`* <*value*> the repository to install from + - *`-g`*, *`--global`* installs the package globally + - *`-c`*, *`--use-cache`* enables the cache + - *`-r`*, *`--repo`* <*value*> the repository to install from - **publish** Publishes a repository - - *`-r`*, *`--repo`* <*value*> repository url + - *`-r`*, *`--repo`* <*value*> repository url - **restore** Restores all project dependencies - - *`-x`*, *`--no-cache`* disables the cache + - *`-x`*, *`--no-cache`* disables the cache - **serve** Starts a local Nyssa repository server - - *`-p`*, *`--port`* <*value*> port of the server (default: 3000) - - *`-n`*, *`--host`* <*value*> the host ip (default: 127.0.0.1) -- **uninstall** <*value*> Uninstalls a Blade package - - *`-g`*, *`--global`* package is a global package \ No newline at end of file + - *`-p`*, *`--port`* <*value*> port of the server (default: 3000) + - *`-n`*, *`--host`* <*value*> the host ip (default: 127.0.0.1) +- **test** Run the tests +- **uninstall** <*value*> Uninstalls a Blade package + - *`-g`*, *`--global`* package is a global package \ No newline at end of file diff --git a/apps/nyssa/docs/installing-nyssa.md b/apps/nyssa/docs/getting-started.md similarity index 88% rename from apps/nyssa/docs/installing-nyssa.md rename to apps/nyssa/docs/getting-started.md index 9244f1ed..18fce308 100644 --- a/apps/nyssa/docs/installing-nyssa.md +++ b/apps/nyssa/docs/getting-started.md @@ -34,11 +34,11 @@ OPTIONS: COMMANDS: account Manages a Nyssa publisher account - create creates a new publisher account - login login to a publisher account - logout log out of a publisher account + create Creates a new publisher account + login Login to a publisher account + logout Log out of a publisher account -r, --repo the repo where the account is located - clean Clear Nyssa storage + clean Clear Nyssa storage and cache -c, --cache clean packages cache -l, --logs clean logs -a, --all clean everything @@ -49,13 +49,14 @@ COMMANDS: -g, --global installs the package globally -c, --use-cache enables the cache -r, --repo the repository to install from - publish Publishes a repository + publish Publishes a Blade package to a repository -r, --repo repository url restore Restores all project dependencies -x, --no-cache disables the cache serve Starts a local Nyssa repository server -p, --port port of the server (default: 3000) -n, --host the host ip (default: 127.0.0.1) + test Run the tests uninstall Uninstalls a Blade package -g, --global package is a global package ``` diff --git a/apps/nyssa/docs/install-and-uninstall-actions.md b/apps/nyssa/docs/install-and-uninstall-actions.md index 8e9dd898..3dc85ea2 100644 --- a/apps/nyssa/docs/install-and-uninstall-actions.md +++ b/apps/nyssa/docs/install-and-uninstall-actions.md @@ -26,7 +26,7 @@ The `pre_uninstall` configuration is much like the `post_install` configuration, The `cli` installation hook allows package authors to specify a script that serves as the CLI entry point to the application. When the _CLI_ script is specified, a CLI entry will be created at `.blade` for local installations or at the root of Blade for global installations. This files will be automatically removed during uninstallation. -For example, the testing framework `qi` specifies a CLI entry point. For this reason, when you install `qi` locally, you can run the Qi command by simply running the command `.blade/qi` (or `.blade\qi` for Windows) to run your tests. This is made possible because during installation, Nyssa will automatically create the corresponding command-line entry file for you. +For example, the testing framework `qi` specifies a CLI entry point. For this reason, when you install `qi` locally, you can run the Qi command by simply running the command `nyssa test` (or `.blade\qi` for Windows) to run your tests. This is made possible because during installation, Nyssa will automatically create the corresponding command-line entry file for you. For applications installated globally, the application will become available on the user terminal **via the name** of the application provided that Blade has been added to path during installation. diff --git a/apps/nyssa/docs/test-assertions.md b/apps/nyssa/docs/test-assertions.md index 4161fa34..bee33f26 100644 --- a/apps/nyssa/docs/test-assertions.md +++ b/apps/nyssa/docs/test-assertions.md @@ -2,50 +2,14 @@ When writing tests you often need to check that a value meets certain criterias. The `expect()` function gives you access to an array of assertions that lets you test against different conditions. This page describes assertions, how to use them and all possible assertions in Qi. -## Reference - -- [Test Assertions](#test-assertions) - - [Reference](#reference) - - [expect()](#expect) - - [Modifiers](#modifiers) - - [not()](#not) - - [Matchers](#matchers) - - [to\_be(value)](#to_bevalue) - - [to\_be\_nil()](#to_be_nil) - - [to\_be\_defined()](#to_be_defined) - - [to\_be\_truthy()](#to_be_truthy) - - [to\_be\_falsy()](#to_be_falsy) - - [to\_be\_greater\_than(number)](#to_be_greater_thannumber) - - [to\_be\_greater\_than\_or\_equal(number)](#to_be_greater_than_or_equalnumber) - - [to\_be\_less\_than(number)](#to_be_less_thannumber) - - [to\_be\_less\_than\_or\_equal(number)](#to_be_less_than_or_equalnumber) - - [to\_match(value)](#to_matchvalue) - - [to\_contain(item)](#to_containitem) - - [to\_throw(error)](#to_throwerror) - - [to\_have\_length(number)](#to_have_lengthnumber) - - [to\_be\_instance\_of(class)](#to_be_instance_ofclass) - - [to\_be\_function(value)](#to_be_functionvalue) - - [to\_have\_property(name, value?)](#to_have_propertyname-value) - - [to\_have\_method(name)](#to_have_methodname) - - [to\_have\_decorator(name)](#to_have_decoratorname) - - [to\_be\_boolean()](#to_be_boolean) - - [to\_be\_number()](#to_be_number) - - [to\_be\_string()](#to_be_string) - - [to\_be\_list()](#to_be_list) - - [to\_be\_dict()](#to_be_dict) - - [to\_be\_class()](#to_be_class) - - [to\_be\_iterable()](#to_be_iterable) - - [to\_be\_file()](#to_be_file) - - [to\_be\_bytes()](#to_be_bytes) - ## expect() The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. Instead, you will use `expect` along with a "matcher" function to assert something about a value. Let's say you have a method `name_of_app()` which is supposed to return the string `'qi'`. Here's how you would test that: -```py -describe('Name of app test', || { - it('should be qi', || { +```blade +describe('Name of app test', @() { + it('should be qi', @() { expect(name_of_app()).to_be('qi') }) }) @@ -61,9 +25,9 @@ The argument to `expect` should be the value that your code produces, and any ar If you know how to test something, `.not()` lets you test its opposite. For example, this code tests that the name of the application is `not` `'qi'`. -```py -describe('Name of app test', || { - it('should be qi', || { +```blade +describe('Name of app test', @() { + it('should be qi', @() { expect(name_of_app()).not().to_be('qi') }) }) @@ -75,7 +39,7 @@ describe('Name of app test', || { > > Matchers can be nested. For example, > -> ```py +> ```blade > expect(10.5).to_be_number().to_be_less_than(20) > ``` @@ -84,18 +48,18 @@ describe('Name of app test', || { Use `.to_be` to compare primitive values or to check referential identity of object instances. For example, this code will validate some properties of the `can` object: -```py +```blade var can = { name: 'pamplemousse', ounces: 12, } -describe('the can', || { - it('has 12 ounces', || { +describe('the can', @() { + it('has 12 ounces', @() { expect(can.ounces).to_be(12) }) - it('has a sophisticated name', || { + it('has a sophisticated name', @() { expect(can.name).to_be('pamplemousse') }) }) @@ -105,12 +69,12 @@ describe('the can', || { `.to_be_nil()` is the same as `.to_be(nil)` but the error messages are a bit nicer. So use `.to_be_nil()` when you want to check that something is nil. -```py +```blade def bloop() { return nil } -it('should return nil', || { +it('should return nil', @() { expect(bloop()).to_be_nil() }) ``` @@ -119,7 +83,7 @@ it('should return nil', || { Use `.to_be_defined` to check that a variable is not `nil`. For example, if you want to check that a function `fetch_new_flavor_idea()` returns something, you can write: -```py +```blade expect(fetch_new_flavor_idea()).to_be_defined() ``` @@ -129,17 +93,17 @@ You could also write `expect(fetch_new_flavor_idea()).not().to_be_nil()` as they Use `.to_be_truthy` when you don't care what a value is and you want to ensure a value is true in a boolean context. For example, let's say you have some application code that looks like: -```py +```blade drink_some_lacroix() -if (thirsty()) { +if thirsty() { drink_more_lacroix() } ``` You may not care what `get_errors` returns, specifically - it might return `true`, `[1]`, or anything that's true in Blade, and your code would still work. So if you want to test you are thirsty before drinking some La Croix, you could write: -```py -it('should be thirsty before drinking La Croix', || { +```blade +it('should be thirsty before drinking La Croix', @() { drink_some_lacroix() expect(thirsty()).to_be_truthy() }) @@ -149,17 +113,17 @@ it('should be thirsty before drinking La Croix', || { Use `.to_be_falsy` when you don't care what a value is and you want to ensure a value is false in a boolean context. For example, let's say you have some application code that looks like: -```py +```blade drink_some_lacroix() -if (!get_errors()) { +if !get_errors() { drink_more_lacroix() } ``` You may not care what `get_errors` returns, specifically - it might return `false`, `nil`, or `-1`, and your code would still work. So if you want to test there are no errors after drinking some La Croix, you could write: -```py -it('does not lead to errors when drinking La Croix', || { +```blade +it('does not lead to errors when drinking La Croix', @() { drink_some_lacroix() expect(get_errors()).to_be_falsy() }) @@ -169,8 +133,8 @@ it('does not lead to errors when drinking La Croix', || { Use `.to_be_greater_than` to compare `received > expected` for number or `received.length() > expected` for string. For example, test that `ounces_per_can()` returns a value of more than 10 ounces: -```py -it('is more than 10 ounces per can', || { +```blade +it('is more than 10 ounces per can', @() { expect(ounces_per_can()).to_be_greater_than(10) }) ``` @@ -179,8 +143,8 @@ it('is more than 10 ounces per can', || { Use `.to_be_greater_than_or_equal` to compare `received >= expected` for number or `received.length() >= expected` for string. For example, test that `ounces_per_can()` returns a value of more than or equal to 10 ounces: -```py -it('is more than or equal to 10 ounces per can', || { +```blade +it('is more than or equal to 10 ounces per can', @() { expect(ounces_per_can()).to_be_greater_than_or_equal(10) }) ``` @@ -189,8 +153,8 @@ it('is more than or equal to 10 ounces per can', || { Use `.to_be_less_than` to compare `received < expected` for number or `received.length() < expected` for string. For example, test that `ounces_per_can()` returns a value of less than 10 ounces: -```py -it('is less than 10 ounces per can', || { +```blade +it('is less than 10 ounces per can', @() { expect(ounces_per_can()).to_be_less_than(10) }) ``` @@ -199,8 +163,8 @@ it('is less than 10 ounces per can', || { Use `.to_be_less_than_or_equal` to compare `received <= expected` for number or `received.length() <= expected` for string. For example, test that `ounces_per_can()` returns a value of less than or equal to 10 ounces: -```py -it('is less than or equal to 10 ounces per can', || { +```blade +it('is less than or equal to 10 ounces per can', @() { expect(ounces_per_can()).to_be_less_than_or_equal(10) }) ``` @@ -211,9 +175,9 @@ Use `.to_match` to check that a string matches a regular expression. For example, you might not know what exactly `essay_on_the_best_flavor()` returns, but you know it's a really long string, and the substring grapefruit should be in there somewhere. You can test this with: -```py -describe('an essay on the best flavor', || { - it('mentions grapefruit', || { +```blade +describe('an essay on the best flavor', @() { + it('mentions grapefruit', @() { expect(essay_on_the_best_flavor()).to_match('/grapefruit/i') }) }) @@ -221,9 +185,9 @@ describe('an essay on the best flavor', || { This matcher also accepts a string, which it will try to match: -```py -describe('grapefruits', || { - it('should be a grape', || { +```blade +describe('grapefruits', @() { + it('should be a grape', @() { expect('grapefruits').to_match('grape') }) }) @@ -235,8 +199,8 @@ Use `.to_contain` when you want to check that an item is in an list or dictionar For example, if `get_all_flavors()` returns an list of flavors and you want to be sure that lime is in there, you can write: -```py -it('should contain lime', || { +```blade +it('should contain lime', @() { expect(get_all_flavors()).to_contain('lime') }) ``` @@ -245,9 +209,9 @@ it('should contain lime', || { Use `.to_throw` to test that a function throws when it is called. For example, if we want to test that `drink_flavor('octopus')` throws, because octopus flavor is too disgusting to drink, we could write: -```py -it('throws on octopus', || { - expect(|| { +```blade +it('throws on octopus', @() { + expect(@() { drink_flavor('octopus') }).to_throw() }) @@ -266,7 +230,7 @@ You can provide an optional argument to test that a specific error is thrown: For example, let's say `drink_flavor()` looks like this: -```py +```blade def drink_flavor(flavor) { if flavor == 'octopus' { die DisgustingFlavorError('yuck, octopus flavor') @@ -277,8 +241,8 @@ def drink_flavor(flavor) { We could test the error thrown in several ways: -```py -it('throws on octopus', || { +```blade +it('throws on octopus', @() { def drink_octopus() { drink_flavor('octopus') } @@ -300,7 +264,7 @@ it('throws on octopus', || { Use `.to_have_length` to check that an object has a .length property and it is set to a certain numeric value. For example: -```py +```blade expect([1, 2, 3]).to_have_length(3) expect('abc').to_have_length(3) expect('').not().to_have_length(5) @@ -310,7 +274,7 @@ expect('').not().to_have_length(5) Use `.to_be_instance_of(class)` to check that an object is an instance of a class. This matcher uses `instance_of` underneath. -```py +```blade class A {} expect(A()).to_be_instance_of(A) @@ -321,16 +285,16 @@ expect(A()).to_be_instance_of(Exception) # fails Use `.to_be_function` when you want to check if a value is a function or a closure. For example, if `do_something()` is a function looking like this: -```py +```blade def do_something(id) { - if id == 1 return || { do_another_thing() } - else return || { do_something_else() } + if id == 1 return @() { do_another_thing() } + else return @() { do_something_else() } } ``` We can test that `do_something()` correctly returns a function. -```py +```blade expect(do_something(1)).to_be_function() ``` @@ -339,7 +303,7 @@ expect(do_something(1)).to_be_function() Use `.to_have_property` to check if an object has a given property. You can provide an optional value argument to compare the received property value against an expected value. -```py +```blade class A { var name = 'something' } @@ -354,7 +318,7 @@ expect(A()).to_have_property('name', 'something') Use the `.to_have_method` to check if an object is an instance of a class having a particular method. For example, let's say you have a class `A` and `B` defined as follows: -```py +```blade class A { testing() {} } @@ -366,7 +330,7 @@ class B { and you have a function `return_class()` that could return an instance of any of `A` or `B`, you can test the output of that method like, -```py +```blade expect(return_class()).to_have_method('testing') ``` @@ -374,7 +338,7 @@ expect(return_class()).to_have_method('testing') Use the `.to_have_decorator` to check if an object is an instance of a class having a particular decorator. For example, let's say you have a class `A` and `B` defined as follows: -```py +```blade class A { @testing() {} } @@ -386,7 +350,7 @@ class B { and you have a function `return_class()` that could return an instance of any of `A` or `B`, you can test the output of that method like, -```py +```blade expect(return_class()).to_have_decorator('testing') ``` @@ -394,8 +358,8 @@ expect(return_class()).to_have_decorator('testing') Use `.to_be_boolean` to check for `true` or `false` values. For example, test that `user_is_admin()` returns a value of `true` or `false`: -```py -it('should be true or false', || { +```blade +it('should be true or false', @() { expect(user_is_admin()).to_be_boolean() }) ``` @@ -404,8 +368,8 @@ it('should be true or false', || { Use `.to_be_number` to check that a value is a number without requiring any specific number. For example, test that `number_of_cans()` returns a valid number: -```py -it('should be a number', || { +```blade +it('should be a number', @() { expect(number_of_cans()).to_be_number() }) ``` @@ -414,8 +378,8 @@ it('should be a number', || { Use `.to_be_string` to check that a value is a string without requiring any specific content. For example, test that `name_of_king()` returns a valid string: -```py -it('should be a string', || { +```blade +it('should be a string', @() { expect(name_of_king()).to_be_string() }) ``` @@ -424,8 +388,8 @@ it('should be a string', || { Use `.to_be_list` to check that a value is a list without requiring any specific content. For example, test that `fruits()` returns a valid list: -```py -it('should be a string', || { +```blade +it('should be a string', @() { expect(fruits()).to_be_list() }) ``` @@ -434,8 +398,8 @@ it('should be a string', || { Use `.to_be_dict` to check that a value is a dictionary without requiring any specific content. For example, test that `{age: 10}` returns a valid dictionary: -```py -it('should be a dictionary', || { +```blade +it('should be a dictionary', @() { expect({age: 10}).to_be_dict() }) ``` @@ -444,8 +408,8 @@ it('should be a dictionary', || { Use `.to_be_class` to check that a value is a class and not an instance. For example, test that `Exception` is actually a class: -```py -it('should be a list', || { +```blade +it('should be a list', @() { expect(Exception).to_be_class() }) ``` @@ -454,7 +418,7 @@ it('should be a list', || { Use `.to_be_iterable` to check that a value is an iterable whether its of basic types (e.g. String, List etc.) or an iterable class. For example, suppose we have a class `Set` defined ass follows: -```py +```blade class Set { @iter() {} @itern() {} @@ -463,8 +427,8 @@ class Set { The following test will show that it's as much an iterable as a list or dictionary can be. -```py -it('should be enumerable', || { +```blade +it('should be enumerable', @() { expect([]).to_be_iterable() expect({}).to_be_iterable() expect(Set()).to_be_iterable() @@ -475,7 +439,7 @@ it('should be enumerable', || { Use `.to_be_file` to check that a value is a file object. For example, you can test that an handle `fh` returned by the function `get_config()` is actually a file like this: -```py +```blade var fh = get_config() expect(fh).to_be_file() @@ -485,7 +449,7 @@ expect(fh).to_be_file() Use `.to_be_bytes` to check that a value is an array of bytes. For example, -```py +```blade expect(bytes(0)).to_be_bytes() ``` diff --git a/apps/nyssa/docs/test-globals.md b/apps/nyssa/docs/test-globals.md index e025fd38..e1ddffd1 100644 --- a/apps/nyssa/docs/test-globals.md +++ b/apps/nyssa/docs/test-globals.md @@ -2,37 +2,24 @@ Qi exposes a set of global functions for use in your test files by putting each of these methods and objects into the global environment. You don't have to require or import anything to use them from your test files. -## Reference - -- [Test Globals](#test-globals) - - [Reference](#reference) - - [Methods](#methods) - - [describe(name, fn)](#describename-fn) - - [it(name, fn)](#itname-fn) - - [Hooks](#hooks) - - [before\_all(fn)](#before_allfn) - - [after\_all(fn)](#after_allfn) - - [before\_each(fn)](#before_eachfn) - - [after\_each(fn)](#after_eachfn) - ## Methods ### describe(name, fn) `describe(name, fn)` creates a block that groups together several related tests. It is the Test Suite. For example, if you have a `myBeverage` object that is supposed to be delicious but not sour, you could test it with: -```py +```blade var myBeverage = { delicious: true, sour: false, } -describe('my beverage', || { - it('should be delicious', || { +describe('my beverage', @() { + it('should be delicious', @() { expect(myBeverage.delicious).to_be_truthy() }); - it('should be sour', || { + it('should be sour', @() { expect(myBeverage.sour).to_be_falsy() }) }) @@ -40,8 +27,8 @@ describe('my beverage', || { You can also nest `describe` blocks if you have a hierarchy of tests: -```py -var binay_string_to_number = | bin_string | { +```blade +var binay_string_to_number = @( bin_string ) { if !bin_string.match('/^[01]+$/') { die CustomError('Not a binary number.') } @@ -49,19 +36,19 @@ var binay_string_to_number = | bin_string | { return to_number('0b' + bin_string) } -describe('binay string to number', || { - describe('given an invalid binary string', || { - it('throws CustomError when composed of non-numbers', || { - expect(|| { binay_string_to_number('abc') }).to_throw(CustomError) +describe('binay string to number', @() { + describe('given an invalid binary string', @() { + it('throws CustomError when composed of non-numbers', @() { + expect(@() { binay_string_to_number('abc') }).to_throw(CustomError) }) - it('throws CustomError when having extra whitespace', || { - expect(|| { binay_string_to_number(' 100') }).to_throw(CustomError) + it('throws CustomError when having extra whitespace', @() { + expect(@() { binay_string_to_number(' 100') }).to_throw(CustomError) }) }) - describe('given a valid binary string', || { - it('returns the correct number', || { + describe('given a valid binary string', @() { + it('returns the correct number', @() { expect(binay_string_to_number('100')).to_be(4) }) }) @@ -70,10 +57,10 @@ describe('binay string to number', || { ### it(name, fn) -The `it(name, fn)` function is the entry point for tests in a test suite. For example, let's say there's a function `inches_of_rain()` that should be zero. Your whole test could be: +The `it(name, fn)` function is the entry point for tests in a test suite. For example, let's say there's a function `inches_of_rain()` that should return zero. Your whole test could be: -```py -it('did not rain', || { +```blade +it('did not rain', @() { expect(inches_of_rain()).to_be(0) }) ``` @@ -95,21 +82,21 @@ Runs a function before each of the tests in the test suite run. This is often us For example: -```py +```blade var global_db = make_global_db() -before_all(|| { +before_all(@() { # Clears the database and adds some testing data. - return globalDatabase.clear(|| { + return globalDatabase.clear(@() { return globalDatabase.insert({testData: 'foo'}) }) }) # Since we only set up the database once in this example, it's important # that our tests don't modify it. -describe('Before all', || { - it('can find things', || { - return global_db.find('thing', {}, |results| { +describe('Before all', @() { + it('can find things', @() { + return global_db.find('thing', {}, @(results) { expect(results.length()).to_be_greater_than(0) }) }) @@ -124,26 +111,26 @@ Runs a function after all the tests in a test suite have completed. This is ofte For example: -```py +```blade var global_db = make_global_db() def clean_up_db(db) { db.clean_up() } -after_all(|| { +after_all(@() { clean_up_db(global_db) }); -describe('confirming after_all works', || { - it('can find things', || { - return global_db.find('thing', {}, |results| { +describe('confirming after_all works', @() { + it('can find things', @() { + return global_db.find('thing', {}, @(results) { expect(results.length()).to_be_greater_than(0) }) }) - it('can insert a thing', || { - return global_db.insert('thing', make_thing(), |response| { + it('can insert a thing', @() { + return global_db.insert('thing', make_thing(), @(response) { expect(response.success).to_be_truthy() }) }) @@ -160,24 +147,24 @@ Runs a function before each of the tests in the test suite runs. This is often u For example: -```py +```blade var global_db = make_global_db() -before_each(|| { +before_each(@() { # Clears the database and adds some testing data. global_db.clear() global_db.insert({testData: 'foo'}); }) -describe('confirming before_each works', || { - it('can find things', || { - return global_db.find('thing', {}, |results| { +describe('confirming before_each works', @() { + it('can find things', @() { + return global_db.find('thing', {}, @(results) { expect(results.length()).to_be_greater_than(0) }) }) - it('can insert a thing', || { - return global_db.insert('thing', make_thing(), |response| { + it('can insert a thing', @() { + return global_db.insert('thing', make_thing(), @(response) { expect(response.success).to_be_truthy() }) }) @@ -192,26 +179,26 @@ Runs a function after each one of the tests in this file completes. This is ofte For example: -```py +```blade var global_db = make_global_db() def clean_up_db(db) { db.clean_up() } -after_each(|| { +after_each(@() { clean_up_db(global_db) }) -describe('confirming after_each works', || { - it('can find things', || { - return global_db.find('thing', {}, |results| { +describe('confirming after_each works', @() { + it('can find things', @() { + return global_db.find('thing', {}, @(results) { expect(results.length()).to_be_greater_than(0) }) }) - it('can insert a thing', || { - return global_db.insert('thing', make_thing(), |response| { + it('can insert a thing', @() { + return global_db.insert('thing', make_thing(), @(response) { expect(response.success).to_be_truthy() }) }) diff --git a/apps/nyssa/docs/testing.md b/apps/nyssa/docs/testing.md index 07a7607d..424c54c5 100644 --- a/apps/nyssa/docs/testing.md +++ b/apps/nyssa/docs/testing.md @@ -8,7 +8,7 @@ Both Nyssa and `Qi` ship with Blade allowing you write comprehensive tests witho Let's write a test for a hypothetical function that returns the product of two numbers. First, we'll create a file `prod.b` that contains the following code: -```py +```blade def prod(x, y) { return x * y } @@ -16,11 +16,11 @@ def prod(x, y) { Now, let's create a test for it by creating a file `prod.test.b` in the `tests` directory and add the following code to it. -```py +```blade import ..prod -describe('Product test suite', || { - it('should return 6 for 2 and 3', || { +describe('Product test suite', @() { + it('should return 6 for 2 and 3', @() { expect(prod(2, 3)).to_be(6) }) }) @@ -31,7 +31,7 @@ describe('Product test suite', || { Now let's run the test. If you have installed Qi using `nyssa` (which is recommended), then you can run the following command at the root directory (the directory that contains the `tests` folder). ```sh -.blade/qi +nyssa test ``` You should get an output similar to this: diff --git a/apps/nyssa/public/css/style.css b/apps/nyssa/public/css/style.css index 0219f95b..496186c9 100644 --- a/apps/nyssa/public/css/style.css +++ b/apps/nyssa/public/css/style.css @@ -29,6 +29,9 @@ input:focus { pre { white-space: pre-wrap; border-radius: .25rem; + color: hsl(210, 20%, 30%); + background: hsl(210, 0%, 99%); + border: solid 1px hsl(200, 20%, 88%); } .columns { @@ -265,13 +268,18 @@ pre.has-icon .icon { line-height: 1.5; } +.content.doc { + font-size: .95em; + line-height: 1.65; +} + .content.doc h2 { border-bottom: 1px solid rgba(0, 0, 0, .1); padding-bottom: .5em; } .content.doc h1 { - border-bottom: 4px solid #0000001a; + border-bottom: 2px solid #0000001a; padding-bottom: .5rem; } @@ -287,6 +295,10 @@ pre.has-icon .icon { color: inherit; } +.content.doc blockquote pre { + margin-bottom: 1em; +} + .content.doc p { margin: 1em 0; } diff --git a/apps/nyssa/templates/doc.html b/apps/nyssa/templates/doc.html index 0aa0c2c7..c3765009 100644 --- a/apps/nyssa/templates/doc.html +++ b/apps/nyssa/templates/doc.html @@ -9,21 +9,25 @@
-
- -
-
-
{{ content|draw }}
+
+
+
+ +
+
+
{{ content|draw }}
+
+
diff --git a/apps/qi/tests/global.b b/apps/qi/tests/global.b deleted file mode 100644 index 49dcb3b7..00000000 --- a/apps/qi/tests/global.b +++ /dev/null @@ -1,42 +0,0 @@ -var myBeverage = { - delicious: true, - sour: false, -} - -describe('my beverage', || { - it('should be delicious', || { - expect(myBeverage.delicious).to_be_truthy() - }); - - it('should be sour', || { - expect(myBeverage.sour).to_be_falsy() - }) -}) - -class CustomError < Exception {} - -var binay_string_to_number = | bin_string | { - if !bin_string.match('/^[01]+$/') { - die CustomError('Not a binary number.') - } - - return to_number('0b' + bin_string) -} - -describe('binay string to number', || { - describe('given an invalid binary string', || { - it('throws CustomError when composed of non-numbers', || { - expect(|| { binay_string_to_number('abc') }).to_throw(CustomError) - }) - - it('throws CustomError when having extra whitespace', || { - expect(|| { binay_string_to_number(' 100') }).to_throw(CustomError) - }) - }) - - describe('given a valid binary string', || { - it('returns the correct number', || { - expect(binay_string_to_number('100')).to_be(4) - }) - }) -}) diff --git a/apps/qi/tests/sample.spec.b b/apps/qi/tests/sample.spec.b deleted file mode 100644 index 38241363..00000000 --- a/apps/qi/tests/sample.spec.b +++ /dev/null @@ -1,12 +0,0 @@ -describe('Some testing', || { - it('should match exactly!', || { - expect(3).to_be(3) - expect(5).to_be(5) - expect(5).not().to_be(56) - - class X {} - - var b = [] - expect(|| { return b[5] }).not().to_throw(X) - }) -}) \ No newline at end of file diff --git a/apps/qi/tests/sample2.spec.b b/apps/qi/tests/sample2.spec.b deleted file mode 100644 index cfadc926..00000000 --- a/apps/qi/tests/sample2.spec.b +++ /dev/null @@ -1,11 +0,0 @@ -describe('Some testing', || { - it('should match exactly!', || { - expect(3).to_be(3) - expect(5).to_be(5) - - class X {} - - var b = [] - expect(|| { return b[5] }).to_throw() - }) -}) \ No newline at end of file diff --git a/apps/qi/tests/setup.spec.b b/apps/qi/tests/setup.spec.b deleted file mode 100644 index 08ef5100..00000000 --- a/apps/qi/tests/setup.spec.b +++ /dev/null @@ -1,10 +0,0 @@ -import ..setup - -describe('Importing', || { - it('should have a valid command', || { - expect(setup.cmd).to_be_string() - }) - it('should have a correct file path', || { - expect(setup.path).to_contain('qi') - }) -}) diff --git a/libs/markdown/block/list.b b/libs/markdown/block/list.b index ffe45f2d..40cf359f 100644 --- a/libs/markdown/block/list.b +++ b/libs/markdown/block/list.b @@ -37,7 +37,7 @@ def _skip_ordered_list_marker(state, start_line) { ch = state.src[pos++ - 1] - if ord(ch) < ord('0') or ord(ch) > ord('9') return -1 + if ord(ch) < 0x30 /* 0 */ or ord(ch) > 0x39 /* 9 */ return -1 iter ;; { # EOL -> fail @@ -45,7 +45,7 @@ def _skip_ordered_list_marker(state, start_line) { ch = state.src[pos++ - 1] - if ord(ch) >= ord('0') and ord(ch) <= ord('9') { + if ord(ch) >= 0x30 and ord(ch) <= 0x39 { # List marker should have no more than 9 digits # (prevents integer overflow in browsers) diff --git a/libs/markdown/index.b b/libs/markdown/index.b index 871d26f7..9ad315c0 100644 --- a/libs/markdown/index.b +++ b/libs/markdown/index.b @@ -148,6 +148,10 @@ def encode_url(string, exclude, keep_escaped) { } def normalize_link(uri) { + if uri.starts_with('#') or uri.starts_with('?') { + return encode_url(uri) + } + var parsed = url.parse(uri) if parsed.host { @@ -407,8 +411,8 @@ class Markdown { * }) * ``` * - * @param {string|nil} preset_name: `commonmark`, `standard` or `zero` (default: `standard`) - * @param {dict|nil} options + * @param {string?} preset_name: `commonmark`, `standard` or `zero` (default: `standard`) + * @param {dict?} options */ Markdown(preset_name, options) { if !instance_of(self, Markdown) { @@ -620,7 +624,7 @@ class Markdown { * in [[Markdown.parse]]. * * @param {string} src: source string - * @param {object|nil} env: environment sandbox + * @param {object?} env: environment sandbox * @return string */ render(src, env) { @@ -635,7 +639,7 @@ class Markdown { * tokens in `children` property. Also updates `env` object. * * @param {string} src: source string - * @param {object|nil} env: environment sandbox + * @param {object?} env: environment sandbox * @return list * @internal **/ @@ -653,7 +657,7 @@ class Markdown { * will NOT be wrapped into `

` tags. * * @param {string} src: source string - * @param {object|nil} env: environment sandbox + * @param {object?} env: environment sandbox * @return string */ render_inline(src, env) { diff --git a/libs/markdown/renderer.b b/libs/markdown/renderer.b index 783ace8a..a278a2ee 100644 --- a/libs/markdown/renderer.b +++ b/libs/markdown/renderer.b @@ -32,7 +32,7 @@ default_rules.fence = @(tokens, idx, options, env, slf) { } if options.highlight { - highlighted = options.highlight(token.content, lang_name, lang_attrs) or escape_html(token.content) + highlighted = options.highlight(token.content, lang_name) or escape_html(token.content) } else { highlighted = escape_html(token.content) } diff --git a/libs/markdown/ruler.b b/libs/markdown/ruler.b index 4e21a80d..d039a5aa 100644 --- a/libs/markdown/ruler.b +++ b/libs/markdown/ruler.b @@ -91,7 +91,7 @@ class Ruler { * * @param {string} name: rule name to replace. * @param {function} fn: new rule function. - * @param {dict|nil} options: new rule options (optional). + * @param {dict?} options: new rule options (optional). */ at(name, fn, options) { var index = self.__find__(name) @@ -125,7 +125,7 @@ class Ruler { * @param {string} before_name: new rule will be added before this one. * @param {string} rule_name: name of added rule. * @param {function} fn: rule function. - * @param {dict|nil} options: rule options (optional). + * @param {dict?} options: rule options (optional). */ before(before_name, rule_name, fn, options) { var index = self.__find__(before_name) @@ -165,7 +165,7 @@ class Ruler { * @param {string} after_name: new rule will be added after this one. * @param {string} rule_name: name of added rule. * @param {function} fn: rule function. - * @param {dict|nil} options: rule options (optional). + * @param {dict?} options: rule options (optional). */ after(after_name, rule_name, fn, options) { var index = self.__find__(after_name) @@ -204,7 +204,7 @@ class Ruler { * * @param {string} rule_name: name of added rule. * @param {function} fn: rule function. - * @param {dict|nil} options: rule options (optional). + * @param {dict?} options: rule options (optional). */ push(rule_name, fn, options) { var opt = options or {} From ed592152bdfd3b2db645176d1aa6717064db6608 Mon Sep 17 00:00:00 2001 From: Richard Ore Date: Sun, 18 Jun 2023 19:28:22 +0100 Subject: [PATCH 4/5] updated nyssa with qi --- apps/nyssa/.blade/libs/highlight.b | 76 ++++++++ .../{ => nyssa/.blade/libs}/qi/.gitattributes | 0 apps/{ => nyssa/.blade/libs}/qi/.gitignore | 0 apps/{ => nyssa/.blade/libs}/qi/README.md | 14 +- apps/{ => nyssa/.blade/libs}/qi/app/index.b | 0 apps/{ => nyssa/.blade/libs}/qi/app/lib.b | 58 +++--- apps/{ => nyssa/.blade/libs}/qi/index.b | 0 apps/{ => nyssa/.blade/libs}/qi/nyssa.json | 0 .../.blade/libs}/qi/tests/.gitignore | 0 apps/nyssa/.blade/libs/qi/tests/global.b | 42 ++++ apps/nyssa/.blade/libs/qi/tests/sample.spec.b | 12 ++ .../nyssa/.blade/libs/qi/tests/sample2.spec.b | 11 ++ apps/nyssa/.blade/libs/qi/tests/setup.spec.b | 10 + apps/{ => nyssa/.blade/libs}/qi/tests/test.b | 24 +-- apps/nyssa/app/commands/test.b | 2 +- apps/nyssa/app/server/router.b | 2 +- apps/nyssa/app/server/template_ext.b | 5 +- apps/nyssa/app/server/views.b | 8 +- apps/nyssa/docs/commands.md | 41 ++-- ...installing-nyssa.md => getting-started.md} | 11 +- .../docs/install-and-uninstall-actions.md | 2 +- apps/nyssa/docs/test-assertions.md | 180 +++++++----------- apps/nyssa/docs/test-globals.md | 101 +++++----- apps/nyssa/docs/testing.md | 10 +- apps/nyssa/public/css/style.css | 14 +- apps/nyssa/templates/doc.html | 34 ++-- apps/qi/tests/global.b | 42 ---- apps/qi/tests/sample.spec.b | 12 -- apps/qi/tests/sample2.spec.b | 11 -- apps/qi/tests/setup.spec.b | 10 - libs/markdown/block/list.b | 4 +- libs/markdown/index.b | 14 +- libs/markdown/renderer.b | 2 +- libs/markdown/ruler.b | 8 +- 34 files changed, 403 insertions(+), 357 deletions(-) create mode 100644 apps/nyssa/.blade/libs/highlight.b rename apps/{ => nyssa/.blade/libs}/qi/.gitattributes (100%) rename apps/{ => nyssa/.blade/libs}/qi/.gitignore (100%) rename apps/{ => nyssa/.blade/libs}/qi/README.md (82%) rename apps/{ => nyssa/.blade/libs}/qi/app/index.b (100%) rename apps/{ => nyssa/.blade/libs}/qi/app/lib.b (81%) rename apps/{ => nyssa/.blade/libs}/qi/index.b (100%) rename apps/{ => nyssa/.blade/libs}/qi/nyssa.json (100%) rename apps/{ => nyssa/.blade/libs}/qi/tests/.gitignore (100%) create mode 100644 apps/nyssa/.blade/libs/qi/tests/global.b create mode 100644 apps/nyssa/.blade/libs/qi/tests/sample.spec.b create mode 100644 apps/nyssa/.blade/libs/qi/tests/sample2.spec.b create mode 100644 apps/nyssa/.blade/libs/qi/tests/setup.spec.b rename apps/{ => nyssa/.blade/libs}/qi/tests/test.b (82%) rename apps/nyssa/docs/{installing-nyssa.md => getting-started.md} (88%) delete mode 100644 apps/qi/tests/global.b delete mode 100644 apps/qi/tests/sample.spec.b delete mode 100644 apps/qi/tests/sample2.spec.b delete mode 100644 apps/qi/tests/setup.spec.b diff --git a/apps/nyssa/.blade/libs/highlight.b b/apps/nyssa/.blade/libs/highlight.b new file mode 100644 index 00000000..031055ff --- /dev/null +++ b/apps/nyssa/.blade/libs/highlight.b @@ -0,0 +1,76 @@ +var blade_keywords = '|'.join([ + 'as', 'assert', 'break', 'catch', 'class', 'continue', + 'def', 'default', 'die', 'do', 'echo', 'else', 'finally', 'for', + 'if', 'import', 'in', 'iter', 'return', 'static', 'try', + 'using', 'var', 'when', 'while', +]) + +var constant_keywords = '|'.join([ + 'nil', 'parent', 'self', 'true', 'false', 'and', 'or' +]) + +def highlight_blade(text) { + text = text. + # operators + replace('/([+\-*=/%!<>@]|\.\.)/', '<_o>$1'). + replace('/\\b(and|or)\\b/', '<_o>$1'). + # quotes + replace('/((\'(?:[^\'\\\\]|\\.)*\')|("(?:[^"\\\\]|\\.)*"))/', '<_q>$1'). + # constant keywords + replace('/\\b(${constant_keywords})\\b/', '<_c>$1'). + + # numbers + replace('/(([0-9][0-9]*\.[0-9]+([eE][0-9]+)?[fd]?)|(0x[0-9a-fA-F]+)|(0c[0-7][0-7]*)|(0b[01][01]*)|([0-9]+))/', '<_n>$1'). + + # functions + # property/method call and access + replace('/(?<=\.)[ ]*([a-zA-Z_][a-zA-Z0-9_]*)[ ]*(?=[(])/', '<_m>$1'). + # definition and call + replace('/(?$1'). + + # keywords + replace('/\\b(${blade_keywords})\\b/', '<_k>$1'). + # comments + replace('/(#[^\\n]*|\/(?!\\\\)\*[\s\S]*?\*(?!\\\\)\/)/', '<_w>$1') + + # clean up comments + var comments = text.matches('/<_w>(.*?)<\/_w>/') + if comments { + for comment in comments[1] { + text = text.replace(comment, comment.replace('/<\/?_([^>]+)>/', ''), false) + } + } + + # clean up quotes + var quotes = text.matches('/<_q>(.*?)<\/_q>/') + if quotes { + for quote in quotes[1] { + text = text.replace( + quote, + quote.replace('/<\/?_([^>]+)>/', ''). + # interpolation + replace('/(\\$\{[^}]+\})/', '<_i>$1'), + false + ) + } + } + + # expand styles. + return text.replace('/<_q>(.*?)<\/_q>/', '$1'). + replace('/<_i>(.*?)<\/_i>/', '$1'). + replace('/<_c>(.*?)<\/_c>/', '$1'). + replace('/<_m>(.*?)<\/_m>/', '$1'). + replace('/<_f>(.*?)<\/_f>/', '$1'). + replace('/<_k>(.*?)<\/_k>/', '$1'). + replace('/<_w>(.*?)<\/_w>/', '$1'). + replace('/<_o>(.*?)<\/_o>/', '$1'). + replace('/<_n>(.*?)<\/_n>/', '$1') +} + +def highlight(text, lang) { + if lang == 'blade' { + return highlight_blade(text) + } + return text +} + diff --git a/apps/qi/.gitattributes b/apps/nyssa/.blade/libs/qi/.gitattributes similarity index 100% rename from apps/qi/.gitattributes rename to apps/nyssa/.blade/libs/qi/.gitattributes diff --git a/apps/qi/.gitignore b/apps/nyssa/.blade/libs/qi/.gitignore similarity index 100% rename from apps/qi/.gitignore rename to apps/nyssa/.blade/libs/qi/.gitignore diff --git a/apps/qi/README.md b/apps/nyssa/.blade/libs/qi/README.md similarity index 82% rename from apps/qi/README.md rename to apps/nyssa/.blade/libs/qi/README.md index 78f6e840..ebb60b25 100644 --- a/apps/qi/README.md +++ b/apps/nyssa/.blade/libs/qi/README.md @@ -14,13 +14,7 @@ A testing framework for Blade programming language. ### Getting started -Qi is a Nyssa package and can be installed using the command: - -``` -nyssa install qi -``` - -Qi is designed to run tests are out the directory `tests` and for this reason, all tests files must reside inside the `tests` directory. +Qi is a Nyssa package that ships along with it and is designed to run tests are out the directory `tests` and for this reason, all tests files must reside inside the `tests` directory. ### Writing a simple test @@ -37,8 +31,8 @@ Now, let's create a test for it by creating a file `prod.test.b` in the `tests` ```py import ..prod -describe('Product test suite', || { - it('should return 6 for 2 and 3', || { +describe('Product test suite', @() { + it('should return 6 for 2 and 3', @() { expect(prod(2, 3)).to_be(6) }) }) @@ -47,7 +41,7 @@ describe('Product test suite', || { Now let's run the test. If you have installed Qi using `nyssa` (which is recommended), then you can run the following command at the root directory (the directory that contains the `tests` folder). ``` -.blade/qi +nyssa test ``` You should get an output similar to this: diff --git a/apps/qi/app/index.b b/apps/nyssa/.blade/libs/qi/app/index.b similarity index 100% rename from apps/qi/app/index.b rename to apps/nyssa/.blade/libs/qi/app/index.b diff --git a/apps/qi/app/lib.b b/apps/nyssa/.blade/libs/qi/app/lib.b similarity index 81% rename from apps/qi/app/lib.b rename to apps/nyssa/.blade/libs/qi/app/lib.b index a017330e..239e146e 100644 --- a/apps/qi/app/lib.b +++ b/apps/nyssa/.blade/libs/qi/app/lib.b @@ -56,7 +56,7 @@ class expect { _run(name, expected, fn) { if self._is_not name = 'not ${name}' - if !fn fn = |x, y| { return x == y } + if !fn fn = @(x, y) { return x == y } var v = to_string(self.value).replace('\r', '\\r').replace('\n', '\\n') var w = expected != nil ? to_string(expected).replace('\r', '\\r').replace('\n', '\\n') : nil @@ -103,22 +103,22 @@ class expect { } to_be_defined() { - self._run('to be defined', nil, |x, y| { return x != nil }) + self._run('to be defined', nil, @(x, y) { return x != nil }) return self } to_be_truthy() { - self._run('to be truthy', nil, |x, y| { return !!x }) + self._run('to be truthy', nil, @(x, y) { return !!x }) return self } to_be_falsy() { - self._run('to be falsy', nil, |x, y| { return !x }) + self._run('to be falsy', nil, @(x, y) { return !x }) return self } to_be_greater_than(e) { - self._run('to be greather than', e, |x, y| { + self._run('to be greather than', e, @(x, y) { if is_string(x) return x.length() > y return x > y }) @@ -126,7 +126,7 @@ class expect { } to_be_greater_than_or_equal(e) { - self._run('to be greather than or equal to', e, |x, y| { + self._run('to be greather than or equal to', e, @(x, y) { if is_string(x) return x.length() >= y return x >= y }) @@ -134,7 +134,7 @@ class expect { } to_be_less_than(e) { - self._run('to be less than', e, |x, y| { + self._run('to be less than', e, @(x, y) { if is_string(x) return x.length() < y return x < y }) @@ -142,7 +142,7 @@ class expect { } to_be_less_than_or_equal(e) { - self._run('to be less than or equal to', e, |x, y| { + self._run('to be less than or equal to', e, @(x, y) { if is_string(x) return x.length() <= y return x <= y }) @@ -150,12 +150,12 @@ class expect { } to_match(e) { - self._run('to match', e, |x, y| { return x.match(y) }) + self._run('to match', e, @(x, y) { return x.match(y) }) return self } to_contain(e) { - self._run('to contain', e, |x, y| { + self._run('to contain', e, @(x, y) { if is_dict(x) return x.contains(y) return x.count(y) > 0 }) @@ -166,7 +166,7 @@ class expect { if !e e = Exception if is_function(self.value) { - self._run('to throw', e, |x, y| { + self._run('to throw', e, @(x, y) { try { x() return false @@ -185,12 +185,12 @@ class expect { } to_have_length(e) { - self._run('to have length', e, |x, y| { return x.length() == y }) + self._run('to have length', e, @(x, y) { return x.length() == y }) return self } to_be_instance_of(e) { - self._run('to be an instance of', e, |x, y| { return instance_of(x, y) }) + self._run('to be an instance of', e, @(x, y) { return instance_of(x, y) }) return self } @@ -198,7 +198,7 @@ class expect { var name = 'to have a property' if value != nil name = 'to have value "${to_string(value)}" in property' - self._run(name, e, |x, y| { + self._run(name, e, @(x, y) { var res = is_instance(x) and reflect.has_prop(x, y) if res and value != nil { return reflect.get_prop(x, y) == value @@ -210,66 +210,66 @@ class expect { } to_have_method(e) { - self._run('to have a method', e, |x, y| { + self._run('to have a method', e, @(x, y) { return is_instance(x) and reflect.has_method(x, y) }) return self } to_have_decorator(e) { - self._run('to have a decorator', e, |x, y| { + self._run('to have a decorator', e, @(x, y) { return is_instance(x) and reflect.has_decorator(x, y) }) return self } to_be_boolean() { - self._run('to be a boolean', nil, |x, y| { return is_bool(x) }) + self._run('to be a boolean', nil, @(x, y) { return is_bool(x) }) return self } to_be_number() { - self._run('to be a number', nil, |x, y| { return is_number(x) }) + self._run('to be a number', nil, @(x, y) { return is_number(x) }) return self } to_be_string() { - self._run('to be a string', nil, |x, y| { return is_string(x) }) + self._run('to be a string', nil, @(x, y) { return is_string(x) }) return self } to_be_list() { - self._run('to be a list', nil, |x, y| { return is_list(x) }) + self._run('to be a list', nil, @(x, y) { return is_list(x) }) return self } to_be_dict() { - self._run('to be a dict', nil, |x, y| { return is_dict(x) }) + self._run('to be a dict', nil, @(x, y) { return is_dict(x) }) return self } to_be_function() { - self._run('to be a function', nil, |x, y| { return is_function(x) }) + self._run('to be a function', nil, @(x, y) { return is_function(x) }) return self } to_be_class() { - self._run('to be a class', nil, |x, y| { return is_class(x) }) + self._run('to be a class', nil, @(x, y) { return is_class(x) }) return self } to_be_iterable() { - self._run('to be an iterable', nil, |x, y| { return is_iterable(x) }) + self._run('to be an iterable', nil, @(x, y) { return is_iterable(x) }) return self } to_be_file() { - self._run('to be a file', nil, |x, y| { return is_file(x) }) + self._run('to be a file', nil, @(x, y) { return is_file(x) }) return self } to_be_bytes() { - self._run('to be bytes', nil, |x, y| { return is_bytes(x) }) + self._run('to be bytes', nil, @(x, y) { return is_bytes(x) }) return self } } @@ -381,8 +381,8 @@ def show_tests_results() { total_time += e.time - var fails = iters.filter(e.it, |x| { - return iters.filter(x.expects, |y|{ return !y.status }).length() > 0 + var fails = iters.filter(e.it, @(x) { + return iters.filter(x.expects, @(y) { return !y.status }).length() > 0 }).length() > 0 if fails failed_suites++ @@ -396,7 +396,7 @@ def show_tests_results() { echo ' ${e.name}' iter var i = 0; i < e.it.length(); i++ { var _e = e.it[i] - var it_fails = iters.filter(_e.expects, |x|{ return !x.status }).length() > 0 + var it_fails = iters.filter(_e.expects, @(x) { return !x.status }).length() > 0 if it_fails failed_tests++ echo ' ' + _print('${_e.name} (${_time(_e.time)})', !it_fails) diff --git a/apps/qi/index.b b/apps/nyssa/.blade/libs/qi/index.b similarity index 100% rename from apps/qi/index.b rename to apps/nyssa/.blade/libs/qi/index.b diff --git a/apps/qi/nyssa.json b/apps/nyssa/.blade/libs/qi/nyssa.json similarity index 100% rename from apps/qi/nyssa.json rename to apps/nyssa/.blade/libs/qi/nyssa.json diff --git a/apps/qi/tests/.gitignore b/apps/nyssa/.blade/libs/qi/tests/.gitignore similarity index 100% rename from apps/qi/tests/.gitignore rename to apps/nyssa/.blade/libs/qi/tests/.gitignore diff --git a/apps/nyssa/.blade/libs/qi/tests/global.b b/apps/nyssa/.blade/libs/qi/tests/global.b new file mode 100644 index 00000000..8f68c5d9 --- /dev/null +++ b/apps/nyssa/.blade/libs/qi/tests/global.b @@ -0,0 +1,42 @@ +var myBeverage = { + delicious: true, + sour: false, +} + +describe('my beverage', @() { + it('should be delicious', @() { + expect(myBeverage.delicious).to_be_truthy() + }); + + it('should be sour', @() { + expect(myBeverage.sour).to_be_falsy() + }) +}) + +class CustomError < Exception {} + +var binay_string_to_number = @( bin_string ) { + if !bin_string.match('/^[01]+$/') { + die CustomError('Not a binary number.') + } + + return to_number('0b' + bin_string) +} + +describe('binay string to number', @() { + describe('given an invalid binary string', @() { + it('throws CustomError when composed of non-numbers', @() { + expect(@() { binay_string_to_number('abc') }).to_throw(CustomError) + }) + + it('throws CustomError when having extra whitespace', @() { + expect(@() { binay_string_to_number(' 100') }).to_throw(CustomError) + }) + }) + + describe('given a valid binary string', @() { + it('returns the correct number', @() { + expect(binay_string_to_number('100')).to_be(4) + }) + }) +}) diff --git a/apps/nyssa/.blade/libs/qi/tests/sample.spec.b b/apps/nyssa/.blade/libs/qi/tests/sample.spec.b new file mode 100644 index 00000000..48fd4749 --- /dev/null +++ b/apps/nyssa/.blade/libs/qi/tests/sample.spec.b @@ -0,0 +1,12 @@ +describe('Some testing', @() { + it('should match exactly!', @() { + expect(3).to_be(3) + expect(5).to_be(5) + expect(5).not().to_be(56) + + class X {} + + var b = [] + expect(@() { return b[5] }).not().to_throw(X) + }) +}) \ No newline at end of file diff --git a/apps/nyssa/.blade/libs/qi/tests/sample2.spec.b b/apps/nyssa/.blade/libs/qi/tests/sample2.spec.b new file mode 100644 index 00000000..19191ce2 --- /dev/null +++ b/apps/nyssa/.blade/libs/qi/tests/sample2.spec.b @@ -0,0 +1,11 @@ +describe('Some testing', @() { + it('should match exactly!', @() { + expect(3).to_be(3) + expect(5).to_be(5) + + class X {} + + var b = [] + expect(@() { return b[5] }).to_throw() + }) +}) \ No newline at end of file diff --git a/apps/nyssa/.blade/libs/qi/tests/setup.spec.b b/apps/nyssa/.blade/libs/qi/tests/setup.spec.b new file mode 100644 index 00000000..3b056e95 --- /dev/null +++ b/apps/nyssa/.blade/libs/qi/tests/setup.spec.b @@ -0,0 +1,10 @@ +import ..setup + +describe('Importing', @() { + it('should have a valid command', @() { + expect(setup.cmd).to_be_string() + }) + it('should have a correct file path', @() { + expect(setup.path).to_contain('qi') + }) +}) diff --git a/apps/qi/tests/test.b b/apps/nyssa/.blade/libs/qi/tests/test.b similarity index 82% rename from apps/qi/tests/test.b rename to apps/nyssa/.blade/libs/qi/tests/test.b index 673e6dee..8fc2e96b 100644 --- a/apps/qi/tests/test.b +++ b/apps/nyssa/.blade/libs/qi/tests/test.b @@ -3,8 +3,8 @@ var can = { ounces: 12, } -describe('the can', || { - it('has 12 ounces', || { +describe('the can', @() { + it('has 12 ounces', @() { expect(can.ounces).to_be(12) class A { @@ -17,7 +17,7 @@ describe('the can', || { expect(10.5).to_be_number().to_be_less_than(20) }) - it('has a sophisticated name', || { + it('has a sophisticated name', @() { expect(can.name).to_be('pamplemousse') expect([1, 2, 3]).to_have_length(3) @@ -25,7 +25,7 @@ describe('the can', || { expect('').not().to_have_length(5) }) - it('should pass this tests', || { + it('should pass this tests', @() { class A { testing() {} @testing() {} @@ -46,8 +46,8 @@ describe('the can', || { }) def do_something(id) { - if id == 1 return || { do_another_thing() } - else return || { do_something_else() } + if id == 1 return @() { do_another_thing() } + else return @() { do_something_else() } } class Set { @@ -55,8 +55,8 @@ class Set { @itern() {} } -describe('grapefruits', || { - it('should be a grape', || { +describe('grapefruits', @() { + it('should be a grape', @() { expect('grapefruits').to_match('grape') expect(do_something(1)).to_be_function() @@ -64,12 +64,12 @@ describe('grapefruits', || { expect(Exception).to_be_class() }) - it('should be valid type', || { + it('should be valid type', @() { expect({age: 10}).to_be_dict() expect(bytes(0)).to_be_bytes() }) - it('should be enumerable', || { + it('should be enumerable', @() { expect([]).to_be_iterable() expect({}).to_be_iterable() expect(Set()).to_be_iterable() @@ -85,8 +85,8 @@ def drink_flavor(flavor) { # Do some other stuff } -describe('testing to throw', || { - it('throws on octopus', || { +describe('testing to throw', @() { + it('throws on octopus', @() { def drink_octopus() { drink_flavor('octopus') } diff --git a/apps/nyssa/app/commands/test.b b/apps/nyssa/app/commands/test.b index f8bd6eed..935a24e1 100644 --- a/apps/nyssa/app/commands/test.b +++ b/apps/nyssa/app/commands/test.b @@ -22,7 +22,7 @@ def run_test_files(files) { def run(value, options, success, error) { if os.dir_exists(setup.TEST_DIR) { - var files = iters.filter(os.read_dir(setup.TEST_DIR), | x | { + var files = iters.filter(os.read_dir(setup.TEST_DIR), @( x ) { return x != '.' and x != '..' and x.ends_with('.b') }) if files { diff --git a/apps/nyssa/app/server/router.b b/apps/nyssa/app/server/router.b index b694cc5d..ed6ee27c 100644 --- a/apps/nyssa/app/server/router.b +++ b/apps/nyssa/app/server/router.b @@ -63,7 +63,7 @@ def _setup_session(req, res) { } # bind '.clear_session()' to response - res.clear_session = || { + res.clear_session = @() { res.session = {} # just for simplicity diff --git a/apps/nyssa/app/server/template_ext.b b/apps/nyssa/app/server/template_ext.b index a48107fa..ef2952a6 100644 --- a/apps/nyssa/app/server/template_ext.b +++ b/apps/nyssa/app/server/template_ext.b @@ -1,9 +1,12 @@ import markdown +import highlight { highlight } import .util + + var md = markdown({ - # linkify: true, html: true, + highlight, }) def template_ext() { diff --git a/apps/nyssa/app/server/views.b b/apps/nyssa/app/server/views.b index d8b158bb..ff6893d8 100644 --- a/apps/nyssa/app/server/views.b +++ b/apps/nyssa/app/server/views.b @@ -7,15 +7,15 @@ import .util import ..setup var doc_files = [ - 'installing-nyssa.md', + 'getting-started.md', 'creating-projects.md', - 'publishing-packages.md', - 'managing-dependencies.md', 'package-layout.md', + 'managing-dependencies.md', 'testing.md', 'test-globals.md', 'test-assertions.md', 'install-and-uninstall-actions.md', + 'publishing-packages.md', 'getting-project-info.md', 'hosting-a-private-repository.md', 'publishers-account.md', @@ -314,7 +314,7 @@ def logout(req, res) { def doc(req, res) { var uri = req.path.replace('~^/docs/?~', '').trim('/') - if uri == '' uri = 'installing-nyssa' + if uri == '' uri = 'getting-started' var doc_file if !doc_files.contains('${uri}.md') or diff --git a/apps/nyssa/docs/commands.md b/apps/nyssa/docs/commands.md index e396d47f..56a1e2de 100644 --- a/apps/nyssa/docs/commands.md +++ b/apps/nyssa/docs/commands.md @@ -3,32 +3,33 @@ Usage: `nyssa` [ [-h] | [-v] ] [COMMAND] ### OPTIONS -- `-h`, `--help` Show this help message and exit -- `-v`, `--version` Show Nyssa version +- `-h`, `--help` Show this help message and exit +- `-v`, `--version` Show Nyssa version ### COMMANDS -- **account** <_choice_> Manages a Nyssa publisher account - - `create` creates a new publisher account - - `login` login to a publisher account - - `logout` log out of a publisher account - - *`-r`*, *`--repo`* <_value_> the repo where the account is located +- **account** <_choice_> Manages a Nyssa publisher account + - `create` creates a new publisher account + - `login` login to a publisher account + - `logout` log out of a publisher account + - *`-r`*, *`--repo`* <_value_> the repo where the account is located - **clean** Clear Nyssa storage - - *`-c`*, *`--cache`* clean packages cache - - *`-l`*, *`--logs`* clean logs - - *`-a`*, *`--all`* clean everything + - *`-c`*, *`--cache`* clean packages cache + - *`-l`*, *`--logs`* clean logs + - *`-a`*, *`--all`* clean everything - **info** Shows current project information - **init** Creates a new package in current directory - - *`-n`*, *`--name`* <_value_> the name of the package + - *`-n`*, *`--name`* <_value_> the name of the package - **install** <_value_> Installs a Blade package - - *`-g`*, *`--global`* installs the package globally - - *`-c`*, *`--use-cache`* enables the cache - - *`-r`*, *`--repo`* <*value*> the repository to install from + - *`-g`*, *`--global`* installs the package globally + - *`-c`*, *`--use-cache`* enables the cache + - *`-r`*, *`--repo`* <*value*> the repository to install from - **publish** Publishes a repository - - *`-r`*, *`--repo`* <*value*> repository url + - *`-r`*, *`--repo`* <*value*> repository url - **restore** Restores all project dependencies - - *`-x`*, *`--no-cache`* disables the cache + - *`-x`*, *`--no-cache`* disables the cache - **serve** Starts a local Nyssa repository server - - *`-p`*, *`--port`* <*value*> port of the server (default: 3000) - - *`-n`*, *`--host`* <*value*> the host ip (default: 127.0.0.1) -- **uninstall** <*value*> Uninstalls a Blade package - - *`-g`*, *`--global`* package is a global package \ No newline at end of file + - *`-p`*, *`--port`* <*value*> port of the server (default: 3000) + - *`-n`*, *`--host`* <*value*> the host ip (default: 127.0.0.1) +- **test** Run the tests +- **uninstall** <*value*> Uninstalls a Blade package + - *`-g`*, *`--global`* package is a global package \ No newline at end of file diff --git a/apps/nyssa/docs/installing-nyssa.md b/apps/nyssa/docs/getting-started.md similarity index 88% rename from apps/nyssa/docs/installing-nyssa.md rename to apps/nyssa/docs/getting-started.md index 9244f1ed..18fce308 100644 --- a/apps/nyssa/docs/installing-nyssa.md +++ b/apps/nyssa/docs/getting-started.md @@ -34,11 +34,11 @@ OPTIONS: COMMANDS: account Manages a Nyssa publisher account - create creates a new publisher account - login login to a publisher account - logout log out of a publisher account + create Creates a new publisher account + login Login to a publisher account + logout Log out of a publisher account -r, --repo the repo where the account is located - clean Clear Nyssa storage + clean Clear Nyssa storage and cache -c, --cache clean packages cache -l, --logs clean logs -a, --all clean everything @@ -49,13 +49,14 @@ COMMANDS: -g, --global installs the package globally -c, --use-cache enables the cache -r, --repo the repository to install from - publish Publishes a repository + publish Publishes a Blade package to a repository -r, --repo repository url restore Restores all project dependencies -x, --no-cache disables the cache serve Starts a local Nyssa repository server -p, --port port of the server (default: 3000) -n, --host the host ip (default: 127.0.0.1) + test Run the tests uninstall Uninstalls a Blade package -g, --global package is a global package ``` diff --git a/apps/nyssa/docs/install-and-uninstall-actions.md b/apps/nyssa/docs/install-and-uninstall-actions.md index 8e9dd898..3dc85ea2 100644 --- a/apps/nyssa/docs/install-and-uninstall-actions.md +++ b/apps/nyssa/docs/install-and-uninstall-actions.md @@ -26,7 +26,7 @@ The `pre_uninstall` configuration is much like the `post_install` configuration, The `cli` installation hook allows package authors to specify a script that serves as the CLI entry point to the application. When the _CLI_ script is specified, a CLI entry will be created at `.blade` for local installations or at the root of Blade for global installations. This files will be automatically removed during uninstallation. -For example, the testing framework `qi` specifies a CLI entry point. For this reason, when you install `qi` locally, you can run the Qi command by simply running the command `.blade/qi` (or `.blade\qi` for Windows) to run your tests. This is made possible because during installation, Nyssa will automatically create the corresponding command-line entry file for you. +For example, the testing framework `qi` specifies a CLI entry point. For this reason, when you install `qi` locally, you can run the Qi command by simply running the command `nyssa test` (or `.blade\qi` for Windows) to run your tests. This is made possible because during installation, Nyssa will automatically create the corresponding command-line entry file for you. For applications installated globally, the application will become available on the user terminal **via the name** of the application provided that Blade has been added to path during installation. diff --git a/apps/nyssa/docs/test-assertions.md b/apps/nyssa/docs/test-assertions.md index 4161fa34..bee33f26 100644 --- a/apps/nyssa/docs/test-assertions.md +++ b/apps/nyssa/docs/test-assertions.md @@ -2,50 +2,14 @@ When writing tests you often need to check that a value meets certain criterias. The `expect()` function gives you access to an array of assertions that lets you test against different conditions. This page describes assertions, how to use them and all possible assertions in Qi. -## Reference - -- [Test Assertions](#test-assertions) - - [Reference](#reference) - - [expect()](#expect) - - [Modifiers](#modifiers) - - [not()](#not) - - [Matchers](#matchers) - - [to\_be(value)](#to_bevalue) - - [to\_be\_nil()](#to_be_nil) - - [to\_be\_defined()](#to_be_defined) - - [to\_be\_truthy()](#to_be_truthy) - - [to\_be\_falsy()](#to_be_falsy) - - [to\_be\_greater\_than(number)](#to_be_greater_thannumber) - - [to\_be\_greater\_than\_or\_equal(number)](#to_be_greater_than_or_equalnumber) - - [to\_be\_less\_than(number)](#to_be_less_thannumber) - - [to\_be\_less\_than\_or\_equal(number)](#to_be_less_than_or_equalnumber) - - [to\_match(value)](#to_matchvalue) - - [to\_contain(item)](#to_containitem) - - [to\_throw(error)](#to_throwerror) - - [to\_have\_length(number)](#to_have_lengthnumber) - - [to\_be\_instance\_of(class)](#to_be_instance_ofclass) - - [to\_be\_function(value)](#to_be_functionvalue) - - [to\_have\_property(name, value?)](#to_have_propertyname-value) - - [to\_have\_method(name)](#to_have_methodname) - - [to\_have\_decorator(name)](#to_have_decoratorname) - - [to\_be\_boolean()](#to_be_boolean) - - [to\_be\_number()](#to_be_number) - - [to\_be\_string()](#to_be_string) - - [to\_be\_list()](#to_be_list) - - [to\_be\_dict()](#to_be_dict) - - [to\_be\_class()](#to_be_class) - - [to\_be\_iterable()](#to_be_iterable) - - [to\_be\_file()](#to_be_file) - - [to\_be\_bytes()](#to_be_bytes) - ## expect() The `expect` function is used every time you want to test a value. You will rarely call `expect` by itself. Instead, you will use `expect` along with a "matcher" function to assert something about a value. Let's say you have a method `name_of_app()` which is supposed to return the string `'qi'`. Here's how you would test that: -```py -describe('Name of app test', || { - it('should be qi', || { +```blade +describe('Name of app test', @() { + it('should be qi', @() { expect(name_of_app()).to_be('qi') }) }) @@ -61,9 +25,9 @@ The argument to `expect` should be the value that your code produces, and any ar If you know how to test something, `.not()` lets you test its opposite. For example, this code tests that the name of the application is `not` `'qi'`. -```py -describe('Name of app test', || { - it('should be qi', || { +```blade +describe('Name of app test', @() { + it('should be qi', @() { expect(name_of_app()).not().to_be('qi') }) }) @@ -75,7 +39,7 @@ describe('Name of app test', || { > > Matchers can be nested. For example, > -> ```py +> ```blade > expect(10.5).to_be_number().to_be_less_than(20) > ``` @@ -84,18 +48,18 @@ describe('Name of app test', || { Use `.to_be` to compare primitive values or to check referential identity of object instances. For example, this code will validate some properties of the `can` object: -```py +```blade var can = { name: 'pamplemousse', ounces: 12, } -describe('the can', || { - it('has 12 ounces', || { +describe('the can', @() { + it('has 12 ounces', @() { expect(can.ounces).to_be(12) }) - it('has a sophisticated name', || { + it('has a sophisticated name', @() { expect(can.name).to_be('pamplemousse') }) }) @@ -105,12 +69,12 @@ describe('the can', || { `.to_be_nil()` is the same as `.to_be(nil)` but the error messages are a bit nicer. So use `.to_be_nil()` when you want to check that something is nil. -```py +```blade def bloop() { return nil } -it('should return nil', || { +it('should return nil', @() { expect(bloop()).to_be_nil() }) ``` @@ -119,7 +83,7 @@ it('should return nil', || { Use `.to_be_defined` to check that a variable is not `nil`. For example, if you want to check that a function `fetch_new_flavor_idea()` returns something, you can write: -```py +```blade expect(fetch_new_flavor_idea()).to_be_defined() ``` @@ -129,17 +93,17 @@ You could also write `expect(fetch_new_flavor_idea()).not().to_be_nil()` as they Use `.to_be_truthy` when you don't care what a value is and you want to ensure a value is true in a boolean context. For example, let's say you have some application code that looks like: -```py +```blade drink_some_lacroix() -if (thirsty()) { +if thirsty() { drink_more_lacroix() } ``` You may not care what `get_errors` returns, specifically - it might return `true`, `[1]`, or anything that's true in Blade, and your code would still work. So if you want to test you are thirsty before drinking some La Croix, you could write: -```py -it('should be thirsty before drinking La Croix', || { +```blade +it('should be thirsty before drinking La Croix', @() { drink_some_lacroix() expect(thirsty()).to_be_truthy() }) @@ -149,17 +113,17 @@ it('should be thirsty before drinking La Croix', || { Use `.to_be_falsy` when you don't care what a value is and you want to ensure a value is false in a boolean context. For example, let's say you have some application code that looks like: -```py +```blade drink_some_lacroix() -if (!get_errors()) { +if !get_errors() { drink_more_lacroix() } ``` You may not care what `get_errors` returns, specifically - it might return `false`, `nil`, or `-1`, and your code would still work. So if you want to test there are no errors after drinking some La Croix, you could write: -```py -it('does not lead to errors when drinking La Croix', || { +```blade +it('does not lead to errors when drinking La Croix', @() { drink_some_lacroix() expect(get_errors()).to_be_falsy() }) @@ -169,8 +133,8 @@ it('does not lead to errors when drinking La Croix', || { Use `.to_be_greater_than` to compare `received > expected` for number or `received.length() > expected` for string. For example, test that `ounces_per_can()` returns a value of more than 10 ounces: -```py -it('is more than 10 ounces per can', || { +```blade +it('is more than 10 ounces per can', @() { expect(ounces_per_can()).to_be_greater_than(10) }) ``` @@ -179,8 +143,8 @@ it('is more than 10 ounces per can', || { Use `.to_be_greater_than_or_equal` to compare `received >= expected` for number or `received.length() >= expected` for string. For example, test that `ounces_per_can()` returns a value of more than or equal to 10 ounces: -```py -it('is more than or equal to 10 ounces per can', || { +```blade +it('is more than or equal to 10 ounces per can', @() { expect(ounces_per_can()).to_be_greater_than_or_equal(10) }) ``` @@ -189,8 +153,8 @@ it('is more than or equal to 10 ounces per can', || { Use `.to_be_less_than` to compare `received < expected` for number or `received.length() < expected` for string. For example, test that `ounces_per_can()` returns a value of less than 10 ounces: -```py -it('is less than 10 ounces per can', || { +```blade +it('is less than 10 ounces per can', @() { expect(ounces_per_can()).to_be_less_than(10) }) ``` @@ -199,8 +163,8 @@ it('is less than 10 ounces per can', || { Use `.to_be_less_than_or_equal` to compare `received <= expected` for number or `received.length() <= expected` for string. For example, test that `ounces_per_can()` returns a value of less than or equal to 10 ounces: -```py -it('is less than or equal to 10 ounces per can', || { +```blade +it('is less than or equal to 10 ounces per can', @() { expect(ounces_per_can()).to_be_less_than_or_equal(10) }) ``` @@ -211,9 +175,9 @@ Use `.to_match` to check that a string matches a regular expression. For example, you might not know what exactly `essay_on_the_best_flavor()` returns, but you know it's a really long string, and the substring grapefruit should be in there somewhere. You can test this with: -```py -describe('an essay on the best flavor', || { - it('mentions grapefruit', || { +```blade +describe('an essay on the best flavor', @() { + it('mentions grapefruit', @() { expect(essay_on_the_best_flavor()).to_match('/grapefruit/i') }) }) @@ -221,9 +185,9 @@ describe('an essay on the best flavor', || { This matcher also accepts a string, which it will try to match: -```py -describe('grapefruits', || { - it('should be a grape', || { +```blade +describe('grapefruits', @() { + it('should be a grape', @() { expect('grapefruits').to_match('grape') }) }) @@ -235,8 +199,8 @@ Use `.to_contain` when you want to check that an item is in an list or dictionar For example, if `get_all_flavors()` returns an list of flavors and you want to be sure that lime is in there, you can write: -```py -it('should contain lime', || { +```blade +it('should contain lime', @() { expect(get_all_flavors()).to_contain('lime') }) ``` @@ -245,9 +209,9 @@ it('should contain lime', || { Use `.to_throw` to test that a function throws when it is called. For example, if we want to test that `drink_flavor('octopus')` throws, because octopus flavor is too disgusting to drink, we could write: -```py -it('throws on octopus', || { - expect(|| { +```blade +it('throws on octopus', @() { + expect(@() { drink_flavor('octopus') }).to_throw() }) @@ -266,7 +230,7 @@ You can provide an optional argument to test that a specific error is thrown: For example, let's say `drink_flavor()` looks like this: -```py +```blade def drink_flavor(flavor) { if flavor == 'octopus' { die DisgustingFlavorError('yuck, octopus flavor') @@ -277,8 +241,8 @@ def drink_flavor(flavor) { We could test the error thrown in several ways: -```py -it('throws on octopus', || { +```blade +it('throws on octopus', @() { def drink_octopus() { drink_flavor('octopus') } @@ -300,7 +264,7 @@ it('throws on octopus', || { Use `.to_have_length` to check that an object has a .length property and it is set to a certain numeric value. For example: -```py +```blade expect([1, 2, 3]).to_have_length(3) expect('abc').to_have_length(3) expect('').not().to_have_length(5) @@ -310,7 +274,7 @@ expect('').not().to_have_length(5) Use `.to_be_instance_of(class)` to check that an object is an instance of a class. This matcher uses `instance_of` underneath. -```py +```blade class A {} expect(A()).to_be_instance_of(A) @@ -321,16 +285,16 @@ expect(A()).to_be_instance_of(Exception) # fails Use `.to_be_function` when you want to check if a value is a function or a closure. For example, if `do_something()` is a function looking like this: -```py +```blade def do_something(id) { - if id == 1 return || { do_another_thing() } - else return || { do_something_else() } + if id == 1 return @() { do_another_thing() } + else return @() { do_something_else() } } ``` We can test that `do_something()` correctly returns a function. -```py +```blade expect(do_something(1)).to_be_function() ``` @@ -339,7 +303,7 @@ expect(do_something(1)).to_be_function() Use `.to_have_property` to check if an object has a given property. You can provide an optional value argument to compare the received property value against an expected value. -```py +```blade class A { var name = 'something' } @@ -354,7 +318,7 @@ expect(A()).to_have_property('name', 'something') Use the `.to_have_method` to check if an object is an instance of a class having a particular method. For example, let's say you have a class `A` and `B` defined as follows: -```py +```blade class A { testing() {} } @@ -366,7 +330,7 @@ class B { and you have a function `return_class()` that could return an instance of any of `A` or `B`, you can test the output of that method like, -```py +```blade expect(return_class()).to_have_method('testing') ``` @@ -374,7 +338,7 @@ expect(return_class()).to_have_method('testing') Use the `.to_have_decorator` to check if an object is an instance of a class having a particular decorator. For example, let's say you have a class `A` and `B` defined as follows: -```py +```blade class A { @testing() {} } @@ -386,7 +350,7 @@ class B { and you have a function `return_class()` that could return an instance of any of `A` or `B`, you can test the output of that method like, -```py +```blade expect(return_class()).to_have_decorator('testing') ``` @@ -394,8 +358,8 @@ expect(return_class()).to_have_decorator('testing') Use `.to_be_boolean` to check for `true` or `false` values. For example, test that `user_is_admin()` returns a value of `true` or `false`: -```py -it('should be true or false', || { +```blade +it('should be true or false', @() { expect(user_is_admin()).to_be_boolean() }) ``` @@ -404,8 +368,8 @@ it('should be true or false', || { Use `.to_be_number` to check that a value is a number without requiring any specific number. For example, test that `number_of_cans()` returns a valid number: -```py -it('should be a number', || { +```blade +it('should be a number', @() { expect(number_of_cans()).to_be_number() }) ``` @@ -414,8 +378,8 @@ it('should be a number', || { Use `.to_be_string` to check that a value is a string without requiring any specific content. For example, test that `name_of_king()` returns a valid string: -```py -it('should be a string', || { +```blade +it('should be a string', @() { expect(name_of_king()).to_be_string() }) ``` @@ -424,8 +388,8 @@ it('should be a string', || { Use `.to_be_list` to check that a value is a list without requiring any specific content. For example, test that `fruits()` returns a valid list: -```py -it('should be a string', || { +```blade +it('should be a string', @() { expect(fruits()).to_be_list() }) ``` @@ -434,8 +398,8 @@ it('should be a string', || { Use `.to_be_dict` to check that a value is a dictionary without requiring any specific content. For example, test that `{age: 10}` returns a valid dictionary: -```py -it('should be a dictionary', || { +```blade +it('should be a dictionary', @() { expect({age: 10}).to_be_dict() }) ``` @@ -444,8 +408,8 @@ it('should be a dictionary', || { Use `.to_be_class` to check that a value is a class and not an instance. For example, test that `Exception` is actually a class: -```py -it('should be a list', || { +```blade +it('should be a list', @() { expect(Exception).to_be_class() }) ``` @@ -454,7 +418,7 @@ it('should be a list', || { Use `.to_be_iterable` to check that a value is an iterable whether its of basic types (e.g. String, List etc.) or an iterable class. For example, suppose we have a class `Set` defined ass follows: -```py +```blade class Set { @iter() {} @itern() {} @@ -463,8 +427,8 @@ class Set { The following test will show that it's as much an iterable as a list or dictionary can be. -```py -it('should be enumerable', || { +```blade +it('should be enumerable', @() { expect([]).to_be_iterable() expect({}).to_be_iterable() expect(Set()).to_be_iterable() @@ -475,7 +439,7 @@ it('should be enumerable', || { Use `.to_be_file` to check that a value is a file object. For example, you can test that an handle `fh` returned by the function `get_config()` is actually a file like this: -```py +```blade var fh = get_config() expect(fh).to_be_file() @@ -485,7 +449,7 @@ expect(fh).to_be_file() Use `.to_be_bytes` to check that a value is an array of bytes. For example, -```py +```blade expect(bytes(0)).to_be_bytes() ``` diff --git a/apps/nyssa/docs/test-globals.md b/apps/nyssa/docs/test-globals.md index e025fd38..e1ddffd1 100644 --- a/apps/nyssa/docs/test-globals.md +++ b/apps/nyssa/docs/test-globals.md @@ -2,37 +2,24 @@ Qi exposes a set of global functions for use in your test files by putting each of these methods and objects into the global environment. You don't have to require or import anything to use them from your test files. -## Reference - -- [Test Globals](#test-globals) - - [Reference](#reference) - - [Methods](#methods) - - [describe(name, fn)](#describename-fn) - - [it(name, fn)](#itname-fn) - - [Hooks](#hooks) - - [before\_all(fn)](#before_allfn) - - [after\_all(fn)](#after_allfn) - - [before\_each(fn)](#before_eachfn) - - [after\_each(fn)](#after_eachfn) - ## Methods ### describe(name, fn) `describe(name, fn)` creates a block that groups together several related tests. It is the Test Suite. For example, if you have a `myBeverage` object that is supposed to be delicious but not sour, you could test it with: -```py +```blade var myBeverage = { delicious: true, sour: false, } -describe('my beverage', || { - it('should be delicious', || { +describe('my beverage', @() { + it('should be delicious', @() { expect(myBeverage.delicious).to_be_truthy() }); - it('should be sour', || { + it('should be sour', @() { expect(myBeverage.sour).to_be_falsy() }) }) @@ -40,8 +27,8 @@ describe('my beverage', || { You can also nest `describe` blocks if you have a hierarchy of tests: -```py -var binay_string_to_number = | bin_string | { +```blade +var binay_string_to_number = @( bin_string ) { if !bin_string.match('/^[01]+$/') { die CustomError('Not a binary number.') } @@ -49,19 +36,19 @@ var binay_string_to_number = | bin_string | { return to_number('0b' + bin_string) } -describe('binay string to number', || { - describe('given an invalid binary string', || { - it('throws CustomError when composed of non-numbers', || { - expect(|| { binay_string_to_number('abc') }).to_throw(CustomError) +describe('binay string to number', @() { + describe('given an invalid binary string', @() { + it('throws CustomError when composed of non-numbers', @() { + expect(@() { binay_string_to_number('abc') }).to_throw(CustomError) }) - it('throws CustomError when having extra whitespace', || { - expect(|| { binay_string_to_number(' 100') }).to_throw(CustomError) + it('throws CustomError when having extra whitespace', @() { + expect(@() { binay_string_to_number(' 100') }).to_throw(CustomError) }) }) - describe('given a valid binary string', || { - it('returns the correct number', || { + describe('given a valid binary string', @() { + it('returns the correct number', @() { expect(binay_string_to_number('100')).to_be(4) }) }) @@ -70,10 +57,10 @@ describe('binay string to number', || { ### it(name, fn) -The `it(name, fn)` function is the entry point for tests in a test suite. For example, let's say there's a function `inches_of_rain()` that should be zero. Your whole test could be: +The `it(name, fn)` function is the entry point for tests in a test suite. For example, let's say there's a function `inches_of_rain()` that should return zero. Your whole test could be: -```py -it('did not rain', || { +```blade +it('did not rain', @() { expect(inches_of_rain()).to_be(0) }) ``` @@ -95,21 +82,21 @@ Runs a function before each of the tests in the test suite run. This is often us For example: -```py +```blade var global_db = make_global_db() -before_all(|| { +before_all(@() { # Clears the database and adds some testing data. - return globalDatabase.clear(|| { + return globalDatabase.clear(@() { return globalDatabase.insert({testData: 'foo'}) }) }) # Since we only set up the database once in this example, it's important # that our tests don't modify it. -describe('Before all', || { - it('can find things', || { - return global_db.find('thing', {}, |results| { +describe('Before all', @() { + it('can find things', @() { + return global_db.find('thing', {}, @(results) { expect(results.length()).to_be_greater_than(0) }) }) @@ -124,26 +111,26 @@ Runs a function after all the tests in a test suite have completed. This is ofte For example: -```py +```blade var global_db = make_global_db() def clean_up_db(db) { db.clean_up() } -after_all(|| { +after_all(@() { clean_up_db(global_db) }); -describe('confirming after_all works', || { - it('can find things', || { - return global_db.find('thing', {}, |results| { +describe('confirming after_all works', @() { + it('can find things', @() { + return global_db.find('thing', {}, @(results) { expect(results.length()).to_be_greater_than(0) }) }) - it('can insert a thing', || { - return global_db.insert('thing', make_thing(), |response| { + it('can insert a thing', @() { + return global_db.insert('thing', make_thing(), @(response) { expect(response.success).to_be_truthy() }) }) @@ -160,24 +147,24 @@ Runs a function before each of the tests in the test suite runs. This is often u For example: -```py +```blade var global_db = make_global_db() -before_each(|| { +before_each(@() { # Clears the database and adds some testing data. global_db.clear() global_db.insert({testData: 'foo'}); }) -describe('confirming before_each works', || { - it('can find things', || { - return global_db.find('thing', {}, |results| { +describe('confirming before_each works', @() { + it('can find things', @() { + return global_db.find('thing', {}, @(results) { expect(results.length()).to_be_greater_than(0) }) }) - it('can insert a thing', || { - return global_db.insert('thing', make_thing(), |response| { + it('can insert a thing', @() { + return global_db.insert('thing', make_thing(), @(response) { expect(response.success).to_be_truthy() }) }) @@ -192,26 +179,26 @@ Runs a function after each one of the tests in this file completes. This is ofte For example: -```py +```blade var global_db = make_global_db() def clean_up_db(db) { db.clean_up() } -after_each(|| { +after_each(@() { clean_up_db(global_db) }) -describe('confirming after_each works', || { - it('can find things', || { - return global_db.find('thing', {}, |results| { +describe('confirming after_each works', @() { + it('can find things', @() { + return global_db.find('thing', {}, @(results) { expect(results.length()).to_be_greater_than(0) }) }) - it('can insert a thing', || { - return global_db.insert('thing', make_thing(), |response| { + it('can insert a thing', @() { + return global_db.insert('thing', make_thing(), @(response) { expect(response.success).to_be_truthy() }) }) diff --git a/apps/nyssa/docs/testing.md b/apps/nyssa/docs/testing.md index 07a7607d..424c54c5 100644 --- a/apps/nyssa/docs/testing.md +++ b/apps/nyssa/docs/testing.md @@ -8,7 +8,7 @@ Both Nyssa and `Qi` ship with Blade allowing you write comprehensive tests witho Let's write a test for a hypothetical function that returns the product of two numbers. First, we'll create a file `prod.b` that contains the following code: -```py +```blade def prod(x, y) { return x * y } @@ -16,11 +16,11 @@ def prod(x, y) { Now, let's create a test for it by creating a file `prod.test.b` in the `tests` directory and add the following code to it. -```py +```blade import ..prod -describe('Product test suite', || { - it('should return 6 for 2 and 3', || { +describe('Product test suite', @() { + it('should return 6 for 2 and 3', @() { expect(prod(2, 3)).to_be(6) }) }) @@ -31,7 +31,7 @@ describe('Product test suite', || { Now let's run the test. If you have installed Qi using `nyssa` (which is recommended), then you can run the following command at the root directory (the directory that contains the `tests` folder). ```sh -.blade/qi +nyssa test ``` You should get an output similar to this: diff --git a/apps/nyssa/public/css/style.css b/apps/nyssa/public/css/style.css index 0219f95b..496186c9 100644 --- a/apps/nyssa/public/css/style.css +++ b/apps/nyssa/public/css/style.css @@ -29,6 +29,9 @@ input:focus { pre { white-space: pre-wrap; border-radius: .25rem; + color: hsl(210, 20%, 30%); + background: hsl(210, 0%, 99%); + border: solid 1px hsl(200, 20%, 88%); } .columns { @@ -265,13 +268,18 @@ pre.has-icon .icon { line-height: 1.5; } +.content.doc { + font-size: .95em; + line-height: 1.65; +} + .content.doc h2 { border-bottom: 1px solid rgba(0, 0, 0, .1); padding-bottom: .5em; } .content.doc h1 { - border-bottom: 4px solid #0000001a; + border-bottom: 2px solid #0000001a; padding-bottom: .5rem; } @@ -287,6 +295,10 @@ pre.has-icon .icon { color: inherit; } +.content.doc blockquote pre { + margin-bottom: 1em; +} + .content.doc p { margin: 1em 0; } diff --git a/apps/nyssa/templates/doc.html b/apps/nyssa/templates/doc.html index 0aa0c2c7..5047f4ae 100644 --- a/apps/nyssa/templates/doc.html +++ b/apps/nyssa/templates/doc.html @@ -9,21 +9,25 @@

-
- -
-
-
{{ content|draw }}
+
+
+
+ +
+
+
{{ content|draw }}
+
+
diff --git a/apps/qi/tests/global.b b/apps/qi/tests/global.b deleted file mode 100644 index 49dcb3b7..00000000 --- a/apps/qi/tests/global.b +++ /dev/null @@ -1,42 +0,0 @@ -var myBeverage = { - delicious: true, - sour: false, -} - -describe('my beverage', || { - it('should be delicious', || { - expect(myBeverage.delicious).to_be_truthy() - }); - - it('should be sour', || { - expect(myBeverage.sour).to_be_falsy() - }) -}) - -class CustomError < Exception {} - -var binay_string_to_number = | bin_string | { - if !bin_string.match('/^[01]+$/') { - die CustomError('Not a binary number.') - } - - return to_number('0b' + bin_string) -} - -describe('binay string to number', || { - describe('given an invalid binary string', || { - it('throws CustomError when composed of non-numbers', || { - expect(|| { binay_string_to_number('abc') }).to_throw(CustomError) - }) - - it('throws CustomError when having extra whitespace', || { - expect(|| { binay_string_to_number(' 100') }).to_throw(CustomError) - }) - }) - - describe('given a valid binary string', || { - it('returns the correct number', || { - expect(binay_string_to_number('100')).to_be(4) - }) - }) -}) diff --git a/apps/qi/tests/sample.spec.b b/apps/qi/tests/sample.spec.b deleted file mode 100644 index 38241363..00000000 --- a/apps/qi/tests/sample.spec.b +++ /dev/null @@ -1,12 +0,0 @@ -describe('Some testing', || { - it('should match exactly!', || { - expect(3).to_be(3) - expect(5).to_be(5) - expect(5).not().to_be(56) - - class X {} - - var b = [] - expect(|| { return b[5] }).not().to_throw(X) - }) -}) \ No newline at end of file diff --git a/apps/qi/tests/sample2.spec.b b/apps/qi/tests/sample2.spec.b deleted file mode 100644 index cfadc926..00000000 --- a/apps/qi/tests/sample2.spec.b +++ /dev/null @@ -1,11 +0,0 @@ -describe('Some testing', || { - it('should match exactly!', || { - expect(3).to_be(3) - expect(5).to_be(5) - - class X {} - - var b = [] - expect(|| { return b[5] }).to_throw() - }) -}) \ No newline at end of file diff --git a/apps/qi/tests/setup.spec.b b/apps/qi/tests/setup.spec.b deleted file mode 100644 index 08ef5100..00000000 --- a/apps/qi/tests/setup.spec.b +++ /dev/null @@ -1,10 +0,0 @@ -import ..setup - -describe('Importing', || { - it('should have a valid command', || { - expect(setup.cmd).to_be_string() - }) - it('should have a correct file path', || { - expect(setup.path).to_contain('qi') - }) -}) diff --git a/libs/markdown/block/list.b b/libs/markdown/block/list.b index ffe45f2d..40cf359f 100644 --- a/libs/markdown/block/list.b +++ b/libs/markdown/block/list.b @@ -37,7 +37,7 @@ def _skip_ordered_list_marker(state, start_line) { ch = state.src[pos++ - 1] - if ord(ch) < ord('0') or ord(ch) > ord('9') return -1 + if ord(ch) < 0x30 /* 0 */ or ord(ch) > 0x39 /* 9 */ return -1 iter ;; { # EOL -> fail @@ -45,7 +45,7 @@ def _skip_ordered_list_marker(state, start_line) { ch = state.src[pos++ - 1] - if ord(ch) >= ord('0') and ord(ch) <= ord('9') { + if ord(ch) >= 0x30 and ord(ch) <= 0x39 { # List marker should have no more than 9 digits # (prevents integer overflow in browsers) diff --git a/libs/markdown/index.b b/libs/markdown/index.b index 871d26f7..9ad315c0 100644 --- a/libs/markdown/index.b +++ b/libs/markdown/index.b @@ -148,6 +148,10 @@ def encode_url(string, exclude, keep_escaped) { } def normalize_link(uri) { + if uri.starts_with('#') or uri.starts_with('?') { + return encode_url(uri) + } + var parsed = url.parse(uri) if parsed.host { @@ -407,8 +411,8 @@ class Markdown { * }) * ``` * - * @param {string|nil} preset_name: `commonmark`, `standard` or `zero` (default: `standard`) - * @param {dict|nil} options + * @param {string?} preset_name: `commonmark`, `standard` or `zero` (default: `standard`) + * @param {dict?} options */ Markdown(preset_name, options) { if !instance_of(self, Markdown) { @@ -620,7 +624,7 @@ class Markdown { * in [[Markdown.parse]]. * * @param {string} src: source string - * @param {object|nil} env: environment sandbox + * @param {object?} env: environment sandbox * @return string */ render(src, env) { @@ -635,7 +639,7 @@ class Markdown { * tokens in `children` property. Also updates `env` object. * * @param {string} src: source string - * @param {object|nil} env: environment sandbox + * @param {object?} env: environment sandbox * @return list * @internal **/ @@ -653,7 +657,7 @@ class Markdown { * will NOT be wrapped into `

` tags. * * @param {string} src: source string - * @param {object|nil} env: environment sandbox + * @param {object?} env: environment sandbox * @return string */ render_inline(src, env) { diff --git a/libs/markdown/renderer.b b/libs/markdown/renderer.b index 783ace8a..a278a2ee 100644 --- a/libs/markdown/renderer.b +++ b/libs/markdown/renderer.b @@ -32,7 +32,7 @@ default_rules.fence = @(tokens, idx, options, env, slf) { } if options.highlight { - highlighted = options.highlight(token.content, lang_name, lang_attrs) or escape_html(token.content) + highlighted = options.highlight(token.content, lang_name) or escape_html(token.content) } else { highlighted = escape_html(token.content) } diff --git a/libs/markdown/ruler.b b/libs/markdown/ruler.b index 4e21a80d..d039a5aa 100644 --- a/libs/markdown/ruler.b +++ b/libs/markdown/ruler.b @@ -91,7 +91,7 @@ class Ruler { * * @param {string} name: rule name to replace. * @param {function} fn: new rule function. - * @param {dict|nil} options: new rule options (optional). + * @param {dict?} options: new rule options (optional). */ at(name, fn, options) { var index = self.__find__(name) @@ -125,7 +125,7 @@ class Ruler { * @param {string} before_name: new rule will be added before this one. * @param {string} rule_name: name of added rule. * @param {function} fn: rule function. - * @param {dict|nil} options: rule options (optional). + * @param {dict?} options: rule options (optional). */ before(before_name, rule_name, fn, options) { var index = self.__find__(before_name) @@ -165,7 +165,7 @@ class Ruler { * @param {string} after_name: new rule will be added after this one. * @param {string} rule_name: name of added rule. * @param {function} fn: rule function. - * @param {dict|nil} options: rule options (optional). + * @param {dict?} options: rule options (optional). */ after(after_name, rule_name, fn, options) { var index = self.__find__(after_name) @@ -204,7 +204,7 @@ class Ruler { * * @param {string} rule_name: name of added rule. * @param {function} fn: rule function. - * @param {dict|nil} options: rule options (optional). + * @param {dict?} options: rule options (optional). */ push(rule_name, fn, options) { var opt = options or {} From 27eb7a2f1bbae0e32d90f6d8be652f58535cac0d Mon Sep 17 00:00:00 2001 From: Richard Ore Date: Sun, 18 Jun 2023 21:36:07 +0100 Subject: [PATCH 5/5] updated testing documentation and add highlighter to nyssa. --- apps/nyssa/.blade/libs/highlight.b | 26 ++++++++++++++++++- apps/nyssa/.blade/libs/qi/README.md | 2 +- apps/nyssa/README.md | 10 +++---- .../docs/hosting-a-private-repository.md | 2 +- apps/nyssa/docs/package-layout.md | 3 ++- apps/nyssa/docs/testing.md | 10 +++---- 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/apps/nyssa/.blade/libs/highlight.b b/apps/nyssa/.blade/libs/highlight.b index 031055ff..b60c4ba4 100644 --- a/apps/nyssa/.blade/libs/highlight.b +++ b/apps/nyssa/.blade/libs/highlight.b @@ -9,13 +9,15 @@ var constant_keywords = '|'.join([ 'nil', 'parent', 'self', 'true', 'false', 'and', 'or' ]) +var _quote_re = '/((\'(?:[^\'\\\\]|\\.)*\')|("(?:[^"\\\\]|\\.)*"))/' + def highlight_blade(text) { text = text. # operators replace('/([+\-*=/%!<>@]|\.\.)/', '<_o>$1'). replace('/\\b(and|or)\\b/', '<_o>$1'). # quotes - replace('/((\'(?:[^\'\\\\]|\\.)*\')|("(?:[^"\\\\]|\\.)*"))/', '<_q>$1'). + replace(_quote_re, '<_q>$1'). # constant keywords replace('/\\b(${constant_keywords})\\b/', '<_c>$1'). @@ -67,9 +69,31 @@ def highlight_blade(text) { replace('/<_n>(.*?)<\/_n>/', '$1') } +def highlight_html5(text, lang) { + var tags = text.matches('/<([^>]+)>/') + if tags { + iter var i = 0; i < tags[0].length(); i++ { + var content = tags[1][i].replace('/([a-zA-Z_\-0-9]+)(?=[=])/', '<^a>$1'). + replace(_quote_re, '<^v>$1') + text = text.replace(tags[0][i], '<${content}>', false) + } + } + + var result = text.replace('/<\^a>(.*?)<\/\^a>/', '$1'). + replace('/<\^v>(.*?)<\/\^v>/', '$1') + + if lang == 'wire' { + result = result.replace('/(\{\{.+?\}\})/', '$1') + } + + return result +} + def highlight(text, lang) { if lang == 'blade' { return highlight_blade(text) + } else if lang == 'html' or lang == 'html5' or lang == 'wire' { + return highlight_html5(text, lang) } return text } diff --git a/apps/nyssa/.blade/libs/qi/README.md b/apps/nyssa/.blade/libs/qi/README.md index ebb60b25..ed68db34 100644 --- a/apps/nyssa/.blade/libs/qi/README.md +++ b/apps/nyssa/.blade/libs/qi/README.md @@ -10,7 +10,7 @@ A testing framework for Blade programming language. - **Tags:** test, blade, testing, tdd, test-driven - **Author:** Richard Ore - **License:** MIT -- **Requires:** Nyssa v0.1.6+ +- **Requires:** Blade v0.0.86+ ### Getting started diff --git a/apps/nyssa/README.md b/apps/nyssa/README.md index 6e674ec2..8cbd70d4 100644 --- a/apps/nyssa/README.md +++ b/apps/nyssa/README.md @@ -1,5 +1,5 @@ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/blade-lang/blade/blob/master/LICENSE) -[![Coverage Status](https://coveralls.io/repos/github/blade-lang/nyssa/badge.svg?branch=main)](https://coveralls.io/github/blade-lang/nyssa?branch=main) +[![Coverage Status](https://coveralls.io/repos/github/blade-lang/nyssa/badge.svg?branch=main)](https://coveralls.io/github/blade-lang/blade?branch=main) [![Version](https://img.shields.io/badge/version-0.1.4-green)](https://github.com/blade-lang/blade) # Nyssa @@ -8,11 +8,11 @@ Nyssa is the official package manager for the Blade programming language. It is #### The CLI -![Nyssa CLI](https://raw.githubusercontent.com/blade-lang/nyssa/main/nyssa-cli.png) +![Nyssa CLI](https://raw.githubusercontent.com/blade-lang/blade/main/apps/nyssa/nyssa-cli.png) #### The browsable repository website. -![Nyssa Repository](https://raw.githubusercontent.com/blade-lang/nyssa/main/nyssa.png) +![Nyssa Repository](https://raw.githubusercontent.com/blade-lang/blade/main/apps/nyssa/nyssa.png) ## Features @@ -32,11 +32,11 @@ Nyssa is the official package manager for the Blade programming language. It is - [x] Create publisher account. - [x] Login to publisher account. - [x] Logout from publisher account. - - [ ] Account Recovery. + - [x] Account Recovery. - [x] Custom Post-Installation script support. - [x] Custom Pre-Uninstallation script support. - [ ] Generate application/library documentation. -- [ ] Test Runner. +- [x] Test Runner. - [ ] C Extension compiler. diff --git a/apps/nyssa/docs/hosting-a-private-repository.md b/apps/nyssa/docs/hosting-a-private-repository.md index 89c54a3f..fa6a4403 100644 --- a/apps/nyssa/docs/hosting-a-private-repository.md +++ b/apps/nyssa/docs/hosting-a-private-repository.md @@ -42,6 +42,6 @@ You fully customize your own repository setup by modifying the `apps/nyssa/app/s ### Production considerations -- While the built-in server can be used as is on production, it is not advisable to do so unless you are not exposing it on the internet (such as internal use on in an organization). Especially if you are trying to run on `https` as the server only supports plain `http`. +- While the built-in server can be used as is on production, it is not advisable to do so unless you are not exposing it on the internet (such as internal use on in an organization). Especially if you are trying to run on `https`. - Consider running behind a reverse-proxy such as `apache` or `lightspeed` or `nginx` or run it behind a `VPN`. diff --git a/apps/nyssa/docs/package-layout.md b/apps/nyssa/docs/package-layout.md index 8c5cc37c..f6ca43ab 100644 --- a/apps/nyssa/docs/package-layout.md +++ b/apps/nyssa/docs/package-layout.md @@ -58,7 +58,8 @@ package.json files. A typical sample of a complete `nyssa.json` file looks like "dependecny3": "1.0.0" }, "post_install": "post-install-script.b", - "pre_uninstall": "pre-uninstall-script.b" + "pre_uninstall": "pre-uninstall-script.b", + "cli": "cli-entry-script.b" } ``` diff --git a/apps/nyssa/docs/testing.md b/apps/nyssa/docs/testing.md index 424c54c5..691aacfc 100644 --- a/apps/nyssa/docs/testing.md +++ b/apps/nyssa/docs/testing.md @@ -1,8 +1,8 @@ # Testing -Blade comes shipped with a test runner called `qi` designed to run tests are out the directory `tests` and for this reason. Nyssa provides the default interface to the test runner via the `test` command allowing you to write and run tests for your Blade applications out of the box. +Blade comes shipped with a test runner called `qi` designed to run tests are out the `tests` directory. Nyssa provides the default interface to the test runner via the `test` command allowing you to write and run tests for your Blade applications out of the box. For this reason, Nyssa considers all files in the `test` directory as test files and will automatically create the directory for you when you create a new project. -Both Nyssa and `Qi` ship with Blade allowing you write comprehensive tests without any third-party package. +Both _Nyssa_ and `Qi` ship with Blade allowing you write comprehensive tests without any third-party package. ### Writing a simple test @@ -28,7 +28,7 @@ describe('Product test suite', @() { ### Running your tests -Now let's run the test. If you have installed Qi using `nyssa` (which is recommended), then you can run the following command at the root directory (the directory that contains the `tests` folder). +Run the following command at the root directory (the directory that contains the `tests` folder) to run all tests. ```sh nyssa test @@ -39,13 +39,13 @@ You should get an output similar to this: ```sh PASS tests/prod.test.b Product test suite - ✔ should return 6 for 2 and 3 (1.09ms) + ✔ should return 6 for 2 and 3 (1.09µs) ✔ expect "6" to be "6" Test suites: 1 passed, 0 failed, 1 total Tests: 1 passed, 0 failed, 1 total Assertions: 1 passed, 0 failed, 1 total -Time: 1.092ms +Time: 1.092µs Ran all test suites. ```