From fa9f77a7e33c8740cc4ceff9c7067d493f0c6cec Mon Sep 17 00:00:00 2001 From: Alexander Kuleshov Date: Sun, 23 Aug 2026 17:51:50 +0500 Subject: [PATCH] general: move code examples to a separate directory --- .github/workflows/examples.yaml | 45 +++++ .github/workflows/link-check.yaml | 2 +- .gitignore | 16 +- CONTRIBUTING.md | 20 ++ README.md | 4 +- content/asm_6.md | 7 +- content/asm_7.md | 15 +- examples/README.md | 20 ++ {casm => examples/casm}/README.md | 2 +- {casm => examples/casm}/casm1/Makefile | 0 {casm => examples/casm}/casm1/casm.asm | 0 {casm => examples/casm}/casm2/Makefile | 0 {casm => examples/casm}/casm2/casm.c | 5 +- {casm => examples/casm}/casm3/Makefile | 0 {casm => examples/casm}/casm3/casm.asm | 0 {casm => examples/casm}/casm3/casm.c | 0 {float => examples/float}/Makefile | 0 examples/float/README.md | 33 +++ {float => examples/float}/dot_product.asm | 10 +- {hello => examples/hello}/Makefile | 0 {hello => examples/hello}/README.md | 2 +- {hello => examples/hello}/hello.asm | 0 {stack => examples/stack}/Makefile | 0 {stack => examples/stack}/README.md | 2 +- {stack => examples/stack}/stack.asm | 0 {strings => examples/strings}/Makefile | 0 {strings => examples/strings}/README.md | 2 +- {strings => examples/strings}/reverse.asm | 0 {sum => examples/sum}/Makefile | 0 {sum => examples/sum}/README.md | 2 +- {sum => examples/sum}/sum.asm | 0 float/README.md | 11 - scripts/ci/pty-run.py | 105 ++++++++++ scripts/ci/test-examples.sh | 236 ++++++++++++++++++++++ 34 files changed, 504 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/examples.yaml create mode 100644 examples/README.md rename {casm => examples/casm}/README.md (84%) rename {casm => examples/casm}/casm1/Makefile (100%) rename {casm => examples/casm}/casm1/casm.asm (100%) rename {casm => examples/casm}/casm2/Makefile (100%) rename {casm => examples/casm}/casm2/casm.c (72%) rename {casm => examples/casm}/casm3/Makefile (100%) rename {casm => examples/casm}/casm3/casm.asm (100%) rename {casm => examples/casm}/casm3/casm.c (100%) rename {float => examples/float}/Makefile (100%) create mode 100644 examples/float/README.md rename {float => examples/float}/dot_product.asm (97%) rename {hello => examples/hello}/Makefile (100%) rename {hello => examples/hello}/README.md (58%) rename {hello => examples/hello}/hello.asm (100%) rename {stack => examples/stack}/Makefile (100%) rename {stack => examples/stack}/README.md (57%) rename {stack => examples/stack}/stack.asm (100%) rename {strings => examples/strings}/Makefile (100%) rename {strings => examples/strings}/README.md (57%) rename {strings => examples/strings}/reverse.asm (100%) rename {sum => examples/sum}/Makefile (100%) rename {sum => examples/sum}/README.md (65%) rename {sum => examples/sum}/sum.asm (100%) delete mode 100644 float/README.md create mode 100755 scripts/ci/pty-run.py create mode 100755 scripts/ci/test-examples.sh diff --git a/.github/workflows/examples.yaml b/.github/workflows/examples.yaml new file mode 100644 index 0000000..0958e14 --- /dev/null +++ b/.github/workflows/examples.yaml @@ -0,0 +1,45 @@ +--- +name: "Test examples" + +"on": + pull_request: + branches: + - "**" + push: + branches: + - "master" + +concurrency: + cancel-in-progress: true + group: "${{ github.workflow }}-${{ github.ref }}" + +permissions: + contents: read + +jobs: + build-and-run: + name: "${{ matrix.example }}" + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + # Build and run every example even if one of them is broken. + fail-fast: false + matrix: + example: + - hello + - sum + - stack + - strings + - float + - casm1 + - casm2 + - casm3 + steps: + - name: "Checkout repository" + uses: "actions/checkout@v7" + - name: "Install NASM" + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends --yes nasm + - name: "Build and run the ${{ matrix.example }} example" + run: ./scripts/ci/test-examples.sh "${{ matrix.example }}" diff --git a/.github/workflows/link-check.yaml b/.github/workflows/link-check.yaml index 62089b1..5b33b26 100644 --- a/.github/workflows/link-check.yaml +++ b/.github/workflows/link-check.yaml @@ -5,7 +5,7 @@ name: "Link Check" - "**" push: branches: - - "main" + - "master" concurrency: cancel-in-progress: true diff --git a/.gitignore b/.gitignore index 038b627..891f7e9 100644 --- a/.gitignore +++ b/.gitignore @@ -52,11 +52,11 @@ Mkfile.old dkms.conf # Our binaries -hello/hello -sum/sum -stack/stack -strings/reverse -float/dot_product -casm/casm1/casm -casm/casm2/casm -casm/casm3/casm +examples/hello/hello +examples/sum/sum +examples/stack/stack +examples/strings/reverse +examples/float/dot_product +examples/casm/casm1/casm +examples/casm/casm2/casm +examples/casm/casm3/casm diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3cbe387..4bd1d55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,26 @@ If you want to directly contribute to the project, create a pull reguest with th 4. Open a pull request in GitHub. Fill the pull request template with the reason and description for the provided changes. Link your pull request with the existing issue, if applicable. After submitting your PR, wait for the review from the project maintainers. +## Code examples + +The programs that accompany the chapters live in the [examples](./examples/) directory. Each of them is a standalone program with its own `Makefile` and `README.md`. + +If you change an example, or add a new one, check that it still builds and prints what the related chapter says it should print: + +```bash +./scripts/ci/test-examples.sh +``` + +To check only some of the examples, pass their names: + +```bash +./scripts/ci/test-examples.sh hello casm3 +``` + +Besides the tools from the [Requirements](./README.md#requirements) section, the script needs [GCC](https://gcc.gnu.org/) to build the examples that interact with C, and [Python](https://www.python.org/) to run the examples that expect a terminal. + +When adding a new example, you also need to add a `test_` function in the script and an entry in the `example` list of the [Examples](./.github/workflows/examples.yaml) workflow. The same script runs in continuous integration for every pull request, so a broken example fails the build. + ## Review and approval process After you submit your PR, wait for the review. The project maintainers will evaluate your changes and provide feedback either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. Address the review suggestions and comments as soon as you can. If your PR looks good, the maintainers approve and merge it. diff --git a/README.md b/README.md index 66f638a..a198047 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Assembly programming -[![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] [![Check Links](https://github.com/0xAX/asm/actions/workflows/link-check.yaml/badge.svg)](https://github.com/0xAX/asm/actions/workflows/link-check.yaml) [![star this repo](https://badgen.net/github/stars/0xAX/asm)](https://github.com/0xAX/asm) [![fork this repo](https://badgen.net/github/forks/0xAX/asm)](https://github.com/0xAX/asm/fork) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/0xAX/asm/issues) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) +[![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] [![Check Links](https://github.com/0xAX/asm/actions/workflows/link-check.yaml/badge.svg)](https://github.com/0xAX/asm/actions/workflows/link-check.yaml) [![Examples](https://github.com/0xAX/asm/actions/workflows/examples.yaml/badge.svg)](https://github.com/0xAX/asm/actions/workflows/examples.yaml) [![star this repo](https://badgen.net/github/stars/0xAX/asm)](https://github.com/0xAX/asm) [![fork this repo](https://badgen.net/github/forks/0xAX/asm)](https://github.com/0xAX/asm/fork) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/0xAX/asm/issues) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) This repository contains blog posts that introduce the [assembly](https://en.wikipedia.org/wiki/Assembly_language) programming language. For this moment, all the content and examples cover only the [x86_64](https://en.wikipedia.org/wiki/X86-64) processors and the GNU Linux operating system. In the future, I plan to post learning materials for the [ARM64](https://en.wikipedia.org/wiki/AArch64) architecture. @@ -29,6 +29,8 @@ Here are links to each post: - [Part 6. Floating-point arithmetic](https://github.com/0xAX/asm/blob/master/content/asm_6.md) - [Part 7. Assembly interaction with high-level programming languages](https://github.com/0xAX/asm/blob/master/content/asm_7.md) +The code examples that accompany the chapters live in the [examples](./examples/) directory. + ## Requirements To run code examples, you need the following tools: diff --git a/content/asm_6.md b/content/asm_6.md index 1283474..f689a87 100644 --- a/content/asm_6.md +++ b/content/asm_6.md @@ -397,13 +397,16 @@ _parse_first_float_vector: ;; Initialize the rsi register with the pointer to the place where ;; the strtod(3) will finish its work. mov rsi, end_buffer_1 + ;; Preserve the pointer to the current floating-point value in the r12 register, + ;; because the strtod(3) may change it. + mov r12, rdi ;; Call the strtod(3) to convert a floating-point value from the input buffer to double representation. call strtod ;; Preserve the pointer to the next floating-point value from the input buffer ;; in the rax register. mov rax, [rel end_buffer_1] ;; Check whether it is the end of the input string. - cmp rax, rdi + cmp rax, r12 ;; Proceed with the second vector if we reached the end of the first vector. je _read_second_float_vector ;; Store the reference to the beginning of the buffer where we will store @@ -440,7 +443,7 @@ As soon as we finish parsing the floating-point values for the first vector, we - To store the number of values within the second vector, we will use the `r15` register instead of `r14`. > [!TIP] -> For reference, you can find the whole code [here](https://github.com/0xAX/asm/blob/master/float/dot_product.asm). +> For reference, you can find the whole code [here](../examples/float/dot_product.asm). ### Calculation of the dot product diff --git a/content/asm_7.md b/content/asm_7.md index 60966f8..8b90620 100644 --- a/content/asm_7.md +++ b/content/asm_7.md @@ -216,12 +216,18 @@ After defining the assembly code, we can specify input and output operands that For example: +- `a` - tells the compiler to use the `rax` register. +- `b` - tells the compiler to use the `rbx` register. +- `c` - tells the compiler to use the `rcx` register. +- `d` - tells the compiler to use the `rdx` register. +- `S` - tells the compiler to use the `rsi` register. +- `D` - tells the compiler to use the `rdi` register. - `r` - tells the compiler to use a [general-purpose register](./asm_2.md). - `g` - tells the compiler to use any register, memory, or immediate integer operand. - `f` - tells the compiler to use a [floating-point register](./asm_6.md). - `m` - forces the compiler to use a memory location. -You can find all the supported constraint strings in the [official documentation](https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Output-Operands). +You can find all the supported constraint strings in the official documentation. For more information, see the [Simple Constraints](https://gcc.gnu.org/onlinedocs/gcc/Simple-Constraints.html) and [Constraints for Particular Machines](https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html) documents. Let's try to rewrite our `hello world` program using the inline assembly: @@ -239,14 +245,17 @@ int main() { "movq %1, %%rsi \n\t" // rsi = str - Set the second argument of `sys_write` to the reference of the `str` variable. "movq %2, %%rdx \n\t" // rdx = len(str) - Set the third argument of `sys_write` to the length of the `str` variable's value. "syscall" // Call the `sys_write` system call. - : "=g"(ret) // Return the result in the `ret` variable. - : "g"(str), "g" (len)); // Put `str` and `len` variables in any general operand (memory, register, or immediate, if possible) + : "=a"(ret) // Return the result of the system call from the `rax` register in the `ret` variable. + : "g"(str), "g" (len) // Put `str` and `len` variables in any general operand (memory, register, or immediate, if possible) + : "rdi", "rsi", "rdx", "memory"); // Tell the compiler about the registers and the memory that the assembly code changes. printf("Bytes written: %d\n", ret); return 0; } ``` +The last part of the assembly block is the list of clobbers. Here we tell the compiler which registers and memory our assembly code changes. The compiler does not analyze the assembly code, so without this list it may store its own values in the `rdi`, `rsi`, or `rdx` registers and lose them. We do not add `rax` to this list, because the `=a` output operand already describes it. + > [!NOTE] > In the example above, we used [GNU `as` assembly](https://sourceware.org/binutils/docs/as.html), which has a slightly different syntax from the [NASM](https://nasm.us/) assembly. The main difference is that the order of operands for the `movq` instruction is reversed, and we move the value of the left operand to the right. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..507eaaa --- /dev/null +++ b/examples/README.md @@ -0,0 +1,20 @@ +# Code examples + +These are the sample programs that accompany the book. Each directory is a standalone program with its own `Makefile` and `README.md`. + +| Example | Description | Chapter | +| --------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------- | +| [hello](./hello/) | The basic "Hello, World!" program | [Part 1. Introduction](../content/asm_1.md) | +| [sum](./sum/) | Sum of two integer numbers | [Part 2. The `x86_64` concepts](../content/asm_2.md) | +| [stack](./stack/) | Sum of two command line arguments | [Part 3. Journey through the stack](../content/asm_3.md) | +| [strings](./strings/) | Reverse a given input string | [Part 4. Data manipulation](../content/asm_4.md) | +| [float](./float/) | Dot product of two vectors of doubles | [Part 6. Floating-point arithmetic](../content/asm_6.md) | +| [casm](./casm/) | Interaction between assembly and C | [Part 7. Assembly interaction with high-level programming languages](../content/asm_7.md) | + +To build an example, go to its directory and run the following command: + +```bash +make +``` + +The tools you need to build and run the examples are listed in the [Requirements](../README.md#requirements) section. diff --git a/casm/README.md b/examples/casm/README.md similarity index 84% rename from casm/README.md rename to examples/casm/README.md index a52b8aa..d8d18af 100644 --- a/casm/README.md +++ b/examples/casm/README.md @@ -12,4 +12,4 @@ To build each program, go to its directory and run: make ``` -For more details, read [Part 7. Assembly interaction with high-level programming languages](https://github.com/0xAX/asm/blob/master/content/asm_7.md). +For more details, read [Part 7. Assembly interaction with high-level programming languages](../../content/asm_7.md). diff --git a/casm/casm1/Makefile b/examples/casm/casm1/Makefile similarity index 100% rename from casm/casm1/Makefile rename to examples/casm/casm1/Makefile diff --git a/casm/casm1/casm.asm b/examples/casm/casm1/casm.asm similarity index 100% rename from casm/casm1/casm.asm rename to examples/casm/casm1/casm.asm diff --git a/casm/casm2/Makefile b/examples/casm/casm2/Makefile similarity index 100% rename from casm/casm2/Makefile rename to examples/casm/casm2/Makefile diff --git a/casm/casm2/casm.c b/examples/casm/casm2/casm.c similarity index 72% rename from casm/casm2/casm.c rename to examples/casm/casm2/casm.c index f8027b8..718d3d2 100644 --- a/casm/casm2/casm.c +++ b/examples/casm/casm2/casm.c @@ -11,8 +11,9 @@ int main() { "movq %1, %%rsi \n\t" // rsi = str - Set the second argument of `sys_write` to the reference of the `str` variable. "movq %2, %%rdx \n\t" // rdx = len(str) - Set the third argument of `sys_write` to the length of the `str` variable's value. "syscall" // Call the `sys_write` system call. - : "=g"(ret) // Return the result in the `ret` variable. - : "g"(str), "g" (len)); // Put `str` and `len` variables in any general operand (memory, register, or immediate, if possible) + : "=a"(ret) // Return the result of the system call from the `rax` register in the `ret` variable. + : "g"(str), "g" (len) // Put `str` and `len` variables in any general operand (memory, register, or immediate, if possible) + : "rdi", "rsi", "rdx", "memory"); // Tell the compiler about the registers and the memory that the assembly code changes. printf("Bytes written: %d\n", ret); return 0; diff --git a/casm/casm3/Makefile b/examples/casm/casm3/Makefile similarity index 100% rename from casm/casm3/Makefile rename to examples/casm/casm3/Makefile diff --git a/casm/casm3/casm.asm b/examples/casm/casm3/casm.asm similarity index 100% rename from casm/casm3/casm.asm rename to examples/casm/casm3/casm.asm diff --git a/casm/casm3/casm.c b/examples/casm/casm3/casm.c similarity index 100% rename from casm/casm3/casm.c rename to examples/casm/casm3/casm.c diff --git a/float/Makefile b/examples/float/Makefile similarity index 100% rename from float/Makefile rename to examples/float/Makefile diff --git a/examples/float/README.md b/examples/float/README.md new file mode 100644 index 0000000..bded35f --- /dev/null +++ b/examples/float/README.md @@ -0,0 +1,33 @@ +# Dot product + +This is a simple program that reads two double vectors and calculates the [dot product](https://en.wikipedia.org/wiki/Dot_product) of their values. + +To build the program, run: + +```bash +make +``` + +Run the program in a terminal and type the values of the vectors when it asks for them: + +```bash +$ ./dot_product +Input the first vector: 2.5 3.17 +Input the second vector: 4.22 100.1 +Dot product = 327.867000 +``` + +> [!IMPORTANT] +> Do not pipe the input to the program. It expects a terminal and gives either a wrong answer or no answer at all when the standard input and output are a pipe or a file: +> +> ```bash +> $ printf '2.5 3.17\n4.22 100.1\n' | ./dot_product +> Input the first vector: Input the second vector: Error: the number of values in vectors should be the same +> ``` +> +> There are two reasons for this behavior: +> +> - The program reads each vector with a single `sys_read` system call. A terminal returns one line per read, but a pipe returns everything that was already written to it. So the first read takes both lines, and the program sees all the values as the first vector and no values as the second one. +> - The program prints the result with the C `printf` function, but exits with the raw `sys_exit` system call. The C library writes to a terminal line by line, but buffers the whole output when it writes to a pipe or a file. Nothing flushes that buffer before `sys_exit` ends the program, so the result never gets printed. + +For more details, read [Part 6. Floating-point arithmetic](../../content/asm_6.md). diff --git a/float/dot_product.asm b/examples/float/dot_product.asm similarity index 97% rename from float/dot_product.asm rename to examples/float/dot_product.asm index 58053b7..33a592f 100644 --- a/float/dot_product.asm +++ b/examples/float/dot_product.asm @@ -122,6 +122,9 @@ _parse_first_float_vector: ;; Initialize the rsi register with the pointer to the place where ;; the strtod(3) will finish its work. mov rsi, end_buffer_1 + ;; Preserve the pointer to the current floating-point value in the r12 register, + ;; because the strtod(3) may change it. + mov r12, rdi ;; Call the strtod(3) to convert a floating-point value from the input buffer to double representation. call strtod @@ -129,7 +132,7 @@ _parse_first_float_vector: ;; in the rax register. mov rax, [rel end_buffer_1] ;; Check whether it is the end of the input string. - cmp rax, rdi + cmp rax, r12 ;; Proceed with the second vector if we reached the end of the first vector. je _read_second_float_vector @@ -199,6 +202,9 @@ _parse_second_float_vector: ;; Initialize the rsi register with the pointer which to the place where ;; the strtod(3) will finish its work. mov rsi, end_buffer_2 + ;; Preserve the pointer to the current floating-point value in the r12 register, + ;; because the strtod(3) may change it. + mov r12, rdi ;; Call the strtod(3) call strtod @@ -206,7 +212,7 @@ _parse_second_float_vector: ;; in the rax register. mov rax, [rel end_buffer_2] ;; Check whether it is the end of the input string. - cmp rax, rdi + cmp rax, r12 ;; Calculate the dot product after we have both vectors. je _calculate_dot_product diff --git a/hello/Makefile b/examples/hello/Makefile similarity index 100% rename from hello/Makefile rename to examples/hello/Makefile diff --git a/hello/README.md b/examples/hello/README.md similarity index 58% rename from hello/README.md rename to examples/hello/README.md index 7f68ab2..f920c9d 100644 --- a/hello/README.md +++ b/examples/hello/README.md @@ -8,4 +8,4 @@ To build the program, run: make ``` -For more details, read [Part 1. Introduction](https://github.com/0xAX/asm/blob/master/content/asm_1.md). +For more details, read [Part 1. Introduction](../../content/asm_1.md). diff --git a/hello/hello.asm b/examples/hello/hello.asm similarity index 100% rename from hello/hello.asm rename to examples/hello/hello.asm diff --git a/stack/Makefile b/examples/stack/Makefile similarity index 100% rename from stack/Makefile rename to examples/stack/Makefile diff --git a/stack/README.md b/examples/stack/README.md similarity index 57% rename from stack/README.md rename to examples/stack/README.md index 6ec1946..d47d9c8 100644 --- a/stack/README.md +++ b/examples/stack/README.md @@ -8,4 +8,4 @@ To build the program, run: make ``` -For more details, read [Part 3. Journey through the stack](https://github.com/0xAX/asm/blob/master/content/asm_3.md). +For more details, read [Part 3. Journey through the stack](../../content/asm_3.md). diff --git a/stack/stack.asm b/examples/stack/stack.asm similarity index 100% rename from stack/stack.asm rename to examples/stack/stack.asm diff --git a/strings/Makefile b/examples/strings/Makefile similarity index 100% rename from strings/Makefile rename to examples/strings/Makefile diff --git a/strings/README.md b/examples/strings/README.md similarity index 57% rename from strings/README.md rename to examples/strings/README.md index 798f3b1..28a6276 100644 --- a/strings/README.md +++ b/examples/strings/README.md @@ -8,4 +8,4 @@ To build the program, run: make ``` -For more details, read [Part 4. Journey through the stack](https://github.com/0xAX/asm/blob/master/content/asm_4.md). +For more details, read [Part 4. Data manipulation](../../content/asm_4.md). diff --git a/strings/reverse.asm b/examples/strings/reverse.asm similarity index 100% rename from strings/reverse.asm rename to examples/strings/reverse.asm diff --git a/sum/Makefile b/examples/sum/Makefile similarity index 100% rename from sum/Makefile rename to examples/sum/Makefile diff --git a/sum/README.md b/examples/sum/README.md similarity index 65% rename from sum/README.md rename to examples/sum/README.md index ad975ed..9c58440 100644 --- a/sum/README.md +++ b/examples/sum/README.md @@ -8,4 +8,4 @@ To build the program, run: make ``` -For more details, read [Part 2. The `x86_64` concepts](https://github.com/0xAX/asm/blob/master/content/asm_2.md). +For more details, read [Part 2. The `x86_64` concepts](../../content/asm_2.md). diff --git a/sum/sum.asm b/examples/sum/sum.asm similarity index 100% rename from sum/sum.asm rename to examples/sum/sum.asm diff --git a/float/README.md b/float/README.md deleted file mode 100644 index f473771..0000000 --- a/float/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Dot product - -This is a simple program that reads two double vectors and calculates the [dot product](https://en.wikipedia.org/wiki/Dot_product) of their values. - -To build the program, run: - -```bash -make -``` - -For more details, read [Part 6. Floating-point arithmetic](https://github.com/0xAX/asm/blob/master/content/asm_6.md). diff --git a/scripts/ci/pty-run.py b/scripts/ci/pty-run.py new file mode 100755 index 0000000..1c61415 --- /dev/null +++ b/scripts/ci/pty-run.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Run a program on a pseudo-terminal and feed it lines of input. + +Some of the examples only behave correctly when their standard input and +output are a terminal: + + - They read the input with a single `sys_read` per vector. On a pipe, one + `read` returns every line that is already buffered, so the program sees + all of the input at once instead of one line at a time. + - They print the result with the C `printf` and then exit with the raw + `sys_exit` system call. Outside of a terminal, the C library buffers the + output fully and nothing flushes it before the program exits. + +Both of these go away on a terminal, where reads return a line at a time and +the C library flushes the output on every new line. + +Usage: + + pty-run.py [argument ...] --- [input line ...] + +The output of the program is written to the standard output and the exit code +of the program becomes the exit code of this script. +""" + +import os +import pty +import select +import sys + +# How long to wait for the program to stop producing output before the next +# input line is sent, and before the program is considered stuck. +QUIET_TIMEOUT = 0.5 +TOTAL_TIMEOUT = 30.0 + + +def main(): + argv = sys.argv[1:] + if "---" in argv: + split = argv.index("---") + command, lines = argv[:split], argv[split + 1:] + else: + command, lines = argv, [] + + if not command: + sys.exit("usage: pty-run.py [argument ...] --- [input line ...]") + + pid, fd = pty.fork() + if pid == 0: + # The child process becomes the program that we want to run. + try: + os.execvp(command[0], command) + except OSError as error: + print(f"failed to execute {command[0]}: {error}", file=sys.stderr) + os._exit(127) + + output = bytearray() + deadline = TOTAL_TIMEOUT + pending = list(lines) + + while True: + try: + readable, _, _ = select.select([fd], [], [], QUIET_TIMEOUT) + except OSError: + break + + if readable: + try: + chunk = os.read(fd, 4096) + except OSError: + # The child closed the terminal, which means it has exited. + break + if not chunk: + break + output.extend(chunk) + continue + + # The program produced no output for QUIET_TIMEOUT seconds. It is + # either waiting for the next line of input or it is done. + if pending: + os.write(fd, pending.pop(0).encode() + b"\n") + continue + + deadline -= QUIET_TIMEOUT + if deadline <= 0: + break + + os.close(fd) + _, status = os.waitpid(pid, 0) + + # The terminal echoes back everything that we write to it, so drop the + # echoed input lines to leave only what the program itself printed. + text = output.decode(errors="replace").replace("\r\n", "\n") + for line in lines: + text = text.replace(line + "\n", "", 1) + + sys.stdout.write(text) + sys.stdout.flush() + + if os.WIFEXITED(status): + sys.exit(os.WEXITSTATUS(status)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/ci/test-examples.sh b/scripts/ci/test-examples.sh new file mode 100755 index 0000000..9f85331 --- /dev/null +++ b/scripts/ci/test-examples.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# +# Build each example, run it, and check that it prints what the related +# chapter says it should print. +# +# Usage: +# +# ./scripts/ci/test-examples.sh # test every example +# ./scripts/ci/test-examples.sh hello sum # test only the given examples +# +# The exit code is 0 if every example was built and produced the expected +# output, and 1 otherwise. + +set -u -o pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +EXAMPLES="${ROOT}/examples" +PTY_RUN="${ROOT}/scripts/ci/pty-run.py" + +# The list of the examples that this script knows how to test. Each of them has +# a `test_` function below. +ALL_EXAMPLES=(hello sum stack strings float casm1 casm2 casm3) + +failed=0 +current="" + +# Print a message about the failed check and remember that something went +# wrong. The exit code of the script is based on the `failed` variable. +fail() { + printf ' FAIL %s\n' "$1" >&2 + failed=1 +} + +pass() { + printf ' ok %s\n' "$1" +} + +# Build the example in the given directory. The directory is relative to the +# `examples` directory. +build() { + local directory="${EXAMPLES}/$1" + local output + + if ! output="$(make -C "${directory}" 2>&1)"; then + fail "${current}: build failed" + printf '%s\n' "${output}" >&2 + return 1 + fi + + pass "${current}: builds" + return 0 +} + +# Remove the build artifacts of the example in the given directory. +clean() { + make -C "${EXAMPLES}/$1" clean >/dev/null 2>&1 || true +} + +# Check that the given actual output is exactly the same as the expected one. +expect_output() { + local description="$1" expected="$2" actual="$3" + + if [ "${actual}" = "${expected}" ]; then + pass "${current}: ${description}" + else + fail "${current}: ${description}" + printf ' expected: %q\n' "${expected}" >&2 + printf ' actual: %q\n' "${actual}" >&2 + fi +} + +# Check that the given actual output contains the expected substring. +expect_contains() { + local description="$1" expected="$2" actual="$3" + + case "${actual}" in + *"${expected}"*) + pass "${current}: ${description}" + ;; + *) + fail "${current}: ${description}" + printf ' expected to contain: %q\n' "${expected}" >&2 + printf ' actual: %q\n' "${actual}" >&2 + ;; + esac +} + +# Check that the given exit code is the expected one. +expect_status() { + local description="$1" expected="$2" actual="$3" + + if [ "${actual}" -eq "${expected}" ]; then + pass "${current}: ${description}" + else + fail "${current}: ${description}" + printf ' expected exit code %d, got %d\n' "${expected}" "${actual}" >&2 + fi +} + +test_hello() { + build hello || return + local output status + output="$("${EXAMPLES}/hello/hello")" + status=$? + expect_output "prints the greeting" "hello, world!" "${output}" + expect_status "exits with success" 0 "${status}" + clean hello +} + +test_sum() { + build sum || return + local output status + output="$("${EXAMPLES}/sum/sum")" + status=$? + expect_output "reports the correct sum" "The sum is correct!" "${output}" + expect_status "exits with success" 0 "${status}" + clean sum +} + +test_stack() { + build stack || return + local output status + + # The program pads every digit of the result to 8 bytes, so the `NUL` + # bytes are removed before the result is compared. + output="$("${EXAMPLES}/stack/stack" 5 10 | tr -d '\0')" + status=$? + expect_output "sums two command-line arguments" "15" "${output}" + expect_status "exits with success" 0 "${status}" + + output="$("${EXAMPLES}/stack/stack" 5 | tr -d '\0')" + expect_output "output contains an error about the number of arguments" \ + "Error: expected two command-line arguments" "${output}" + + clean stack +} + +test_strings() { + build strings || return + local output status + output="$("${EXAMPLES}/strings/reverse")" + status=$? + expect_output "reverses the string" "!dlrow olleH" "${output}" + expect_status "exits with success" 0 "${status}" + clean strings +} + +test_float() { + build float || return + local output status + + # This example needs a terminal, see the comment in `pty-run.py`. + output="$(python3 "${PTY_RUN}" "${EXAMPLES}/float/dot_product" \ + --- "2.5 3.17" "4.22 100.1")" + status=$? + expect_contains "calculates the dot product" \ + "Dot product = 327.867000" "${output}" + expect_status "exits with success" 0 "${status}" + + clean float +} + +test_casm1() { + build casm/casm1 || return + local output status + output="$("${EXAMPLES}/casm/casm1/casm")" + status=$? + expect_output "prints the greeting" "hello, world!" "${output}" + expect_status "exits with success" 0 "${status}" + clean casm/casm1 +} + +test_casm2() { + build casm/casm2 || return + local output status + output="$("${EXAMPLES}/casm/casm2/casm")" + status=$? + # The `"=a"` output operand of the inline assembly takes the result of + # the system call from the `rax` register, so the number of the written + # bytes is the length of the string and does not depend on how the + # compiler allocates the registers. + expect_output "writes the string with inline assembly" \ + "$(printf 'Hello World\nBytes written: 12')" "${output}" + expect_status "exits with success" 0 "${status}" + clean casm/casm2 +} + +test_casm3() { + build casm/casm3 || return + local output status + + output="$("${EXAMPLES}/casm/casm3/casm" hello)" + status=$? + expect_output "returns the length of the argument" \ + "The argument length is - 5" "${output}" + expect_status "exits with success" 0 "${status}" + + output="$("${EXAMPLES}/casm/casm3/casm" 2>&1)" + status=$? + expect_contains "output contains an error about the number of arguments" \ + "must have 1 command line argument" "${output}" + expect_status "exits with failure" 1 "${status}" + + clean casm/casm3 +} + +main() { + local examples=() + + if [ "$#" -gt 0 ]; then + examples=("$@") + else + examples=("${ALL_EXAMPLES[@]}") + fi + + for example in "${examples[@]}"; do + if ! declare -F "test_${example}" >/dev/null; then + printf 'unknown example: %s\n' "${example}" >&2 + printf 'known examples: %s\n' "${ALL_EXAMPLES[*]}" >&2 + exit 1 + fi + + current="${example}" + printf '%s\n' "${example}" + "test_${example}" + done + + if [ "${failed}" -ne 0 ]; then + printf '\nsome examples failed\n' >&2 + exit 1 + fi + + printf '\nall examples passed\n' +} + +main "$@"