Customize Shell

Shell Type

buildtest will default to bash shell when running test, but we can configure shell option using the shell field. The shell field is defined in schema as follows:

"shell": {
  "type": "string",
  "description": "Specify a shell launcher to use when running jobs. This sets the shebang line in your test script. The ``shell`` key can be used with ``run`` section to describe content of script and how its executed",
  "pattern": "^(/bin/bash|/bin/sh|/bin/csh|/bin/tcsh|/bin/zsh|bash|sh|csh|tcsh|zsh|python).*"
},

The shell pattern is a regular expression where one can specify a shell name along with shell options. The shell will configure the shebang in the test-script. In this example, we illustrate a few tests using different shell field.

buildspecs:
  _bin_sh_shell:
    executor: generic.local.sh
    type: script
    description: "/bin/sh shell example"
    shell: /bin/sh
    tags: [tutorials]
    run: "bzip2 --help"

  _bin_bash_shell:
    executor: generic.local.bash
    type: script
    description: "/bin/bash shell example"
    shell: /bin/bash
    tags: [tutorials]
    run: "bzip2 -h"

  bash_shell:
    executor: generic.local.bash
    type: script
    description: "bash shell example"
    shell: bash
    tags: [tutorials]
    run: "echo $SHELL"

  sh_shell:
    executor: generic.local.sh
    type: script
    description: "sh shell example"
    shell: sh
    tags: [tutorials]
    run: "echo $SHELL"

  shell_options:
    executor: generic.local.sh
    type: script
    description: "shell options"
    shell: "sh -x"
    tags: [tutorials]
    run: |
      echo $SHELL
      hostname

The generated test-script for buildspec _bin_sh_shell will specify shebang /bin/sh because we specified shell: /bin/sh:

#!/bin/sh
# Content of run section
bzip2 --help

If you don’t specify a shell path such as shell: sh, then buildtest will resolve path by looking in $PATH and build the shebang line.

In test shell_options we specify shell: "sh -x", buildtest will tack on the shell options into the called script as follows:

#!/bin/bash


############# START VARIABLE DECLARATION ########################
export BUILDTEST_TEST_NAME=shell_options
export BUILDTEST_TEST_ROOT=/Users/siddiq90/Documents/GitHubDesktop/buildtest/var/tests/generic.local.sh/shell_examples/shell_options/0
export BUILDTEST_BUILDSPEC_DIR=/Users/siddiq90/Documents/GitHubDesktop/buildtest/tutorials
export BUILDTEST_STAGE_DIR=/Users/siddiq90/Documents/GitHubDesktop/buildtest/var/tests/generic.local.sh/shell_examples/shell_options/0/stage
export BUILDTEST_TEST_ID=95c11f54-bbb1-4154-849d-44313e4417c2
############# END VARIABLE DECLARATION   ########################


# source executor startup script
source /Users/siddiq90/Documents/GitHubDesktop/buildtest/var/executor/generic.local.sh/before_script.sh
# Run generated script
sh -x /Users/siddiq90/Documents/GitHubDesktop/buildtest/var/tests/generic.local.sh/shell_examples/shell_options/0/stage/shell_options.sh
# Get return code
returncode=$?
# Exit with return code
exit $returncode

If you prefer csh or tcsh for writing scripts just set shell: csh or shell: tcsh, note you will need to match this with appropriate executor. For now use executor: generic.local.csh to run your csh/tcsh scripts. In this example below we define a script using csh, take note of run section we can write csh style.

buildspecs:
  csh_shell:
    executor: generic.local.csh
    type: script
    description: "csh shell example"
    shell: csh
    tags: [tutorials]
    vars:
      file: "/etc/csh.cshrc"
    run: |
      if (! -e $file) then
        echo "$file file not found"
        exit 1
      else
        echo "$file file found"        
      endif

Customize Shebang

You may customize the shebang line in testscript using shebang field. This takes precedence over the shell property which automatically detects the shebang based on shell path.

