From d53969e53d1fdd1af31ed739e831ae2c20819685 Mon Sep 17 00:00:00 2001 From: Przemyslaw Stekiel Date: Wed, 25 Oct 2017 13:19:34 +0200 Subject: [PATCH 1/2] Add bare-metal test for uart. This test is for Vendors to check if uart (RawSerial) drivers are ready to use green-tea. Example usage: python test.py -t GCC_ARM -m NUCLEO_F070RB -b 9600 - run test using GCC_ARM toolchain, NUCLEO_F070RB board and test serial port with baud rate set to 9600 b/s. --- bare_metal_tests/.mbedignore | 1 + bare_metal_tests/uart/main.cpp | 156 +++++++++++++++++++ bare_metal_tests/uart/test.py | 276 +++++++++++++++++++++++++++++++++ 3 files changed, 433 insertions(+) create mode 100644 bare_metal_tests/.mbedignore create mode 100644 bare_metal_tests/uart/main.cpp create mode 100644 bare_metal_tests/uart/test.py diff --git a/bare_metal_tests/.mbedignore b/bare_metal_tests/.mbedignore new file mode 100644 index 00000000000..72e8ffc0db8 --- /dev/null +++ b/bare_metal_tests/.mbedignore @@ -0,0 +1 @@ +* diff --git a/bare_metal_tests/uart/main.cpp b/bare_metal_tests/uart/main.cpp new file mode 100644 index 00000000000..87c593d9663 --- /dev/null +++ b/bare_metal_tests/uart/main.cpp @@ -0,0 +1,156 @@ +/* mbed Microcontroller Library + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "drivers/RawSerial.h" + +using namespace mbed; + +/* UART TEST FOR GREEN TEA + * + * Greentea-client uses RawSerial class to handle UART communication. + * RawSerial class must provide valid implementation of the following + * methods (used by Greentea-client): + * - int RawSerial::getc() + * - int RawSerial::putc(int c). + * + * Greentea-client uses USB serial port for communication with the + * following transmission parameters: + * - 8 data bits, + * - 1 stop bit, + * - none parity bit, + * - configurable baudrate. + * + * Greentea-client operates in pooling mode only. + * Flow control is disabled. + * + * UART buffer size is limited to 120 bytes. + * + * TEST SCENARIO + * This part of the test is executed on mbed board. + * + * After startup it will try to communicate via UART with host part of the test. + * This part sends 4 messages with different length to the host and waits + * for the response (host should respond with the same message). + * Last message "PASSED" is send to inform that all responses from host were + * successfully received. + * + * To perform this test with serial port baud rate different than default (9600), + * call host test with appropriate --baudrate parameter. + * + */ + +/* Default baud rate if undefined. */ +#ifndef BAUD_RATE +#define BAUD_RATE 9600 +#endif + +#define NUMBER_OF_TEST_STRINGS 5 +#define NUMBER_OF_TEST_RETRIES 10 + +typedef enum +{ + PASSED, FAILED +} test_status; + +/* Array of string to be transmitted. */ +const char *test_strings[NUMBER_OF_TEST_STRINGS] = + { "x", + "mbed", "Green Tea UART bare metal test.", + "Long Message-Long Message-Long Message-Long Message-Long Message-Long Message-Long Message-Long Message-Long Message--.", + "PASSED" }; + +/* Function writes provided string to the serial port. */ +void write_line(RawSerial &serial, const char *str) +{ + while (true) { + serial.putc(*str); + if (*str == '\0') { + break; + } + + str++; + }; +} + +/* Function reads provided string from the serial port. */ +int read_line(RawSerial &serial, char *buffer) +{ + int count = 0; + while (true) { + *buffer = (char) serial.getc(); + count++; + if (*buffer == '\0') { + return count; + } + + buffer += 1; + } +} + +/* Function compares provided strings and returns 0 if are equal. */ +int strCmp(const char string1[], char string2[]) +{ + for (int i = 0;; i++) { + if (string1[i] != string2[i]) { + return string1[i] < string2[i] ? -1 : 1; + } + + if (string1[i] == '\0') { + return 0; + } + } +} + +/* Function writes provided message to the serial port and + * waits for the response from the Host. Host should + * respond with the same message. + * + * If valid response has been received, then function returns + * PASSED status, otherwise transmission is repeated. + * + * If max number of transmission attempts is reached, then + * function returns FAILED status. + * + * */ +test_status test_uart_communication(RawSerial &serial, const char * msg) +{ + char buffer[120]; + + for (int i = 0; i < NUMBER_OF_TEST_RETRIES; i++) { + write_line(serial, msg); + read_line(serial, buffer); + + if (strCmp(msg, buffer) == 0) { + return PASSED; + } + } + + return FAILED; +} + +int main() +{ + RawSerial serial(USBTX, USBRX, BAUD_RATE); + + /* Start the test. */ + for (unsigned int i = 0; i < (sizeof(test_strings) / sizeof(test_strings[0])); i++) { + if (test_uart_communication(serial, test_strings[i]) == FAILED) { + break; + } + } + + while (true); +} + diff --git a/bare_metal_tests/uart/test.py b/bare_metal_tests/uart/test.py new file mode 100644 index 00000000000..28d3bb05326 --- /dev/null +++ b/bare_metal_tests/uart/test.py @@ -0,0 +1,276 @@ +""" +mbed SDK +Copyright (c) 2017 ARM Limited + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +import time +import serial +import sys +import mbed_lstools +from subprocess import Popen, PIPE, STDOUT +import argparse +import json +import os +import logging +import atexit + +"""! UART TEST FOR GREEN TEA + + TEST SCENARIO + This part of the test is executed on the host (PC). + + Actions performed by the script: + - build mbed part of the UART test, + - request user to flash and reset the mbed board, + - perform uart communication test, + - report test status. + + Test scenario: + After startup script will try to communicate via UART with the mbed device. + This part of the test waits for the expected message and responds with + the same message. + 4 test messages should be received. Last message ("PASSED") informs + that on the mbed side also all messages have been received. + + To perform this test with serial port baud rate different than default (9600), + call host test with appropriate --baudrate parameter. + + Example usage: + test.py -m K64F -t GCC_ARM -b 9600 + This command will run test on K64F platfom using GCC_ARM toolchain and + serial connection speed equal to 9600 b/s. + + For more options type: + test.py -h +""" + +RETRY_LIMIT = 10 + +def serial_cleanup(serial_port): + """! Serial port cleanup handler. + This function closes opened serial port. + + @param serial_port serial port. + """ + if (serial_port != None and serial_port.isOpen() == True): + serial_port.close() + +def run_command(cmd): + """! Runs command and prints proc stdout on screen. + It is used to build the test. + + @param cmd command to be executed. + @return image path. + """ + image_path = str() + + # Remove build log file if exists + try: + os.remove('build.log') + except OSError: + pass + + try: + p = Popen(cmd, stdout=sys.stdout) + except OSError: + logging.exception ('Command can not be executed.') + sys.exit() + + # Check process status. + returncode = p.wait() + if returncode != 0: + logging.error('Error while executing command.') + sys.exit() + + # Get image path + try: + with open('build.log') as f: + build_data = json.load(f) + image_path = build_data['builds'][0]['bin'] + # Image path can not be found. + except (KeyError, IndexError, IOError, AttributeError): + # If we are here, then build process has failed. + logging.exception ('Unable to locate image file.') + sys.exit() + + return image_path + +def build_test(target, toolchain, clean_flag, baudrate): + """Build the mbed test. + + @param target target id. + @param toolchain compile toolchain. + @return image path. + """ + logging.info('Building mbed UART test for green tea.') + + clean = '' + if (clean_flag): + clean = '-c' + + cmd = 'mbed compile -t %s -m %s --source . --source ../.. ' \ + '--build-data build.log %s -D BAUD_RATE=%s' \ + % (toolchain, target, clean, baudrate) + + return run_command(cmd) + +def serial_write_line(serial_port, line): + """Write line to the serial port. + + @param line line to be written. + """ + serial_port.write(line) + logging.debug('[SEND] %s' % line) + +def serial_read_line(serial_port): + """Read line from the serial port with timeout. + + @return string read from the serial port. + """ + limit = 0 + line = str() + while (True): + if (serial_port.in_waiting == 0): + time.sleep(0.001) + limit += 1 + if (limit == RETRY_LIMIT): + break + else: + limit = 0 + c = serial_port.read() + line += c + if (c == '\0'): + break + logging.debug('[RECV] %s' % line) + return line + +def wait_for_message_and_respond(serial_port, msg): + """! This function waits for the message from the mbed device. + If valid message has been obtained, then it responds with + the same message. If invalid message has been received, then + it responds with 'RETRY\0'. + Note: First received message may be incomplete and is used for + synchronisation. + + @param msg expected message + """ + for x in range(0, RETRY_LIMIT): + line = serial_read_line(serial_port) + if (line == msg): + logging.info('Expected string has been received from the mbed device.') + serial_write_line(serial_port, line) + return True + else: + # if the line is unexpected (corrupted) and nul terminator and send, + # this will trigger mbed part to resend the test message. + if not line.endswith('\0'): + line += '\0' + serial_write_line(serial_port, line) + + return False + +def main(): + serial_port = None + sel_platform_id = None + muts_list = None + + # List of expected messages from the mbed device. + message_list = ['x\0', + 'mbed\0', + 'Green Tea UART bare metal test.\0', + 'Long Message-Long Message-Long Message-Long Message-Long Message-Long Message-Long Message-Long Message-Long Message--.\0', + 'PASSED\0'] + + # Prepare required/optional arguments. + parser = argparse.ArgumentParser(description='Execute UART bare metal test to check if platform is ready to use green tea.') + parser.add_argument("-m", "--target", help="Target id", required=True) + parser.add_argument("-t", "--toolchain", help="Compile toolchain. Example: ARM, GCC_ARM, IAR.", default='GCC_ARM') + parser.add_argument("-b", "--baudrate", help="Serial port baud rate.", default=9600) + parser.add_argument("-c", "--clean", action='store_true', help="Perform a clean build.") + parser.add_argument("-v", "--verbose", action='store_true', help="Increase output verbosity.", default=0) + args = parser.parse_args() + + # Set verbosity level (print transmission details or not) + if (args.verbose): + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + # Create list of connected mbed devices. + mbeds = mbed_lstools.create() + muts_list = mbeds.list_mbeds() + + # Print platform and baud rate info. + logging.info('Selected platform: %s.' % (args.target)) + logging.info('Selected serial port baud rate: %s b/s.' % (args.baudrate)) + + # Print list of devices with details. + logging.info('Available platforms: ') + for i, mut in enumerate(muts_list): + sel_str = '' + if (mut['platform_name'] == args.target): + sel_platform_id = i + sel_str = ' --- SELECTED ---' + logging.info('%-20s %-10s %-10s %-10s' % (mut['platform_name'], mut['serial_port'], mut['mount_point'], sel_str)) + + # If our device is not on the list of connected mbed devices, then quit. + if (sel_platform_id == None): + logging.info('Provided platform - %s has not been found.' % (args.target)) + sys.exit() + + # Build the test. + build_test(args.target, args.toolchain, args.clean, args.baudrate) + + # Inform what to do next. + logging.info('Perform the following steps: \n' + '1. Copy the binary file to the board.\n' + '2. Press the reset button on the mbed board to start the program.\n' + '3. Press Enter to start the test...\n') + + # Wait for signal to continue. + raw_input('') + + # Indicate start of the communication test. + logging.info('Starting test, please wait...') + + # Open serial port. + try: + serial_port = serial.Serial(port=muts_list[sel_platform_id]['serial_port'], + baudrate=args.baudrate, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS) + + # Register cleanup exit handler + atexit.register(serial_cleanup, serial_port) + + except (ValueError, serial.SerialException): + logging.exception ('Could not open serial port.') + sys.exit() + + # Perform serial comunication test with the mbed device. + for msg in message_list: + if not wait_for_message_and_respond(serial_port, msg): + logging.error("Expected message has NOT been received by the host.") + sys.exit() + + # If we are here, then all expected messages have been received from the mbed device. + # Assume that last message - "PASSED" only inform that on mbed device side also + # all responses from host were successfully received. + logging.info('Test passed!') + sys.exit() + +if __name__ =='__main__': + main() + \ No newline at end of file From 0c43ed0b2e3dc828badf3a729e15f5ed2d3de0e1 Mon Sep 17 00:00:00 2001 From: Przemyslaw Stekiel Date: Thu, 23 Nov 2017 13:03:04 +0100 Subject: [PATCH 2/2] Add uart bare metal test to Travis. --- tools/build_travis.py | 56 ++++++++++++++++++++----------------------- tools/paths.py | 1 + tools/tests.py | 7 ++++++ 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/tools/build_travis.py b/tools/build_travis.py index 325a68d7283..4fc6bf86c69 100644 --- a/tools/build_travis.py +++ b/tools/build_travis.py @@ -2,16 +2,12 @@ """ Travis-CI build script - mbed SDK Copyright (c) 2011-2013 ARM Limited - Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -187,25 +183,25 @@ "NXP": ( {"target": "LPC1768", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_15", "MBED_16", "MBED_17"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_15", "MBED_16", "MBED_17", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "K64F", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "K22F", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "KL43Z", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, @@ -216,99 +212,99 @@ "STM": ( {"target": "NUCLEO_F446RE", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUCLEO_F446ZE", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUCLEO_F401RE", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUCLEO_F411RE", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUCLEO_F412ZG", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], } }, {"target": "NUCLEO_F429ZI", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUCLEO_F207ZG", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUCLEO_F746ZG", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUCLEO_F767ZI", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUCLEO_L476RG", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], } }, {"target": "DISCO_F429ZI", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], } }, {"target": "DISCO_F407VG", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "DISCO_F413ZH", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], } }, {"target": "NUCLEO_F303ZE", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "DISCO_L475VG_IOT01A", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "DISCO_L476VG", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "DISCO_L072CZ_LRWAN1", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], } }, ) @@ -317,19 +313,19 @@ "NUVOTON": ( {"target": "NUMAKER_PFM_NUC472", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUMAKER_PFM_M453", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } }, {"target": "NUMAKER_PFM_M487", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], "usb" : ["USB_1", "USB_2" ,"USB_3"], } } @@ -341,7 +337,7 @@ { "target": "RZ_A1H", "toolchains": "GCC_ARM", - "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16"], + "tests": {"" : ["MBED_2", "MBED_10", "MBED_11", "MBED_16", "BARE_METAL_UART"], } }, ) @@ -411,4 +407,4 @@ def run_test_testsuite(dry_run, vendor): run_builds("-s" in sys.argv, options.vendor) run_test_linking("-s" in sys.argv, options.vendor) - run_test_testsuite("-s" in sys.argv, options.vendor) + run_test_testsuite("-s" in sys.argv, options.vendor) \ No newline at end of file diff --git a/tools/paths.py b/tools/paths.py index c8e745b00b6..017a3d68516 100644 --- a/tools/paths.py +++ b/tools/paths.py @@ -48,6 +48,7 @@ # Tests TEST_DIR = join(LIB_DIR, "tests") +BARE_METAL_TEST_DIR = join(ROOT, "bare_metal_tests") HOST_TESTS = join(ROOT, "tools", "host_tests") # mbed RPC diff --git a/tools/tests.py b/tools/tests.py index 4b31665d34a..2e433f85e22 100644 --- a/tools/tests.py +++ b/tools/tests.py @@ -830,6 +830,13 @@ #"host_test" : "detect_auto", }, + # Bare metal tests + { + "id": "BARE_METAL_UART", "description": "Bare metal uart communication test for Green Tea", + "source_dir": join(BARE_METAL_TEST_DIR, "uart"), + "dependencies": [MBED_LIBRARIES], + "automated": True, + }, ] # Group tests with the same goals into categories