diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 6d0186f..dba56bb 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -32,3 +32,4 @@ jobs: - name: ShellCheck run: | ./shellcheck -- x86-64-level + ./shellcheck -- tests/x86-64-level.sh diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..f92dfe4 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,24 @@ +on: [push, pull_request] + +name: unit_tests + +jobs: + checks: + if: "! contains(github.event.head_commit.message, '[ci skip]')" + + timeout-minutes: 2 + + runs-on: ubuntu-22.04 + + name: unit_tests + + strategy: + fail-fast: false + + steps: + - name: Checkout git repository + uses: actions/checkout@v3 + + - name: Run unit tests + run: | + PATH=.:$PATH tests/x86-64-level.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3b66fd1 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +all: shellcheck check + +shellcheck: + shellcheck x86-64-level + shellcheck tests/x86-64-level.sh + +check: + @PATH=.:${PATH} tests/x86-64-level.sh diff --git a/NEWS.md b/NEWS.md index 08c786a..b7e94e9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,22 @@ +# Version 0.2.2 [2023-05-25] + +## New Features + + * Now `x86-64-level` asserts that the input CPU flags are of the + correct format, which is assumed to be only lower-case letters, + digits, and underscores. + +## Bug Fixes + + * Calling `x86-64-level --assert=""` would produce error message + `merror: command not found` and not the intended `ERROR: Option + '--assert' must not be empty`. + +## Miscellaneous + + * Add unit tests. + + # Version 0.2.1 [2023-01-18] * Now `--assert` reports also on the CPU name. diff --git a/README.md b/README.md index 54ed2b4..235c8f6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ [![shellcheck](https://github.com/HenrikBengtsson/x86-64-level/actions/workflows/shellcheck.yml/badge.svg)](https://github.com/HenrikBengtsson/x86-64-level/actions/workflows/shellcheck.yml) +[![unit_tests](https://github.com/HenrikBengtsson/x86-64-level/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/HenrikBengtsson/x86-64-level/actions/workflows/unit_tests.yml) # x86-64-level - Get the x86-64 Microarchitecture Level on the Current Machine -TL;DR: The `x86-64-level` tool identifies the current CPU supports +TL;DR: The `x86-64-level` tool identifies if the current CPU supports x86-64-v1, x86-64-v2, x86-64-v3, or x86-64-v4, e.g. ```sh @@ -14,7 +15,7 @@ $ x86-64-level # Background **x86-64** is a 64-bit version of the x86 CPU instruction set -supported by AMD and Intel CPUs among others. Since the first +supported by AMD and Intel CPUs, among others. Since the first generations of CPUs, more low-level CPU features have been added over the years. The x86-64 CPU features can be grouped into four [CPU microarchitecture levels]: @@ -28,8 +29,9 @@ microarchitecture levels]: The x86-64-v1 level is the same as the original, baseline x86-64 level. These levels are subsets of each other, i.e. x86-64-v1 ⊂ -x86-64-v2 ⊂ x86-64-v3 ⊂ x86-64-v4. - +x86-64-v2 ⊂ x86-64-v3 ⊂ x86-64-v4. For a CPU to support a level, it +must support _all_ CPU features of that version level, and, because +they are subsets of each other, all those of the lower versions. Software can be written so that they use the most powerful set of CPU features available. This optimization happens at compile time and @@ -44,12 +46,18 @@ might get something like: address 0x2b3a8b234ccd, cause 'illegal operand' ``` +or + +``` +Illegal instruction (core dumped) +``` + This is because the older CPU does not understand one of the CPU -instructions ("operands"). Note that the software might not crash each -time. It will only do so if it reach the part of the code that uses -the never CPU instructions. +instructions ("operands"). Note that the software might not crash +each time. It will only do so if it reaches the part of the code that +uses a CPU instruction that is not recognized by the current CPU. -In contrast, if we compile the software on the older x86-64-v3 +In contrast, if we compile the software towards the older x86-64-v3 machine, the produced binary will only use x86-64-v3 instructions and will therefor also run on the newer x86-64-v4 machine. @@ -65,8 +73,8 @@ illegal operation' problem. ## Finding CPU's x86-64 level -This tool, `x86-64-level`, allows you to query the which x86-64 level -the CPU on current machine supports. For example, +This tool, `x86-64-level`, allows you to query which x86-64 level the +CPU on current machine supports. For example, ```sh $ x86-64-level @@ -81,12 +89,13 @@ $ echo "x86-64-v${level}" x86-64-v3 ``` -If you want to know an "explanation", specify option `--verbose`, e.g. +If you want to get an explanation for the identified level, specify +option `--verbose`, e.g. ```sh $ x86-64-level --verbose Identified x86-64-v3, because x86-64-v4 requires 'avx512f', which -this CPU [Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz] does not support +is not supported by this CPU [Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz] 3 ``` @@ -123,7 +132,7 @@ x86-64-level --assert=4 || exit 1 This will output that error message (to the standard error) and exit the script with exit code 1, if, and only if, the current machine does -not support x86-64-v4. In all other cases, it continue silently. +not support x86-64-v4. In all other cases, it continues silently. diff --git a/tests/x86-64-level.sh b/tests/x86-64-level.sh new file mode 100755 index 0000000..48d2659 --- /dev/null +++ b/tests/x86-64-level.sh @@ -0,0 +1,364 @@ +#! /usr/bin/env bash + +nerrors=0 + +echo "x86-64-level ..." + +#-------------------------------------------------------------------------- +# x86-64-level --version +#-------------------------------------------------------------------------- +echo "* x86-64-level --version" +version=$(x86-64-level --version) +exit_code=$? +if [[ ${exit_code} -ne 0 ]]; then + >&2 echo "ERROR: Exit code is non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) +fi +if ! grep -q -E "^[[:digit:]]+([.-][[:digit:]]+)+$" <<< "${version}"; then + >&2 echo "ERROR: Unexpected version string: ${version}" + nerrors=$((nerrors + 1)) +fi + +## Outputs nothing to stderr +stderr=$( { >&2 x86-64-level --version > /dev/null; } 2>&1 ) +if [[ -n ${stderr} ]]; then + >&2 echo "ERROR: Detected output to standard error: ${stderr}" + nerrors=$((nerrors + 1)) +fi + + +#-------------------------------------------------------------------------- +# x86-64-level --help +#-------------------------------------------------------------------------- +echo "* x86-64-level --help" +help=$(x86-64-level --help) +exit_code=$? +if [[ ${exit_code} -ne 0 ]]; then + >&2 echo "ERROR: Exit code is non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) +fi +if ! grep -q -E "^Version:" <<< "${help}"; then + >&2 echo "ERROR: Help does not show version: ${help}" + nerrors=$((nerrors + 1)) +fi +if ! grep -q -E "^License:" <<< "${help}"; then + >&2 echo "ERROR: Help does not show license: ${help}" + nerrors=$((nerrors + 1)) +fi +if ! grep -q -E "^Source code:" <<< "${help}"; then + >&2 echo "ERROR: Help does not link to source code: ${help}" + nerrors=$((nerrors + 1)) +fi +if ! grep -q -E "^Authors:" <<< "${help}"; then + >&2 echo "ERROR: Help does not list authors: ${help}" + nerrors=$((nerrors + 1)) +fi +if ! grep -q -E "^Examples:" <<< "${help}"; then + >&2 echo "ERROR: Help does not show examples: ${help}" + nerrors=$((nerrors + 1)) +fi +if ! grep -q -E "^Usage:" <<< "${help}"; then + >&2 echo "ERROR: Help does not show usage: ${help}" + nerrors=$((nerrors + 1)) +fi +if ! grep -q -E "^Options:" <<< "${help}"; then + >&2 echo "ERROR: Help does not show options: ${help}" + nerrors=$((nerrors + 1)) +fi + +## Outputs nothing to stderr +stderr=$( { >&2 x86-64-level --help > /dev/null; } 2>&1 ) +if [[ -n ${stderr} ]]; then + >&2 echo "ERROR: Detected output to standard error: ${stderr}" + nerrors=$((nerrors + 1)) +fi + + +#-------------------------------------------------------------------------- +# x86-64-level +#-------------------------------------------------------------------------- +echo "* x86-64-level" +level=$(x86-64-level) +exit_code=$? +if [[ ${exit_code} -ne 0 ]]; then + >&2 echo "ERROR: Exit code is non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) +fi +if ! grep -q -E "^[[:digit:]]+$" <<< "${level}"; then + >&2 echo "ERROR: Non-integer x86-64 level: ${level}" + nerrors=$((nerrors + 1)) +fi +if [[ ${level} -lt 1 ]] || [[ ${level} -gt 4 ]]; then + >&2 echo "ERROR: x86-64 level out of range [1,4]: ${level}" + nerrors=$((nerrors + 1)) +fi + +## Outputs nothing to stderr +stderr=$( { >&2 x86-64-level > /dev/null; } 2>&1 ) +if [[ -n ${stderr} ]]; then + >&2 echo "ERROR: Detected output to standard error: ${stderr}" + nerrors=$((nerrors + 1)) +fi + + +#-------------------------------------------------------------------------- +# x86-64-level --assert= +#-------------------------------------------------------------------------- +echo "* x86-64-level --assert=" +level=$(x86-64-level) + +stderr=$(x86-64-level --assert="${level}" 2>&1) +exit_code=$? +if [[ ${exit_code} -ne 0 ]]; then + >&2 echo "ERROR: Exit code is non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) +fi + +## Outputs nothing to stdout +stdout=$( x86-64-level --assert="${level}"> /dev/null ) +if [[ -n ${stdout} ]]; then + >&2 echo "ERROR: Detected output to standard output: ${stdout}" + nerrors=$((nerrors + 1)) +fi + + +#-------------------------------------------------------------------------- +# x86-64-level --assert=1 +#-------------------------------------------------------------------------- +echo "* x86-64-level --assert=1" +stderr=$(x86-64-level --assert=1 2>&1) +exit_code=$? +if [[ ${exit_code} -ne 0 ]]; then + >&2 echo "ERROR: Exit code is non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) +fi + +## Outputs nothing to stdout +stdout=$( x86-64-level --assert="${level}"> /dev/null ) +if [[ -n ${stdout} ]]; then + >&2 echo "ERROR: Detected output to standard output: ${stdout}" + nerrors=$((nerrors + 1)) +fi + + + +#-------------------------------------------------------------------------- +# x86-64-level - <<< "flags: avx" +#-------------------------------------------------------------------------- +required_flags=( + "lm cmov cx8 fpu fxsr mmx syscall sse2" + "cx16 lahf_lm popcnt sse4_1 sse4_2 ssse3" + "avx avx2 bmi1 bmi2 f16c fma abm movbe xsave" + "avx512f avx512bw avx512cd avx512dq avx512vl" +) + +cpu_flags=("dummy") +for value in "${required_flags[@]}"; do + cpu_flags+=("${cpu_flags[-1]} ${value}") +done + +for truth in $(seq 0 "$((${#cpu_flags[@]} - 1))"); do + flags=${cpu_flags[${truth}]} + + echo "* x86-64-level - <<< 'flags: ${flags}'" + level=$(x86-64-level - <<< "flags: ${flags}") + exit_code=$? + if [[ ${exit_code} -ne 0 ]]; then + >&2 echo "ERROR: Exit code is non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) + fi + + if [[ "${level}" -ne "${truth}" ]]; then + >&2 echo "ERROR: Unexpected level: ${level} != ${truth}" + nerrors=$((nerrors + 1)) + fi + + ## Outputs nothing to stderr + stderr=$( { >&2 x86-64-level <<< "flags: ${flags}" > /dev/null; } 2>&1 ) + if [[ -n ${stderr} ]]; then + >&2 echo "ERROR: Detected output to standard error: ${stderr}" + nerrors=$((nerrors + 1)) + fi +done + + + + +#-------------------------------------------------------------------------- +# Exceptions +#-------------------------------------------------------------------------- +# x86-64-level --assert= +for level in -1 0 5 100; do + echo "* x86-64-level --assert=${level} (exception)" + stderr=$(x86-64-level --assert="${level}" 2>&1) + exit_code=$? + if [[ ${exit_code} -eq 0 ]]; then + >&2 echo "ERROR: Exit code should be non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) + fi + + if [[ -z ${stderr} ]]; then + >&2 echo "ERROR: No error message: '${stderr}'" + nerrors=$((nerrors + 1)) + fi + + if ! head -n 1 <<< "${stderr}" | grep -q -E "^ERROR:"; then + >&2 echo "ERROR: Standard error output does not begin with 'ERROR:': '${stderr}'" + nerrors=$((nerrors + 1)) + fi + + if ! grep -q -E "^ERROR: .*out of range.* ${level}" <<< "${stderr}"; then + >&2 echo "ERROR: Unexpected error message: '${stderr}'" + nerrors=$((nerrors + 1)) + fi + + ## Outputs nothing to stdout + stdout=$(x86-64-level --assert="${level}" 2> /dev/null) + if [[ -n ${stdout} ]]; then + >&2 echo "ERROR: Detected output to standard output: ${stdout}" + nerrors=$((nerrors + 1)) + fi +done + + +# x86-64-level --assert= +for level in 1.2 world; do + echo "* x86-64-level --assert=${level} (exception)" + stderr=$(x86-64-level --assert="${level}" 2>&1) + exit_code=$? + if [[ ${exit_code} -eq 0 ]]; then + >&2 echo "ERROR: Exit code should be non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) + fi + + if [[ -z ${stderr} ]]; then + >&2 echo "ERROR: No error message: '${stderr}'" + nerrors=$((nerrors + 1)) + fi + + if ! head -n 1 <<< "${stderr}" | grep -q -E "^ERROR:"; then + >&2 echo "ERROR: Standard error output does not begin with 'ERROR:': '${stderr}'" + nerrors=$((nerrors + 1)) + fi + + if ! grep -q -E "^ERROR: .*does not specify an integer.* ${level}" <<< "${stderr}"; then + >&2 echo "ERROR: Unexpected error message: '${stderr}'" + nerrors=$((nerrors + 1)) + fi + + ## Outputs nothing to stdout + stdout=$(x86-64-level --assert="${level}" 2> /dev/null) + if [[ -n ${stdout} ]]; then + >&2 echo "ERROR: Detected output to standard output: ${stdout}" + nerrors=$((nerrors + 1)) + fi +done + + + +# x86-64-level --assert='' +echo "* x86-64-level --assert='' (exception)" +stderr=$(x86-64-level --assert="" 2>&1) +exit_code=$? +if [[ ${exit_code} -eq 0 ]]; then + >&2 echo "ERROR: Exit code should be non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) +fi + +if [[ -z ${stderr} ]]; then + >&2 echo "ERROR: No error message: '${stderr}'" + nerrors=$((nerrors + 1)) +fi + +if ! head -n 1 <<< "${stderr}" | grep -q -E "^ERROR:"; then + >&2 echo "ERROR: Standard error output does not begin with 'ERROR:': '${stderr}'" + nerrors=$((nerrors + 1)) +fi + +if ! grep -q -E "^ERROR: .*must not be empty" <<< "${stderr}"; then + >&2 echo "ERROR: Unexpected error message: '${stderr}'" + nerrors=$((nerrors + 1)) +fi + +## Outputs nothing to stdout +stdout=$(x86-64-level --assert="" 2> /dev/null) +if [[ -n ${stdout} ]]; then + >&2 echo "ERROR: Detected output to standard output: ${stdout}" + nerrors=$((nerrors + 1)) +fi + + +echo "* x86-64-level - <<< '' (exception)" +stderr=$(x86-64-level - <<< '' 2>&1) +exit_code=$? +if [[ ${exit_code} -eq 0 ]]; then + >&2 echo "ERROR: Exit code is not non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) +fi + +if [[ -z ${stderr} ]]; then + >&2 echo "ERROR: No error message: '${stderr}'" + nerrors=$((nerrors + 1)) +fi + +if ! head -n 1 <<< "${stderr}" | grep -q -E "^ERROR:"; then + >&2 echo "ERROR: Standard error output does not begin with 'ERROR:': '${stderr}'" + nerrors=$((nerrors + 1)) +fi + +if ! grep -q -E "^ERROR: .*Input data is empty" <<< "${stderr}"; then + >&2 echo "ERROR: Unexpected error message: '${stderr}'" + nerrors=$((nerrors + 1)) +fi + +## Outputs nothing to stdout +stdout=$( x86-64-level - <<< '' 2> /dev/null ) +if [[ -n ${stdout} ]]; then + >&2 echo "ERROR: Detected output to standard output: ${stdout}" + nerrors=$((nerrors + 1)) +fi + + +echo "* x86-64-level - <<< 'flags: AVX' (exception)" +stderr=$(x86-64-level - <<< 'flags: AVX' 2>&1) +exit_code=$? +if [[ ${exit_code} -eq 0 ]]; then + >&2 echo "ERROR: Exit code is not non-zero: ${exit_code}" + nerrors=$((nerrors + 1)) +fi + +if [[ -z ${stderr} ]]; then + >&2 echo "ERROR: No error message: '${stderr}'" + nerrors=$((nerrors + 1)) +fi + +if ! head -n 1 <<< "${stderr}" | grep -q -E "^ERROR:"; then + >&2 echo "ERROR: Standard error output does not begin with 'ERROR:': '${stderr}'" + nerrors=$((nerrors + 1)) +fi + +if ! grep -q -E "^ERROR: .*format of the CPU flags" <<< "${stderr}"; then + >&2 echo "ERROR: Unexpected error message: '${stderr}'" + nerrors=$((nerrors + 1)) +fi + +## Outputs nothing to stdout +stdout=$( x86-64-level - <<< 'flags: AVX' 2> /dev/null ) +if [[ -n ${stdout} ]]; then + >&2 echo "ERROR: Detected output to standard output: ${stdout}" + nerrors=$((nerrors + 1)) +fi + + + +#-------------------------------------------------------------------------- +# Summary +#-------------------------------------------------------------------------- +if [[ ${nerrors} -eq 0 ]]; then + echo "x86-64-level ... DONE" +else + echo "Number of ERRORS: ${nerrors}" + echo "x86-64-level ... ERROR" + exit 1 +fi + diff --git a/x86-64-level b/x86-64-level index cc6d7e3..25343cc 100755 --- a/x86-64-level +++ b/x86-64-level @@ -6,7 +6,7 @@ #' i.e. x86-64-v1, x86-64-v2, x86-64-v3, or x86-64-v4. #' #' Usage: -#' x86-64-version +#' x86-64-level #' #' Options: #' --help Show this help @@ -41,9 +41,9 @@ #' $ echo $? #' 1 #' -#' Version: 0.2.1 +#' Version: 0.2.2 #' License: CC BY-SA 4.0 -#' Source: https://github.com/ucsf-wynton/wynton-tools +#' Source code: https://github.com/ucsf-wynton/wynton-tools #' #' Authors: #' * Henrik Bengtsson (expanded on Gilles implementation [2]) @@ -73,21 +73,23 @@ version() { #--------------------------------------------------------------------- data= read_input() { - if $stdin; then - data=$(< /dev/stdin) - else - data=$(< /proc/cpuinfo) - fi if [[ -z ${data} ]]; then - echo >&2 "ERROR: Input data is empty" - exit 1 + if ${stdin}; then + data=$(< /dev/stdin) + else + data=$(< /proc/cpuinfo) + fi + if [[ -z ${data} ]]; then + echo >&2 "ERROR: Input data is empty" + exit 1 + fi fi } get_cpu_name() { local name bfr=$(grep -E "^model name[[:space:]]*:" <<< "${data}") - name=$(echo "$bfr" | head -1) + name=$(echo "${bfr}" | head -n 1) name="${name#model name*:}" echo "${name## }" } @@ -97,7 +99,18 @@ get_cpu_flags() { local flags flags=$(grep "^flags[[:space:]]*:" <<< "${data}" | head -n 1) flags="${flags#*:}" - echo "${flags## }" + flags="${flags## }" + if grep -v -q -E "^[[:lower:][:digit:]_ ]+$" <<< "${flags}"; then + echo >&2 "ERROR: Cannot reliably infer the CPU x86-64 level, because the format of the CPU flags comprise of other symbols than only lower-case letters, digits, and underscores: '${flags}'" + exit 1 + fi + echo "${flags}" +} + + +validate_cpu_flags() { + read_input + get_cpu_flags > /dev/null } @@ -109,12 +122,12 @@ has_cpu_flags() { for flag; do ## Note, it's important to keep a trailing space case " ${flags} " in - *" $flag "*) + *" ${flag} "*) : ;; *) - if "$verbose"; then - msg="Identified x86-64-v${level}, because x86-64-v$((level + 1)) requires '$flag', which is not supported by this CPU" + if ${verbose}; then + msg="Identified x86-64-v${level}, because x86-64-v$((level + 1)) requires '${flag}', which is not supported by this CPU" cpu_name=$(get_cpu_name) [[ -n ${cpu_name} ]] && msg="${msg} [${cpu_name}]" echo >&2 "${msg}" @@ -158,7 +171,7 @@ report_cpu_version() { exit 1 fi determine_cpu_version - echo "$level" + echo "${level}" } @@ -189,16 +202,17 @@ while [[ $# -gt 0 ]]; do key=${1//--} key=${key//=*} value=${1//--[[:alpha:]]*=} - if [[ -z $value ]]; then - merror "Option '--$key' must not be empty" + if [[ -z ${value} ]]; then + echo >&2 "ERROR: Option '--${key}' must not be empty" + exit 2 fi - if [[ "$key" == "assert" ]]; then - assert=$value - if [[ ! $assert =~ ^-?[0-9]+$ ]]; then - echo >&2 "ERROR: Option --assert does not specify an integer: $assert" + if [[ "${key}" == "assert" ]]; then + assert=${value} + if [[ ! ${assert} =~ ^-?[0-9]+$ ]]; then + echo >&2 "ERROR: Option --assert does not specify an integer: ${assert}" exit 2 - elif [[ $assert -lt 1 ]] || [[ $assert -gt 4 ]]; then - echo >&2 "ERROR: Option --assert is out of range [1,4]: $assert" + elif [[ ${assert} -lt 1 ]] || [[ ${assert} -gt 4 ]]; then + echo >&2 "ERROR: Option --assert is out of range [1,4]: ${assert}" exit 2 fi else @@ -212,9 +226,10 @@ while [[ $# -gt 0 ]]; do shift done -if [[ -n $assert ]]; then +if [[ -n ${assert} ]]; then + validate_cpu_flags version=$(report_cpu_version) - if [[ $version < $assert ]]; then + if [[ ${version} -lt ${assert} ]]; then read_input cpu_info=$(get_cpu_name) [[ -n ${cpu_info} ]] && cpu_info=" [${cpu_info}]" @@ -222,5 +237,6 @@ if [[ -n $assert ]]; then exit 1 fi else + validate_cpu_flags report_cpu_version fi