In next example we have two tests bash_login_shebang and bash_nonlogin_shebang which tests if shell is Login or Non-Login. The #!/bin/bash -l indicates we want to run in login shell and expects an output of Login Shell while test bash_nonlogin_shebang should run in default behavior which is non-login shell and expects output Not Login Shell. We match this with regular expression with stdout stream.

buildspecs:
  bash_login_shebang:
    type: script
    executor: generic.local.bash
    shebang: "#!/bin/bash -l"
    description: customize shebang line with bash login shell
    tags: tutorials
    run: shopt -q login_shell && echo 'Login Shell' || echo 'Not Login Shell'
    status:
      regex:
        exp: "^Login Shell$"
        stream: stdout

  bash_nonlogin_shebang:
    type: script
    executor: generic.local.bash
    shebang: "#!/bin/bash"
    description: customize shebang line with default bash (nonlogin) shell
    tags: tutorials
    run: shopt -q login_shell && echo 'Login Shell' || echo 'Not Login Shell'
    status:
      regex:
        exp: "^Not Login Shell$"
        stream: stdout

Now let’s run this test as we see the following.

buildtest build -b tutorials/shebang.yml
$ buildtest build -b tutorials/shebang.yml
╭───────────────────────────── buildtest summary ──────────────────────────────╮
│                                                                              │
│ User:               docs                                                     │
│ Hostname:           build-23444606-project-280831-buildtest                  │
│ Platform:           Linux                                                    │
│ Current Time:       2024/02/14 17:19:32                                      │
│ buildtest path:     /home/docs/checkouts/readthedocs.org/user_builds/buildte │
│ buildtest version:  1.7                                                      │
│ python path:        /home/docs/checkouts/readthedocs.org/user_builds/buildte │
│ python version:     3.8.18                                                   │
│ Configuration File: /tmp/tmpjcb9dx2w/config.yml                              │
│ Test Directory:     /tmp/tmpjcb9dx2w/var/tests                               │
│ Report File:        /tmp/tmpjcb9dx2w/var/report.json                         │
│ Command:            /home/docs/checkouts/readthedocs.org/user_builds/buildte │
│                                                                              │
╰──────────────────────────────────────────────────────────────────────────────╯
───────────────────────────  Discovering Buildspecs ────────────────────────────
                             Discovered buildspecs                              
╔══════════════════════════════════════════════════════════════════════════════╗
║ buildspec                                                                    ║
╟──────────────────────────────────────────────────────────────────────────────╢
║ /home/docs/checkouts/readthedocs.org/user_builds/buildtest/checkouts/stable/ ║
║ tutorials/shebang.yml                                                        ║
╚══════════════════════════════════════════════════════════════════════════════╝


Total Discovered Buildspecs:  1
Total Excluded Buildspecs:  0
Detected Buildspecs after exclusion:  1
────────────────────────────── Parsing Buildspecs ──────────────────────────────
Valid Buildspecs: 1
Invalid Buildspecs: 0
/home/docs/checkouts/readthedocs.org/user_builds/buildtest/checkouts/stable/tutorials/shebang.yml: VALID
Total builder objects created: 2
                            Builders by type=script                             
