From: Bartosz Golaszewski Date: Thu, 9 Feb 2017 10:02:13 +0000 (+0100) Subject: tests: add a simple unit testing framework and a couple tests X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=8ff90454fc111a914802f7c4f2c4e1355ee0c16c;p=qemu-gpiodev%2Flibgpiod.git tests: add a simple unit testing framework and a couple tests Implement a simple unit testing framework working together with the gpio-mockup module present in the mainline linux kernel. Signed-off-by: Bartosz Golaszewski --- diff --git a/.gitignore b/.gitignore index 67e5cad..043fe9b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ configure libtool m4/ stamp-h1 + +# unit tests +gpiod-unit diff --git a/Makefile.am b/Makefile.am index 56c8b2b..159eb3c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -9,6 +9,12 @@ AUTOMAKE_OPTIONS = foreign SUBDIRS = include src +if WITH_TESTS + +SUBDIRS += tests + +endif + if HAS_DOXYGEN doc: diff --git a/configure.ac b/configure.ac index 2226513..4572987 100644 --- a/configure.ac +++ b/configure.ac @@ -82,6 +82,34 @@ then AC_CHECK_HEADERS([sys/signalfd.h], [], [HEADER_NOT_FOUND_TOOLS([sys/signalfd.h])]) fi +AC_ARG_ENABLE([tests], + [AC_HELP_STRING([--enable-tests], + [enable libgpiod tests [default=no]])], + [ + if test "x$enableval" = xyes + then + with_tests=true + else + with_tests=false + fi + ], + [with_tests=false]) +AM_CONDITIONAL([WITH_TESTS], [test "x$with_tests" = xtrue]) + +AC_DEFUN([HEADER_NOT_FOUND_TESTS], + [ERR_NOT_FOUND([$1 header], [tests])]) + +AC_DEFUN([FUNC_NOT_FOUND_TESTS], + [ERR_NOT_FOUND([$1()], [tests])]) + +if test "x$with_tools" = xtrue +then + AC_CHECK_LIB([kmod], [kmod_module_probe_insert_module], [], + [AC_MSG_ERROR([libkmod not found (needed to build tests])]) + AC_CHECK_HEADERS([libkmod.h], [], [HEADER_NOT_FOUND_TESTS([libkmod.h])]) + AC_CHECK_FUNC([qsort], [], [FUNC_NOT_FOUND_TESTS([qsort])]) +fi + AC_CHECK_PROG([has_doxygen], [doxygen], [true], [false]) AM_CONDITIONAL([HAS_DOXYGEN], [test "x$has_doxygen" = xtrue]) if test "x$has_doxygen" = xfalse @@ -93,6 +121,8 @@ AC_CONFIG_FILES([Makefile include/Makefile src/Makefile src/lib/Makefile - src/tools/Makefile]) + src/tools/Makefile + tests/Makefile + tests/unit/Makefile]) AC_OUTPUT diff --git a/tests/Makefile.am b/tests/Makefile.am new file mode 100644 index 0000000..fe02e4a --- /dev/null +++ b/tests/Makefile.am @@ -0,0 +1,9 @@ +# +# Copyright (C) 2017 Bartosz Golaszewski +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License +# as published by the Free Software Foundation. +# + +SUBDIRS = . unit diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am new file mode 100644 index 0000000..b92e101 --- /dev/null +++ b/tests/unit/Makefile.am @@ -0,0 +1,16 @@ +# +# Copyright (C) 2017 Bartosz Golaszewski +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2.1 of the GNU Lesser General Public License +# as published by the Free Software Foundation. +# + +AM_CFLAGS = -I$(top_srcdir)/include/ -include $(top_srcdir)/config.h +AM_CFLAGS += -Wall -Wextra -g +LDADD = -lgpiod -L$(top_srcdir)/src/lib -lkmod +DEPENDENCIES = libgpiod.la + +check_PROGRAMS = gpiod-unit + +gpiod_unit_SOURCES = gpiod-unit.c tests-chip.c diff --git a/tests/unit/gpiod-unit.c b/tests/unit/gpiod-unit.c new file mode 100644 index 0000000..a5b77bb --- /dev/null +++ b/tests/unit/gpiod-unit.c @@ -0,0 +1,404 @@ +/* + * Unit testing framework for libgpiod. + * + * Copyright (C) 2017 Bartosz Golaszewski + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2.1 of the GNU Lesser General Public License + * as published by the Free Software Foundation. + */ + +#include "gpiod-unit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NORETURN __attribute__((noreturn)) + +struct mockup_chip { + char *path; + char *name; + unsigned int number; +}; + +struct test_context { + struct mockup_chip *chips; + size_t num_chips; + bool test_failed; + struct timeval mod_loaded_ts; +}; + +static struct { + struct gu_test *test_list_head; + struct gu_test *test_list_tail; + unsigned int num_tests; + unsigned int tests_failed; + struct kmod_ctx *module_ctx; + struct kmod_module *module; + struct test_context test_ctx; +} globals; + +static void vmsg(FILE *stream, const char *hdr, const char *fmt, va_list va) +{ + fprintf(stream, "[%.5s] ", hdr); + vfprintf(stream, fmt, va); + fputc('\n', stream); +} + +void gu_msg(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + vmsg(stderr, " INFO", fmt, va); + va_end(va); +} + +void gu_err(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + vmsg(stderr, "ERROR", fmt, va); + va_end(va); +} + +static void GU_PRINTF(1, 2) NORETURN die(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + vmsg(stderr, "FATAL", fmt, va); + va_end(va); + + exit(EXIT_FAILURE); +} + +static void GU_PRINTF(1, 2) NORETURN die_perr(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + fprintf(stderr, "[FATAL] "); + vfprintf(stderr, fmt, va); + fprintf(stderr, ": %s\n", strerror(errno)); + va_end(va); + + exit(EXIT_FAILURE); +} + +static void check_chip_index(unsigned int index) +{ + if (index >= globals.test_ctx.num_chips) + die("invalid chip number requested from test code"); +} + +const char * gu_chip_path(unsigned int index) +{ + check_chip_index(index); + + return globals.test_ctx.chips[index].path; +} + +const char * gu_chip_name(unsigned int index) +{ + check_chip_index(index); + + return globals.test_ctx.chips[index].name; +} + +unsigned int gu_chip_num(unsigned int index) +{ + check_chip_index(index); + + return globals.test_ctx.chips[index].number; +} + +void _gu_register_test(struct gu_test *test) +{ + struct gu_test *tmp; + + if (!globals.test_list_head) { + globals.test_list_head = globals.test_list_tail = test; + test->_next = NULL; + } else { + tmp = globals.test_list_tail; + globals.test_list_tail = test; + test->_next = NULL; + tmp->_next = test; + } + + globals.num_tests++; +} + +void _gu_set_test_failed(void) +{ + globals.test_ctx.test_failed = true; +} + +static bool mockup_loaded(void) +{ + int state; + + if (!globals.module_ctx || !globals.module) + return false; + + state = kmod_module_get_initstate(globals.module); + + return state == KMOD_MODULE_LIVE; +} + +static void module_cleanup(void) +{ + gu_msg("cleaning up"); + + if (mockup_loaded()) + kmod_module_remove_module(globals.module, 0); + + if (globals.module) + kmod_module_unref(globals.module); + + if (globals.module_ctx) + kmod_unref(globals.module_ctx); +} + +static void check_gpio_mockup(void) +{ + const char *modpath; + int status; + + gu_msg("checking gpio-mockup availability"); + + globals.module_ctx = kmod_new(NULL, NULL); + if (!globals.module_ctx) + die_perr("error creating kernel module context"); + + status = kmod_module_new_from_name(globals.module_ctx, + "gpio-mockup", &globals.module); + if (status) + die_perr("error allocating module info"); + + /* First see if we can find the module. */ + modpath = kmod_module_get_path(globals.module); + if (!modpath) + die("the gpio-mockup module does not exist in the system or is built into the kernel"); + + /* Then see if we can freely load and unload it. */ + status = kmod_module_probe_insert_module(globals.module, 0, + NULL, NULL, NULL, NULL); + if (status) + die_perr("unable to load gpio-mockup"); + + status = kmod_module_remove_module(globals.module, 0); + if (status) + die_perr("unable to remove gpio-mockup"); + + gu_msg("gpio-mockup ok"); +} + +static void test_load_module(struct gu_chip_descr *descr) +{ + char *modarg, *tmp_modarg; + char **line_sizes; + size_t modarg_len; + unsigned int i; + int status; + + line_sizes = malloc(sizeof(char *) * descr->num_chips); + if (!line_sizes) + die("out of memory"); + + modarg_len = strlen("gpio_mockup_ranges="); + for (i = 0; i < descr->num_chips; i++) { + status = asprintf(&line_sizes[i], + "-1,%u,", descr->num_lines[i]); + if (status < 0) + die_perr("asprintf"); + + modarg_len += status; + } + + modarg = malloc(modarg_len + 1); + if (!modarg) + die("out of memory"); + tmp_modarg = modarg; + + status = sprintf(tmp_modarg, "gpio_mockup_ranges="); + tmp_modarg += status; + + for (i = 0; i < descr->num_chips; i++) { + status = sprintf(tmp_modarg, "%s", line_sizes[i]); + tmp_modarg += status; + } + + /* + * TODO Once the support for named lines in the gpio-mockup module + * is merged upstream, implement checking the named_lines field of + * the test description and setting the corresponding module param. + */ + + modarg[modarg_len - 1] = '\0'; /* Remove the last comma. */ + + gettimeofday(&globals.test_ctx.mod_loaded_ts, NULL); + status = kmod_module_probe_insert_module(globals.module, 0, + modarg, NULL, NULL, NULL); + if (status) + die_perr("unable to load gpio-mockup"); + + free(modarg); + for (i = 0; i < descr->num_chips; i++) + free(line_sizes[i]); + free(line_sizes); +} + +/* + * To see if given chip is a mockup chip, check if it was created after + * inserting the gpio-mockup module. It's not too clever, but works well + * enough... + */ +static bool is_mockup_chip(const char *name) +{ + struct timeval gdev_created_ts; + struct stat gdev_stat; + char *path; + int status; + + status = asprintf(&path, "/dev/%s", name); + if (status < 0) + die("asprintf"); + + status = stat(path, &gdev_stat); + free(path); + if (status < 0) + die_perr("stat"); + + gdev_created_ts.tv_sec = gdev_stat.st_ctim.tv_sec; + gdev_created_ts.tv_usec = gdev_stat.st_ctim.tv_nsec / 1000; + + return timercmp(&globals.test_ctx.mod_loaded_ts, &gdev_created_ts, >=); +} + +static int chipcmp(const void *c1, const void *c2) +{ + const struct mockup_chip *chip1 = c1; + const struct mockup_chip *chip2 = c2; + + return strcmp(chip1->name, chip2->name); +} + +static void test_prepare(struct gu_chip_descr *descr) +{ + struct test_context *ctx; + struct mockup_chip *chip; + unsigned int current = 0; + struct dirent *dentry; + int status; + DIR *dir; + + ctx = &globals.test_ctx; + memset(ctx, 0, sizeof(*ctx)); + + test_load_module(descr); + + ctx->num_chips = descr->num_chips; + ctx->chips = malloc(sizeof(*ctx->chips) * ctx->num_chips); + if (!ctx->chips) + die("out of memory"); + + dir = opendir("/dev"); + if (!dir) + die_perr("error opening /dev"); + + for (dentry = readdir(dir); dentry; dentry = readdir(dir)) { + if (strncmp(dentry->d_name, "gpiochip", 8) == 0) { + if (!is_mockup_chip(dentry->d_name)) + continue; + + chip = &ctx->chips[current++]; + + chip->name = strdup(dentry->d_name); + if (!chip->name) + die("out of memory"); + + status = asprintf(&chip->path, + "/dev/%s", dentry->d_name); + if (status < 0) + die_perr("asprintf"); + + status = sscanf(dentry->d_name, + "gpiochip%u", &chip->number); + if (status != 1) + die("unable to determine the chip number"); + } + } + + qsort(ctx->chips, ctx->num_chips, sizeof(*ctx->chips), chipcmp); + + if (descr->num_chips != current) + die("number of requested and detected mockup gpiochips is not the same"); + + closedir(dir); +} + +static void test_teardown(void) +{ + struct mockup_chip *chip; + unsigned int i; + int status; + + for (i = 0; i < globals.test_ctx.num_chips; i++) { + chip = &globals.test_ctx.chips[i]; + + free(chip->path); + free(chip->name); + } + + free(globals.test_ctx.chips); + + status = kmod_module_remove_module(globals.module, 0); + if (status) + die_perr("unable to remove gpio-mockup"); +} + +int main(int argc GU_UNUSED, char **argv GU_UNUSED) +{ + struct gu_test *test; + + atexit(module_cleanup); + + gu_msg("libgpiod unit-test suite"); + gu_msg("%u tests registered", globals.num_tests); + + check_gpio_mockup(); + + gu_msg("running tests"); + + for (test = globals.test_list_head; test; test = test->_next) { + test_prepare(&test->chip_descr); + + test->func(); + gu_msg("test '%s': %s", test->name, + globals.test_ctx.test_failed ? "FAILED" : "OK"); + if (globals.test_ctx.test_failed) + globals.tests_failed++; + + test_teardown(); + } + + if (!globals.tests_failed) + gu_msg("all tests passed"); + else + gu_err("%u out of %u tests failed", + globals.tests_failed, globals.num_tests); + + return globals.tests_failed ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/tests/unit/gpiod-unit.h b/tests/unit/gpiod-unit.h new file mode 100644 index 0000000..ffefabe --- /dev/null +++ b/tests/unit/gpiod-unit.h @@ -0,0 +1,92 @@ +/* + * Unit testing framework for libgpiod. + * + * Copyright (C) 2017 Bartosz Golaszewski + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2.1 of the GNU Lesser General Public License + * as published by the Free Software Foundation. + */ + +#ifndef __GPIOD_UNIT_H__ +#define __GPIOD_UNIT_H__ + +#include +#include +#include + +#define GU_INIT __attribute__((constructor)) +#define GU_UNUSED __attribute__((unused)) +#define GU_PRINTF(fmt, arg) __attribute__((format(printf, fmt, arg))) +#define GU_CLEANUP(func) __attribute__((cleanup(func))) + +#define GU_ARRAY_SIZE(x) (sizeof(x) / sizeof(*(x))) + +typedef void (*gu_test_func)(void); + +struct gu_chip_descr { + unsigned int num_chips; + unsigned int *num_lines; + bool named_lines; +}; + +struct gu_test { + struct gu_test *_next; + + const char *name; + gu_test_func func; + + struct gu_chip_descr chip_descr; +}; + +void _gu_register_test(struct gu_test *test); +void _gu_set_test_failed(void); + +#define GU_REGISTER_TEST(test) \ + static GU_INIT void _gu_register_##test(void) \ + { \ + _gu_register_test(&test); \ + } \ + static int _gu_##test##_sentinel GU_UNUSED + +#define GU_DEFINE_TEST(_a_func, _a_name, _a_named_lines, ...) \ + static unsigned int _##_a_func##_lines[] = __VA_ARGS__; \ + static struct gu_test _##_a_func##_descr = { \ + .name = _a_name, \ + .func = _a_func, \ + .chip_descr = { \ + .num_chips = GU_ARRAY_SIZE(_##_a_func##_lines), \ + .num_lines = _##_a_func##_lines, \ + .named_lines = _a_named_lines, \ + }, \ + }; \ + GU_REGISTER_TEST(_##_a_func##_descr) + +enum { + GU_LINES_UNNAMED = false, + GU_LINES_NAMED = true, +}; + +void GU_PRINTF(1, 2) gu_msg(const char *fmt, ...); +void GU_PRINTF(1, 2) gu_err(const char *fmt, ...); + +const char * gu_chip_path(unsigned int index); +const char * gu_chip_name(unsigned int index); +unsigned int gu_chip_num(unsigned int index); + +#define GU_ASSERT(statement) \ + do { \ + if (!(statement)) { \ + gu_err("assertion failed (%s:%d): '%s'", \ + __FILE__, __LINE__, #statement); \ + _gu_set_test_failed(); \ + return; \ + } \ + } while (0) + +#define GU_ASSERT_NOT_NULL(ptr) GU_ASSERT(ptr != NULL) +#define GU_ASSERT_NULL(ptr) GU_ASSERT(ptr == NULL) +#define GU_ASSERT_EQ(a1, a2) GU_ASSERT(a1 == a2) +#define GU_ASSERT_STR_EQ(s1, s2) GU_ASSERT(strcmp(s1, s2) == 0) + +#endif /* __GPIOD_UNIT_H__ */ diff --git a/tests/unit/tests-chip.c b/tests/unit/tests-chip.c new file mode 100644 index 0000000..514da80 --- /dev/null +++ b/tests/unit/tests-chip.c @@ -0,0 +1,161 @@ +/* + * GPIO chip test cases for libgpiod. + * + * Copyright (C) 2017 Bartosz Golaszewski + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2.1 of the GNU Lesser General Public License + * as published by the Free Software Foundation. + */ + +#include "gpiod-unit.h" +#include + +#include +#include + +static void close_chip(struct gpiod_chip **chip) +{ + if (*chip) + gpiod_chip_close(*chip); +} + +static void free_str(char **str) +{ + if (*str) + free(*str); +} + +static void chip_open_good(void) +{ + GU_CLEANUP(close_chip) struct gpiod_chip *chip = NULL; + + chip = gpiod_chip_open(gu_chip_path(0)); + GU_ASSERT_NOT_NULL(chip); +} +GU_DEFINE_TEST(chip_open_good, + "gpiod_chip_open() good", + GU_LINES_UNNAMED, { 8 }); + +static void chip_open_nonexistent(void) +{ + struct gpiod_chip *chip; + + chip = gpiod_chip_open("/dev/nonexistent_gpiochip"); + GU_ASSERT_NULL(chip); + GU_ASSERT_EQ(gpiod_errno(), ENOENT); +} +GU_DEFINE_TEST(chip_open_nonexistent, + "gpiod_chip_open() nonexistent", + GU_LINES_UNNAMED, { 8 }); + +static void chip_open_notty(void) +{ + struct gpiod_chip *chip; + + chip = gpiod_chip_open("/dev/null"); + GU_ASSERT_NULL(chip); + GU_ASSERT_EQ(gpiod_errno(), ENOTTY); +} +GU_DEFINE_TEST(chip_open_notty, + "gpiod_chip_open() notty", + GU_LINES_UNNAMED, { 8 }); + +static void chip_open_by_number_good(void) +{ + GU_CLEANUP(close_chip) struct gpiod_chip *chip = NULL; + + chip = gpiod_chip_open_by_number(gu_chip_num(0)); + GU_ASSERT_NOT_NULL(chip); +} +GU_DEFINE_TEST(chip_open_by_number_good, + "gpiod_chip_open_by_number() good", + GU_LINES_UNNAMED, { 8 }); + +static void chip_open_lookup(void) +{ + GU_CLEANUP(close_chip) struct gpiod_chip *chip_by_name = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip_by_path = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip_by_num = NULL; + GU_CLEANUP(free_str) char *chip_num; + + GU_ASSERT(asprintf(&chip_num, "%u", gu_chip_num(0)) > 0); + + chip_by_name = gpiod_chip_open_lookup(gu_chip_name(0)); + chip_by_path = gpiod_chip_open_lookup(gu_chip_path(0)); + chip_by_num = gpiod_chip_open_lookup(chip_num); + + GU_ASSERT_NOT_NULL(chip_by_name); + GU_ASSERT_NOT_NULL(chip_by_path); + GU_ASSERT_NOT_NULL(chip_by_num); +} +GU_DEFINE_TEST(chip_open_lookup, + "gpiod_chip_open_lookup()", + GU_LINES_UNNAMED, { 8 }); + +static void chip_name(void) +{ + GU_CLEANUP(close_chip) struct gpiod_chip *chip0 = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip1 = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip2 = NULL; + + chip0 = gpiod_chip_open(gu_chip_path(0)); + chip1 = gpiod_chip_open(gu_chip_path(1)); + chip2 = gpiod_chip_open(gu_chip_path(2)); + GU_ASSERT_NOT_NULL(chip0); + GU_ASSERT_NOT_NULL(chip1); + GU_ASSERT_NOT_NULL(chip2); + + GU_ASSERT_STR_EQ(gpiod_chip_name(chip0), gu_chip_name(0)); + GU_ASSERT_STR_EQ(gpiod_chip_name(chip1), gu_chip_name(1)); + GU_ASSERT_STR_EQ(gpiod_chip_name(chip2), gu_chip_name(2)); +} +GU_DEFINE_TEST(chip_name, "gpiod_chip_name()", GU_LINES_UNNAMED, { 8, 8, 8 }); + +static void chip_label(void) +{ + GU_CLEANUP(close_chip) struct gpiod_chip *chip0 = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip1 = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip2 = NULL; + + chip0 = gpiod_chip_open(gu_chip_path(0)); + chip1 = gpiod_chip_open(gu_chip_path(1)); + chip2 = gpiod_chip_open(gu_chip_path(2)); + GU_ASSERT_NOT_NULL(chip0); + GU_ASSERT_NOT_NULL(chip1); + GU_ASSERT_NOT_NULL(chip2); + + GU_ASSERT_STR_EQ(gpiod_chip_label(chip0), "gpio-mockup-A"); + GU_ASSERT_STR_EQ(gpiod_chip_label(chip1), "gpio-mockup-B"); + GU_ASSERT_STR_EQ(gpiod_chip_label(chip2), "gpio-mockup-C"); +} +GU_DEFINE_TEST(chip_label, "gpiod_chip_label()", + GU_LINES_UNNAMED, { 8, 8, 8 }); + +static void chip_num_lines(void) +{ + GU_CLEANUP(close_chip) struct gpiod_chip *chip0 = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip1 = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip2 = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip3 = NULL; + GU_CLEANUP(close_chip) struct gpiod_chip *chip4 = NULL; + + chip0 = gpiod_chip_open(gu_chip_path(0)); + chip1 = gpiod_chip_open(gu_chip_path(1)); + chip2 = gpiod_chip_open(gu_chip_path(2)); + chip3 = gpiod_chip_open(gu_chip_path(3)); + chip4 = gpiod_chip_open(gu_chip_path(4)); + GU_ASSERT_NOT_NULL(chip0); + GU_ASSERT_NOT_NULL(chip1); + GU_ASSERT_NOT_NULL(chip2); + GU_ASSERT_NOT_NULL(chip3); + GU_ASSERT_NOT_NULL(chip4); + + GU_ASSERT_EQ(gpiod_chip_num_lines(chip0), 1); + GU_ASSERT_EQ(gpiod_chip_num_lines(chip1), 4); + GU_ASSERT_EQ(gpiod_chip_num_lines(chip2), 8); + GU_ASSERT_EQ(gpiod_chip_num_lines(chip3), 16); + GU_ASSERT_EQ(gpiod_chip_num_lines(chip4), 32); +} +GU_DEFINE_TEST(chip_num_lines, "chip_num_lines()", + GU_LINES_UNNAMED, { 1, 4, 8, 16, 32 });