┏━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┓
┃          ┃        ┃          ┃          ┃       ┃       ┃ descript ┃ buildsp ┃
┃ builder  ┃ type   ┃ executor ┃ compiler ┃ nodes ┃ procs ┃ ion      ┃ ecs     ┃
┡━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━┩
│ bash_log │ script │ generic. │ None     │ None  │ None  │ customiz │ /home/d │
│ in_sheba │        │ local.ba │          │       │       │ e        │ ocs/che │
│ ng/9e041 │        │ sh       │          │       │       │ shebang  │ ckouts/ │
│ ccd      │        │          │          │       │       │ line     │ readthe │
│          │        │          │          │       │       │ with     │ docs.or │
│          │        │          │          │       │       │ bash     │ g/user_ │
│          │        │          │          │       │       │ login    │ builds/ │
│          │        │          │          │       │       │ shell    │ buildte │
│          │        │          │          │       │       │          │ st/chec │
│          │        │          │          │       │       │          │ kouts/s │
│          │        │          │          │       │       │          │ table/t │
│          │        │          │          │       │       │          │ utorial │
│          │        │          │          │       │       │          │ s/sheba │
│          │        │          │          │       │       │          │ ng.yml  │
├──────────┼────────┼──────────┼──────────┼───────┼───────┼──────────┼─────────┤
│ bash_non │ script │ generic. │ None     │ None  │ None  │ customiz │ /home/d │
│ login_sh │        │ local.ba │          │       │       │ e        │ ocs/che │
│ ebang/41 │        │ sh       │          │       │       │ shebang  │ ckouts/ │
│ 7e9e62   │        │          │          │       │       │ line     │ readthe │
│          │        │          │          │       │       │ with     │ docs.or │
│          │        │          │          │       │       │ default  │ g/user_ │
│          │        │          │          │       │       │ bash     │ builds/ │
│          │        │          │          │       │       │ (nonlogi │ buildte │
│          │        │          │          │       │       │ n) shell │ st/chec │
│          │        │          │          │       │       │          │ kouts/s │
│          │        │          │          │       │       │          │ table/t │
│          │        │          │          │       │       │          │ utorial │
│          │        │          │          │       │       │          │ s/sheba │
│          │        │          │          │       │       │          │ ng.yml  │
└──────────┴────────┴──────────┴──────────┴───────┴───────┴──────────┴─────────┘
──────────────────────────────── Building Test ─────────────────────────────────
bash_login_shebang/9e041ccd: Creating Test Directory: /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_login_shebang/9e041ccd
bash_nonlogin_shebang/417e9e62: Creating Test Directory: /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_nonlogin_shebang/417e9e62
──────────────────────────────── Running Tests ─────────────────────────────────
Spawning 1 processes for processing builders
───────────────────────────────── Iteration 1 ──────────────────────────────────
bash_nonlogin_shebang/417e9e62 does not have any dependencies adding test to queue
bash_login_shebang/9e041ccd does not have any dependencies adding test to queue
     Builders Eligible to Run     
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Builder                        ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ bash_nonlogin_shebang/417e9e62 │
│ bash_login_shebang/9e041ccd    │
└────────────────────────────────┘
bash_nonlogin_shebang/417e9e62: Current Working Directory : /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_nonlogin_shebang/417e9e62/stage
bash_nonlogin_shebang/417e9e62: Running Test via command: bash bash_nonlogin_shebang_build.sh
bash_nonlogin_shebang/417e9e62: Test completed in 0.006073 seconds with returncode: 0
bash_nonlogin_shebang/417e9e62: Writing output file -  /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_nonlogin_shebang/417e9e62/bash_nonlogin_shebang.out
bash_nonlogin_shebang/417e9e62: Writing error file - /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_nonlogin_shebang/417e9e62/bash_nonlogin_shebang.err
bash_nonlogin_shebang/417e9e62: performing regular expression - '^Not Login Shell$' on file: /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_nonlogin_shebang/417e9e62/bash_nonlogin_shebang.out
bash_nonlogin_shebang/417e9e62: Regular Expression Match - Success!
bash_login_shebang/9e041ccd: Current Working Directory : /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_login_shebang/9e041ccd/stage
bash_login_shebang/9e041ccd: Running Test via command: bash bash_login_shebang_build.sh
bash_login_shebang/9e041ccd: Test completed in 0.005745 seconds with returncode: 0
bash_login_shebang/9e041ccd: Writing output file -  /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_login_shebang/9e041ccd/bash_login_shebang.out
bash_login_shebang/9e041ccd: Writing error file - /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_login_shebang/9e041ccd/bash_login_shebang.err
bash_login_shebang/9e041ccd: performing regular expression - '^Login Shell$' on file: /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/shebang/bash_login_shebang/9e041ccd/bash_login_shebang.out
bash_login_shebang/9e041ccd: Regular Expression Match - Failed!
                                  Test Summary                                  
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━┓
┃ builder                 ┃ executor           ┃ status ┃ returncode ┃ runtime ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━┩
│ bash_login_shebang/9e04 │ generic.local.bash │ FAIL   │ 0          │ 0.006   │
│ 1ccd                    │                    │        │            │         │
├─────────────────────────┼────────────────────┼────────┼────────────┼─────────┤
│ bash_nonlogin_shebang/4 │ generic.local.bash │ PASS   │ 0          │ 0.006   │
│ 17e9e62                 │                    │        │            │         │
└─────────────────────────┴────────────────────┴────────┴────────────┴─────────┘



Passed Tests: 1/2 Percentage: 50.000%
Failed Tests: 1/2 Percentage: 50.000%


Adding 2 test results to /tmp/tmpjcb9dx2w/var/report.json
Writing Logfile to /tmp/tmpjcb9dx2w/var/logs/buildtest_2jd13269.log

If we look at the generated test for bash_login_shebang we see the shebang line is passed into the script:

$ cat $(buildtest path -t bash_login_shebang)
#!/bin/bash -l
set -eo pipefail
# Content of run section
shopt -q login_shell && echo 'Login Shell' || echo 'Not Login Shell'

Python Shell

You can use script schema to write python scripts using the run property. In order to write python code you must set shell property to python interpreter such as shell: python or full path to python wrapper such as shell: /usr/bin/python.

Here is a python example calculating area of circle

buildspecs:
  circle_area:
    executor: generic.local.bash
    type: script
    shell: python
    description: "Calculate circle of area given a radius"
    tags: [tutorials, python]
    run: |
      import math
      radius = 2
      area = math.pi * radius * radius
      print("Circle Radius ", radius)
      print("Area of circle ", area)

Note

Python scripts are very picky when it comes to formatting, in the run section if you are defining multiline python script you must remember to use 2 space indent to register multiline string. buildtest will extract the content from run section and inject in your test script. To ensure proper formatting for a more complex python script you may be better off writing a python script in separate file and invoke the python script in the run section.

Let’s try building this example and analyze the generated test script.

buildtest build -b tutorials/python-shell.yml
$ buildtest build -b tutorials/python-shell.yml
╭───────────────────────────── buildtest summary ──────────────────────────────╮
│                                                                              │
│ User:               docs                                                     │
│ Hostname:           build-23444606-project-280831-buildtest                  │
│ Platform:           Linux                                                    │
│ Current Time:       2024/02/14 17:19:33                                      │
│ buildtest path:     /home/docs/checkouts/readthedocs.org/user_builds/buildte │
│ buildtest version:  1.7                                                      │
│ python path:        /home/docs/checkouts/readthedocs.org/user_builds/buildte │
│ python version:     3.8.18                                                   │
│ Configuration File: /tmp/tmpjcb9dx2w/config.yml                              │
│ Test Directory:     /tmp/tmpjcb9dx2w/var/tests                               │
│ Report File:        /tmp/tmpjcb9dx2w/var/report.json                         │
│ Command:            /home/docs/checkouts/readthedocs.org/user_builds/buildte │
│                                                                              │
╰──────────────────────────────────────────────────────────────────────────────╯
───────────────────────────  Discovering Buildspecs ────────────────────────────
                             Discovered buildspecs                              
╔══════════════════════════════════════════════════════════════════════════════╗
║ buildspec                                                                    ║
╟──────────────────────────────────────────────────────────────────────────────╢
║ /home/docs/checkouts/readthedocs.org/user_builds/buildtest/checkouts/stable/ ║
║ tutorials/python-shell.yml                                                   ║
╚══════════════════════════════════════════════════════════════════════════════╝


Total Discovered Buildspecs:  1
Total Excluded Buildspecs:  0
Detected Buildspecs after exclusion:  1
────────────────────────────── Parsing Buildspecs ──────────────────────────────
Valid Buildspecs: 1
Invalid Buildspecs: 0
/home/docs/checkouts/readthedocs.org/user_builds/buildtest/checkouts/stable/tutorials/python-shell.yml: VALID
Total builder objects created: 1
                            Builders by type=script                             
┏━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┓
┃          ┃        ┃          ┃          ┃       ┃       ┃ descript ┃ buildsp ┃
┃ builder  ┃ type   ┃ executor ┃ compiler ┃ nodes ┃ procs ┃ ion      ┃ ecs     ┃
┡━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━┩
│ circle_a │ script │ generic. │ None     │ None  │ None  │ Calculat │ /home/d │
│ rea/54bb │        │ local.ba │          │       │       │ e circle │ ocs/che │
│ 8f65     │        │ sh       │          │       │       │ of area  │ ckouts/ │
│          │        │          │          │       │       │ given a  │ readthe │
│          │        │          │          │       │       │ radius   │ docs.or │
│          │        │          │          │       │       │          │ g/user_ │
│          │        │          │          │       │       │          │ builds/ │
│          │        │          │          │       │       │          │ buildte │
│          │        │          │          │       │       │          │ st/chec │
│          │        │          │          │       │       │          │ kouts/s │
│          │        │          │          │       │       │          │ table/t │
│          │        │          │          │       │       │          │ utorial │
│          │        │          │          │       │       │          │ s/pytho │
│          │        │          │          │       │       │          │ n-shell │
│          │        │          │          │       │       │          │ .yml    │
└──────────┴────────┴──────────┴──────────┴───────┴───────┴──────────┴─────────┘
──────────────────────────────── Building Test ─────────────────────────────────
circle_area/54bb8f65: Creating Test Directory: /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/python-shell/circle_area/54bb8f65
──────────────────────────────── Running Tests ─────────────────────────────────
Spawning 1 processes for processing builders
───────────────────────────────── Iteration 1 ──────────────────────────────────
circle_area/54bb8f65 does not have any dependencies adding test to queue
Builders Eligible to Run
┏━━━━━━━━━━━━━━━━━━━━━━┓
┃ Builder              ┃
┡━━━━━━━━━━━━━━━━━━━━━━┩
│ circle_area/54bb8f65 │
└──────────────────────┘
circle_area/54bb8f65: Current Working Directory : /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/python-shell/circle_area/54bb8f65/stage
circle_area/54bb8f65: Running Test via command: bash circle_area_build.sh
circle_area/54bb8f65: Test completed in 0.032021 seconds with returncode: 0
circle_area/54bb8f65: Writing output file -  /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/python-shell/circle_area/54bb8f65/circle_area.out
circle_area/54bb8f65: Writing error file - /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/python-shell/circle_area/54bb8f65/circle_area.err
                                Test Summary                                 
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━┓
┃ builder              ┃ executor           ┃ status ┃ returncode ┃ runtime ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━┩
│ circle_area/54bb8f65 │ generic.local.bash │ PASS   │ 0          │ 0.032   │
└──────────────────────┴────────────────────┴────────┴────────────┴─────────┘



Passed Tests: 1/1 Percentage: 100.000%
Failed Tests: 0/1 Percentage: 0.000%


Adding 1 test results to /tmp/tmpjcb9dx2w/var/report.json
Writing Logfile to /tmp/tmpjcb9dx2w/var/logs/buildtest_59fp7hh0.log

Take note in the generated test script, we simply call a python script

$ cat $(buildtest path -t circle_area)
#!/bin/bash

python /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/python-shell/circle_area/54bb8f65/stage/circle_area.py

The python script will contain content specified in the run section. This script is located in the stage directory, we can retrieve the path to this file using the following expression $(buildtest path -s circle_area)/circle_area.py and then view the content via cat.

$ ls -l $(buildtest path -s circle_area)/circle_area.py
-rw-r--r-- 1 docs docs 119 Feb 14 17:19 /tmp/tmpjcb9dx2w/var/tests/generic.local.bash/python-shell/circle_area/54bb8f65/stage/circle_area.py
$ cat $(buildtest path -s circle_area)/circle_area.py
import math
radius = 2
area = math.pi * radius * radius
print("Circle Radius ", radius)
print("Area of circle ", area)