From 08334c51dbb99d9ecd2bb86a2d94ed06da9e167a Mon Sep 17 00:00:00 2001 From: Brooks Davis Date: Tue, 17 Mar 2020 16:56:50 +0000 Subject: [PATCH] Import the kyua testing framework for infrastructure software Imported at 0.13 plus assumulated changes to git hash a685f91. Obtained from: https://github.com/jmmv/kyua Sponsored by: DARPA --- .gitignore | 23 + .travis.yml | 49 + AUTHORS | 11 + CONTRIBUTING.md | 173 +++ CONTRIBUTORS | 20 + Doxyfile.in | 59 + INSTALL.md | 268 +++++ Kyuafile | 18 + LICENSE | 27 + Makefile.am | 186 +++ NEWS.md | 622 ++++++++++ README.md | 84 ++ admin/.gitignore | 6 + admin/Makefile.am.inc | 41 + admin/build-bintray-dist.sh | 131 +++ admin/check-api-docs.awk | 72 ++ admin/check-style-common.awk | 79 ++ admin/check-style-cpp.awk | 87 ++ admin/check-style-make.awk | 71 ++ admin/check-style-man.awk | 71 ++ admin/check-style-shell.awk | 95 ++ admin/check-style.sh | 170 +++ admin/clean-all.sh | 90 ++ admin/travis-build.sh | 98 ++ admin/travis-install-deps.sh | 83 ++ bootstrap/.gitignore | 4 + bootstrap/Kyuafile | 5 + bootstrap/Makefile.am.inc | 90 ++ bootstrap/atf_helpers.cpp | 71 ++ bootstrap/plain_helpers.cpp | 141 +++ bootstrap/testsuite.at | 200 ++++ cli/Kyuafile | 14 + cli/Makefile.am.inc | 123 ++ cli/cmd_about.cpp | 160 +++ cli/cmd_about.hpp | 57 + cli/cmd_about_test.cpp | 306 +++++ cli/cmd_config.cpp | 122 ++ cli/cmd_config.hpp | 54 + cli/cmd_config_test.cpp | 144 +++ cli/cmd_db_exec.cpp | 200 ++++ cli/cmd_db_exec.hpp | 61 + cli/cmd_db_exec_test.cpp | 165 +++ cli/cmd_db_migrate.cpp | 82 ++ cli/cmd_db_migrate.hpp | 54 + cli/cmd_debug.cpp | 94 ++ cli/cmd_debug.hpp | 54 + cli/cmd_debug_test.cpp | 82 ++ cli/cmd_help.cpp | 250 ++++ cli/cmd_help.hpp | 62 + cli/cmd_help_test.cpp | 347 ++++++ cli/cmd_list.cpp | 161 +++ cli/cmd_list.hpp | 65 + cli/cmd_list_test.cpp | 112 ++ cli/cmd_report.cpp | 421 +++++++ cli/cmd_report.hpp | 54 + cli/cmd_report_html.cpp | 474 ++++++++ cli/cmd_report_html.hpp | 55 + cli/cmd_report_junit.cpp | 89 ++ cli/cmd_report_junit.hpp | 54 + cli/cmd_test.cpp | 186 +++ cli/cmd_test.hpp | 54 + cli/cmd_test_test.cpp | 63 + cli/common.cpp | 411 +++++++ cli/common.hpp | 104 ++ cli/common.ipp | 30 + cli/common_test.cpp | 488 ++++++++ cli/config.cpp | 223 ++++ cli/config.hpp | 55 + cli/config_test.cpp | 351 ++++++ cli/main.cpp | 356 ++++++ cli/main.hpp | 61 + cli/main_test.cpp | 489 ++++++++ configure.ac | 173 +++ doc/.gitignore | 14 + doc/Kyuafile | 5 + doc/Makefile.am.inc | 152 +++ doc/build-root.mdoc | 104 ++ doc/kyua-about.1.in | 95 ++ doc/kyua-config.1.in | 59 + doc/kyua-db-exec.1.in | 80 ++ doc/kyua-db-migrate.1.in | 63 + doc/kyua-debug.1.in | 145 +++ doc/kyua-help.1.in | 64 + doc/kyua-list.1.in | 90 ++ doc/kyua-report-html.1.in | 103 ++ doc/kyua-report-junit.1.in | 87 ++ doc/kyua-report.1.in | 118 ++ doc/kyua-test.1.in | 102 ++ doc/kyua.1.in | 400 +++++++ doc/kyua.conf.5.in | 141 +++ doc/kyuafile.5.in | 407 +++++++ doc/manbuild.sh | 171 +++ doc/manbuild_test.sh | 235 ++++ doc/results-file-flag-read.mdoc | 53 + doc/results-file-flag-write.mdoc | 46 + doc/results-files-report-example.mdoc | 32 + doc/results-files.mdoc | 68 ++ doc/test-filters.mdoc | 40 + doc/test-isolation.mdoc | 112 ++ drivers/Kyuafile | 7 + drivers/Makefile.am.inc | 72 ++ drivers/debug_test.cpp | 109 ++ drivers/debug_test.hpp | 79 ++ drivers/list_tests.cpp | 84 ++ drivers/list_tests.hpp | 92 ++ drivers/list_tests_helpers.cpp | 98 ++ drivers/list_tests_test.cpp | 287 +++++ drivers/report_junit.cpp | 258 ++++ drivers/report_junit.hpp | 75 ++ drivers/report_junit_test.cpp | 415 +++++++ drivers/run_tests.cpp | 344 ++++++ drivers/run_tests.hpp | 106 ++ drivers/scan_results.cpp | 107 ++ drivers/scan_results.hpp | 105 ++ drivers/scan_results_test.cpp | 258 ++++ engine/Kyuafile | 17 + engine/Makefile.am.inc | 155 +++ engine/atf.cpp | 242 ++++ engine/atf.hpp | 72 ++ engine/atf_helpers.cpp | 414 +++++++ engine/atf_list.cpp | 196 +++ engine/atf_list.hpp | 51 + engine/atf_list_test.cpp | 278 +++++ engine/atf_result.cpp | 642 ++++++++++ engine/atf_result.hpp | 114 ++ engine/atf_result_fwd.hpp | 43 + engine/atf_result_test.cpp | 788 +++++++++++++ engine/atf_test.cpp | 450 +++++++ engine/config.cpp | 254 ++++ engine/config.hpp | 65 + engine/config_fwd.hpp | 43 + engine/config_test.cpp | 203 ++++ engine/exceptions.cpp | 81 ++ engine/exceptions.hpp | 75 ++ engine/exceptions_test.cpp | 69 ++ engine/filters.cpp | 389 ++++++ engine/filters.hpp | 134 +++ engine/filters_fwd.hpp | 45 + engine/filters_test.cpp | 594 ++++++++++ engine/kyuafile.cpp | 694 +++++++++++ engine/kyuafile.hpp | 96 ++ engine/kyuafile_fwd.hpp | 43 + engine/kyuafile_test.cpp | 606 ++++++++++ engine/plain.cpp | 143 +++ engine/plain.hpp | 67 ++ engine/plain_helpers.cpp | 238 ++++ engine/plain_test.cpp | 207 ++++ engine/requirements.cpp | 293 +++++ engine/requirements.hpp | 51 + engine/requirements_test.cpp | 511 ++++++++ engine/scanner.cpp | 216 ++++ engine/scanner.hpp | 76 ++ engine/scanner_fwd.hpp | 59 + engine/scanner_test.cpp | 476 ++++++++ engine/scheduler.cpp | 1373 ++++++++++++++++++++++ engine/scheduler.hpp | 282 +++++ engine/scheduler_fwd.hpp | 61 + engine/scheduler_test.cpp | 1242 +++++++++++++++++++ engine/tap.cpp | 191 +++ engine/tap.hpp | 67 ++ engine/tap_helpers.cpp | 202 ++++ engine/tap_parser.cpp | 438 +++++++ engine/tap_parser.hpp | 99 ++ engine/tap_parser_fwd.hpp | 50 + engine/tap_parser_test.cpp | 465 ++++++++ engine/tap_test.cpp | 218 ++++ examples/Kyuafile | 5 + examples/Kyuafile.top | 52 + examples/Makefile.am.inc | 45 + examples/kyua.conf | 69 ++ examples/syntax_test.cpp | 210 ++++ integration/Kyuafile | 16 + integration/Makefile.am.inc | 150 +++ integration/cmd_about_test.sh | 158 +++ integration/cmd_config_test.sh | 355 ++++++ integration/cmd_db_exec_test.sh | 165 +++ integration/cmd_db_migrate_test.sh | 167 +++ integration/cmd_debug_test.sh | 421 +++++++ integration/cmd_help_test.sh | 93 ++ integration/cmd_list_test.sh | 600 ++++++++++ integration/cmd_report_html_test.sh | 267 +++++ integration/cmd_report_junit_test.sh | 300 +++++ integration/cmd_report_test.sh | 381 ++++++ integration/cmd_test_test.sh | 1071 +++++++++++++++++ integration/global_test.sh | 146 +++ integration/helpers/.gitignore | 11 + integration/helpers/Makefile.am.inc | 90 ++ integration/helpers/bad_test_program.cpp | 50 + integration/helpers/bogus_test_cases.cpp | 64 + integration/helpers/config.cpp | 58 + integration/helpers/dump_env.cpp | 74 ++ integration/helpers/expect_all_pass.cpp | 92 ++ integration/helpers/expect_some_fail.cpp | 94 ++ integration/helpers/interrupts.cpp | 62 + integration/helpers/metadata.cpp | 95 ++ integration/helpers/race.cpp | 99 ++ integration/helpers/simple_all_pass.cpp | 55 + integration/helpers/simple_some_fail.cpp | 53 + integration/utils.sh | 177 +++ m4/ax_cxx_compile_stdcxx.m4 | 951 +++++++++++++++ m4/compiler-features.m4 | 122 ++ m4/compiler-flags.m4 | 169 +++ m4/developer-mode.m4 | 112 ++ m4/doxygen.m4 | 62 + m4/fs.m4 | 125 ++ m4/getopt.m4 | 213 ++++ m4/memory.m4 | 122 ++ m4/signals.m4 | 92 ++ m4/uname.m4 | 63 + main.cpp | 50 + misc/Makefile.am.inc | 32 + misc/context.html | 55 + misc/index.html | 187 +++ misc/report.css | 78 ++ misc/test_result.html | 76 ++ model/Kyuafile | 10 + model/Makefile.am.inc | 89 ++ model/README | 11 + model/context.cpp | 159 +++ model/context.hpp | 76 ++ model/context_fwd.hpp | 43 + model/context_test.cpp | 106 ++ model/exceptions.cpp | 76 ++ model/exceptions.hpp | 71 ++ model/exceptions_test.cpp | 65 + model/metadata.cpp | 1068 +++++++++++++++++ model/metadata.hpp | 130 ++ model/metadata_fwd.hpp | 44 + model/metadata_test.cpp | 461 ++++++++ model/test_case.cpp | 339 ++++++ model/test_case.hpp | 98 ++ model/test_case_fwd.hpp | 51 + model/test_case_test.cpp | 263 +++++ model/test_program.cpp | 452 +++++++ model/test_program.hpp | 110 ++ model/test_program_fwd.hpp | 55 + model/test_program_test.cpp | 711 +++++++++++ model/test_result.cpp | 142 +++ model/test_result.hpp | 79 ++ model/test_result_fwd.hpp | 53 + model/test_result_test.cpp | 185 +++ model/types.hpp | 61 + store/Kyuafile | 15 + store/Makefile.am.inc | 145 +++ store/dbtypes.cpp | 255 ++++ store/dbtypes.hpp | 68 ++ store/dbtypes_test.cpp | 234 ++++ store/exceptions.cpp | 88 ++ store/exceptions.hpp | 72 ++ store/exceptions_test.cpp | 65 + store/layout.cpp | 264 +++++ store/layout.hpp | 84 ++ store/layout_fwd.hpp | 54 + store/layout_test.cpp | 350 ++++++ store/metadata.cpp | 137 +++ store/metadata.hpp | 68 ++ store/metadata_fwd.hpp | 43 + store/metadata_test.cpp | 154 +++ store/migrate.cpp | 287 +++++ store/migrate.hpp | 55 + store/migrate_test.cpp | 132 +++ store/migrate_v1_v2.sql | 357 ++++++ store/migrate_v2_v3.sql | 120 ++ store/read_backend.cpp | 160 +++ store/read_backend.hpp | 77 ++ store/read_backend_fwd.hpp | 43 + store/read_backend_test.cpp | 152 +++ store/read_transaction.cpp | 532 +++++++++ store/read_transaction.hpp | 120 ++ store/read_transaction_fwd.hpp | 44 + store/read_transaction_test.cpp | 262 +++++ store/schema_inttest.cpp | 492 ++++++++ store/schema_v1.sql | 314 +++++ store/schema_v2.sql | 293 +++++ store/schema_v3.sql | 255 ++++ store/testdata_v1.sql | 330 ++++++ store/testdata_v2.sql | 462 ++++++++ store/testdata_v3_1.sql | 42 + store/testdata_v3_2.sql | 190 +++ store/testdata_v3_3.sql | 171 +++ store/testdata_v3_4.sql | 141 +++ store/transaction_test.cpp | 170 +++ store/write_backend.cpp | 208 ++++ store/write_backend.hpp | 81 ++ store/write_backend_fwd.hpp | 52 + store/write_backend_test.cpp | 204 ++++ store/write_transaction.cpp | 440 +++++++ store/write_transaction.hpp | 89 ++ store/write_transaction_fwd.hpp | 43 + store/write_transaction_test.cpp | 416 +++++++ utils/.gitignore | 2 + utils/Kyuafile | 24 + utils/Makefile.am.inc | 133 +++ utils/auto_array.hpp | 102 ++ utils/auto_array.ipp | 227 ++++ utils/auto_array_fwd.hpp | 43 + utils/auto_array_test.cpp | 312 +++++ utils/cmdline/Kyuafile | 11 + utils/cmdline/Makefile.am.inc | 96 ++ utils/cmdline/base_command.cpp | 201 ++++ utils/cmdline/base_command.hpp | 162 +++ utils/cmdline/base_command.ipp | 104 ++ utils/cmdline/base_command_fwd.hpp | 47 + utils/cmdline/base_command_test.cpp | 295 +++++ utils/cmdline/commands_map.hpp | 96 ++ utils/cmdline/commands_map.ipp | 161 +++ utils/cmdline/commands_map_fwd.hpp | 45 + utils/cmdline/commands_map_test.cpp | 140 +++ utils/cmdline/exceptions.cpp | 175 +++ utils/cmdline/exceptions.hpp | 109 ++ utils/cmdline/exceptions_test.cpp | 83 ++ utils/cmdline/globals.cpp | 78 ++ utils/cmdline/globals.hpp | 48 + utils/cmdline/globals_test.cpp | 77 ++ utils/cmdline/options.cpp | 605 ++++++++++ utils/cmdline/options.hpp | 237 ++++ utils/cmdline/options_fwd.hpp | 51 + utils/cmdline/options_test.cpp | 526 +++++++++ utils/cmdline/parser.cpp | 385 ++++++ utils/cmdline/parser.hpp | 85 ++ utils/cmdline/parser.ipp | 83 ++ utils/cmdline/parser_fwd.hpp | 58 + utils/cmdline/parser_test.cpp | 688 +++++++++++ utils/cmdline/ui.cpp | 276 +++++ utils/cmdline/ui.hpp | 79 ++ utils/cmdline/ui_fwd.hpp | 45 + utils/cmdline/ui_mock.cpp | 114 ++ utils/cmdline/ui_mock.hpp | 78 ++ utils/cmdline/ui_test.cpp | 424 +++++++ utils/config/Kyuafile | 10 + utils/config/Makefile.am.inc | 87 ++ utils/config/exceptions.cpp | 149 +++ utils/config/exceptions.hpp | 106 ++ utils/config/exceptions_test.cpp | 133 +++ utils/config/keys.cpp | 70 ++ utils/config/keys.hpp | 52 + utils/config/keys_fwd.hpp | 51 + utils/config/keys_test.cpp | 114 ++ utils/config/lua_module.cpp | 282 +++++ utils/config/lua_module.hpp | 50 + utils/config/lua_module_test.cpp | 474 ++++++++ utils/config/nodes.cpp | 589 ++++++++++ utils/config/nodes.hpp | 272 +++++ utils/config/nodes.ipp | 408 +++++++ utils/config/nodes_fwd.hpp | 70 ++ utils/config/nodes_test.cpp | 695 +++++++++++ utils/config/parser.cpp | 181 +++ utils/config/parser.hpp | 95 ++ utils/config/parser_fwd.hpp | 45 + utils/config/parser_test.cpp | 252 ++++ utils/config/tree.cpp | 338 ++++++ utils/config/tree.hpp | 128 ++ utils/config/tree.ipp | 156 +++ utils/config/tree_fwd.hpp | 52 + utils/config/tree_test.cpp | 1086 +++++++++++++++++ utils/datetime.cpp | 613 ++++++++++ utils/datetime.hpp | 140 +++ utils/datetime_fwd.hpp | 46 + utils/datetime_test.cpp | 593 ++++++++++ utils/defs.hpp.in | 57 + utils/env.cpp | 200 ++++ utils/env.hpp | 58 + utils/env_test.cpp | 167 +++ utils/format/Kyuafile | 7 + utils/format/Makefile.am.inc | 59 + utils/format/containers.hpp | 66 ++ utils/format/containers.ipp | 138 +++ utils/format/containers_test.cpp | 190 +++ utils/format/exceptions.cpp | 110 ++ utils/format/exceptions.hpp | 84 ++ utils/format/exceptions_test.cpp | 74 ++ utils/format/formatter.cpp | 293 +++++ utils/format/formatter.hpp | 123 ++ utils/format/formatter.ipp | 76 ++ utils/format/formatter_fwd.hpp | 45 + utils/format/formatter_test.cpp | 265 +++++ utils/format/macros.hpp | 58 + utils/fs/Kyuafile | 10 + utils/fs/Makefile.am.inc | 84 ++ utils/fs/auto_cleaners.cpp | 261 ++++ utils/fs/auto_cleaners.hpp | 89 ++ utils/fs/auto_cleaners_fwd.hpp | 46 + utils/fs/auto_cleaners_test.cpp | 167 +++ utils/fs/directory.cpp | 360 ++++++ utils/fs/directory.hpp | 120 ++ utils/fs/directory_fwd.hpp | 55 + utils/fs/directory_test.cpp | 190 +++ utils/fs/exceptions.cpp | 162 +++ utils/fs/exceptions.hpp | 110 ++ utils/fs/exceptions_test.cpp | 95 ++ utils/fs/lua_module.cpp | 340 ++++++ utils/fs/lua_module.hpp | 54 + utils/fs/lua_module_test.cpp | 376 ++++++ utils/fs/operations.cpp | 803 +++++++++++++ utils/fs/operations.hpp | 72 ++ utils/fs/operations_test.cpp | 826 +++++++++++++ utils/fs/path.cpp | 303 +++++ utils/fs/path.hpp | 87 ++ utils/fs/path_fwd.hpp | 45 + utils/fs/path_test.cpp | 277 +++++ utils/logging/Kyuafile | 6 + utils/logging/Makefile.am.inc | 53 + utils/logging/macros.hpp | 68 ++ utils/logging/macros_test.cpp | 115 ++ utils/logging/operations.cpp | 303 +++++ utils/logging/operations.hpp | 54 + utils/logging/operations_fwd.hpp | 54 + utils/logging/operations_test.cpp | 354 ++++++ utils/memory.cpp | 158 +++ utils/memory.hpp | 45 + utils/memory_test.cpp | 63 + utils/noncopyable.hpp | 75 ++ utils/optional.hpp | 90 ++ utils/optional.ipp | 252 ++++ utils/optional_fwd.hpp | 61 + utils/optional_test.cpp | 285 +++++ utils/passwd.cpp | 194 +++ utils/passwd.hpp | 72 ++ utils/passwd_fwd.hpp | 45 + utils/passwd_test.cpp | 179 +++ utils/process/.gitignore | 1 + utils/process/Kyuafile | 13 + utils/process/Makefile.am.inc | 113 ++ utils/process/child.cpp | 385 ++++++ utils/process/child.hpp | 113 ++ utils/process/child.ipp | 110 ++ utils/process/child_fwd.hpp | 45 + utils/process/child_test.cpp | 846 +++++++++++++ utils/process/deadline_killer.cpp | 54 + utils/process/deadline_killer.hpp | 58 + utils/process/deadline_killer_fwd.hpp | 45 + utils/process/deadline_killer_test.cpp | 108 ++ utils/process/exceptions.cpp | 91 ++ utils/process/exceptions.hpp | 78 ++ utils/process/exceptions_test.cpp | 63 + utils/process/executor.cpp | 869 ++++++++++++++ utils/process/executor.hpp | 231 ++++ utils/process/executor.ipp | 182 +++ utils/process/executor_fwd.hpp | 49 + utils/process/executor_test.cpp | 940 +++++++++++++++ utils/process/fdstream.cpp | 76 ++ utils/process/fdstream.hpp | 66 ++ utils/process/fdstream_fwd.hpp | 45 + utils/process/fdstream_test.cpp | 73 ++ utils/process/helpers.cpp | 74 ++ utils/process/isolation.cpp | 207 ++++ utils/process/isolation.hpp | 60 + utils/process/isolation_test.cpp | 622 ++++++++++ utils/process/operations.cpp | 273 +++++ utils/process/operations.hpp | 56 + utils/process/operations_fwd.hpp | 49 + utils/process/operations_test.cpp | 471 ++++++++ utils/process/status.cpp | 200 ++++ utils/process/status.hpp | 84 ++ utils/process/status_fwd.hpp | 45 + utils/process/status_test.cpp | 209 ++++ utils/process/system.cpp | 59 + utils/process/system.hpp | 66 ++ utils/process/systembuf.cpp | 152 +++ utils/process/systembuf.hpp | 71 ++ utils/process/systembuf_fwd.hpp | 45 + utils/process/systembuf_test.cpp | 166 +++ utils/sanity.cpp | 194 +++ utils/sanity.hpp | 183 +++ utils/sanity_fwd.hpp | 52 + utils/sanity_test.cpp | 322 +++++ utils/signals/Kyuafile | 9 + utils/signals/Makefile.am.inc | 73 ++ utils/signals/exceptions.cpp | 102 ++ utils/signals/exceptions.hpp | 83 ++ utils/signals/exceptions_test.cpp | 73 ++ utils/signals/interrupts.cpp | 309 +++++ utils/signals/interrupts.hpp | 83 ++ utils/signals/interrupts_fwd.hpp | 46 + utils/signals/interrupts_test.cpp | 266 +++++ utils/signals/misc.cpp | 106 ++ utils/signals/misc.hpp | 49 + utils/signals/misc_test.cpp | 133 +++ utils/signals/programmer.cpp | 138 +++ utils/signals/programmer.hpp | 63 + utils/signals/programmer_fwd.hpp | 49 + utils/signals/programmer_test.cpp | 140 +++ utils/signals/timer.cpp | 547 +++++++++ utils/signals/timer.hpp | 86 ++ utils/signals/timer_fwd.hpp | 45 + utils/signals/timer_test.cpp | 426 +++++++ utils/sqlite/Kyuafile | 9 + utils/sqlite/Makefile.am.inc | 82 ++ utils/sqlite/c_gate.cpp | 83 ++ utils/sqlite/c_gate.hpp | 74 ++ utils/sqlite/c_gate_fwd.hpp | 45 + utils/sqlite/c_gate_test.cpp | 96 ++ utils/sqlite/database.cpp | 328 ++++++ utils/sqlite/database.hpp | 111 ++ utils/sqlite/database_fwd.hpp | 45 + utils/sqlite/database_test.cpp | 287 +++++ utils/sqlite/exceptions.cpp | 175 +++ utils/sqlite/exceptions.hpp | 94 ++ utils/sqlite/exceptions_test.cpp | 129 ++ utils/sqlite/statement.cpp | 621 ++++++++++ utils/sqlite/statement.hpp | 137 +++ utils/sqlite/statement.ipp | 52 + utils/sqlite/statement_fwd.hpp | 57 + utils/sqlite/statement_test.cpp | 784 ++++++++++++ utils/sqlite/test_utils.hpp | 151 +++ utils/sqlite/transaction.cpp | 142 +++ utils/sqlite/transaction.hpp | 69 ++ utils/sqlite/transaction_fwd.hpp | 45 + utils/sqlite/transaction_test.cpp | 135 +++ utils/stacktrace.cpp | 370 ++++++ utils/stacktrace.hpp | 68 ++ utils/stacktrace_helper.cpp | 36 + utils/stacktrace_test.cpp | 620 ++++++++++ utils/stream.cpp | 149 +++ utils/stream.hpp | 57 + utils/stream_test.cpp | 157 +++ utils/test_utils.ipp | 113 ++ utils/text/Kyuafile | 9 + utils/text/Makefile.am.inc | 74 ++ utils/text/exceptions.cpp | 91 ++ utils/text/exceptions.hpp | 77 ++ utils/text/exceptions_test.cpp | 76 ++ utils/text/operations.cpp | 261 ++++ utils/text/operations.hpp | 68 ++ utils/text/operations.ipp | 91 ++ utils/text/operations_test.cpp | 435 +++++++ utils/text/regex.cpp | 302 +++++ utils/text/regex.hpp | 92 ++ utils/text/regex_fwd.hpp | 46 + utils/text/regex_test.cpp | 177 +++ utils/text/table.cpp | 428 +++++++ utils/text/table.hpp | 125 ++ utils/text/table_fwd.hpp | 58 + utils/text/table_test.cpp | 413 +++++++ utils/text/templates.cpp | 764 ++++++++++++ utils/text/templates.hpp | 122 ++ utils/text/templates_fwd.hpp | 45 + utils/text/templates_test.cpp | 1001 ++++++++++++++++ utils/units.cpp | 172 +++ utils/units.hpp | 96 ++ utils/units_fwd.hpp | 45 + utils/units_test.cpp | 248 ++++ 542 files changed, 96704 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 AUTHORS create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTORS create mode 100644 Doxyfile.in create mode 100644 INSTALL.md create mode 100644 Kyuafile create mode 100644 LICENSE create mode 100644 Makefile.am create mode 100644 NEWS.md create mode 100644 README.md create mode 100644 admin/.gitignore create mode 100644 admin/Makefile.am.inc create mode 100755 admin/build-bintray-dist.sh create mode 100644 admin/check-api-docs.awk create mode 100644 admin/check-style-common.awk create mode 100644 admin/check-style-cpp.awk create mode 100644 admin/check-style-make.awk create mode 100644 admin/check-style-man.awk create mode 100644 admin/check-style-shell.awk create mode 100755 admin/check-style.sh create mode 100755 admin/clean-all.sh create mode 100755 admin/travis-build.sh create mode 100755 admin/travis-install-deps.sh create mode 100644 bootstrap/.gitignore create mode 100644 bootstrap/Kyuafile create mode 100644 bootstrap/Makefile.am.inc create mode 100644 bootstrap/atf_helpers.cpp create mode 100644 bootstrap/plain_helpers.cpp create mode 100644 bootstrap/testsuite.at create mode 100644 cli/Kyuafile create mode 100644 cli/Makefile.am.inc create mode 100644 cli/cmd_about.cpp create mode 100644 cli/cmd_about.hpp create mode 100644 cli/cmd_about_test.cpp create mode 100644 cli/cmd_config.cpp create mode 100644 cli/cmd_config.hpp create mode 100644 cli/cmd_config_test.cpp create mode 100644 cli/cmd_db_exec.cpp create mode 100644 cli/cmd_db_exec.hpp create mode 100644 cli/cmd_db_exec_test.cpp create mode 100644 cli/cmd_db_migrate.cpp create mode 100644 cli/cmd_db_migrate.hpp create mode 100644 cli/cmd_debug.cpp create mode 100644 cli/cmd_debug.hpp create mode 100644 cli/cmd_debug_test.cpp create mode 100644 cli/cmd_help.cpp create mode 100644 cli/cmd_help.hpp create mode 100644 cli/cmd_help_test.cpp create mode 100644 cli/cmd_list.cpp create mode 100644 cli/cmd_list.hpp create mode 100644 cli/cmd_list_test.cpp create mode 100644 cli/cmd_report.cpp create mode 100644 cli/cmd_report.hpp create mode 100644 cli/cmd_report_html.cpp create mode 100644 cli/cmd_report_html.hpp create mode 100644 cli/cmd_report_junit.cpp create mode 100644 cli/cmd_report_junit.hpp create mode 100644 cli/cmd_test.cpp create mode 100644 cli/cmd_test.hpp create mode 100644 cli/cmd_test_test.cpp create mode 100644 cli/common.cpp create mode 100644 cli/common.hpp create mode 100644 cli/common.ipp create mode 100644 cli/common_test.cpp create mode 100644 cli/config.cpp create mode 100644 cli/config.hpp create mode 100644 cli/config_test.cpp create mode 100644 cli/main.cpp create mode 100644 cli/main.hpp create mode 100644 cli/main_test.cpp create mode 100644 configure.ac create mode 100644 doc/.gitignore create mode 100644 doc/Kyuafile create mode 100644 doc/Makefile.am.inc create mode 100644 doc/build-root.mdoc create mode 100644 doc/kyua-about.1.in create mode 100644 doc/kyua-config.1.in create mode 100644 doc/kyua-db-exec.1.in create mode 100644 doc/kyua-db-migrate.1.in create mode 100644 doc/kyua-debug.1.in create mode 100644 doc/kyua-help.1.in create mode 100644 doc/kyua-list.1.in create mode 100644 doc/kyua-report-html.1.in create mode 100644 doc/kyua-report-junit.1.in create mode 100644 doc/kyua-report.1.in create mode 100644 doc/kyua-test.1.in create mode 100644 doc/kyua.1.in create mode 100644 doc/kyua.conf.5.in create mode 100644 doc/kyuafile.5.in create mode 100755 doc/manbuild.sh create mode 100755 doc/manbuild_test.sh create mode 100644 doc/results-file-flag-read.mdoc create mode 100644 doc/results-file-flag-write.mdoc create mode 100644 doc/results-files-report-example.mdoc create mode 100644 doc/results-files.mdoc create mode 100644 doc/test-filters.mdoc create mode 100644 doc/test-isolation.mdoc create mode 100644 drivers/Kyuafile create mode 100644 drivers/Makefile.am.inc create mode 100644 drivers/debug_test.cpp create mode 100644 drivers/debug_test.hpp create mode 100644 drivers/list_tests.cpp create mode 100644 drivers/list_tests.hpp create mode 100644 drivers/list_tests_helpers.cpp create mode 100644 drivers/list_tests_test.cpp create mode 100644 drivers/report_junit.cpp create mode 100644 drivers/report_junit.hpp create mode 100644 drivers/report_junit_test.cpp create mode 100644 drivers/run_tests.cpp create mode 100644 drivers/run_tests.hpp create mode 100644 drivers/scan_results.cpp create mode 100644 drivers/scan_results.hpp create mode 100644 drivers/scan_results_test.cpp create mode 100644 engine/Kyuafile create mode 100644 engine/Makefile.am.inc create mode 100644 engine/atf.cpp create mode 100644 engine/atf.hpp create mode 100644 engine/atf_helpers.cpp create mode 100644 engine/atf_list.cpp create mode 100644 engine/atf_list.hpp create mode 100644 engine/atf_list_test.cpp create mode 100644 engine/atf_result.cpp create mode 100644 engine/atf_result.hpp create mode 100644 engine/atf_result_fwd.hpp create mode 100644 engine/atf_result_test.cpp create mode 100644 engine/atf_test.cpp create mode 100644 engine/config.cpp create mode 100644 engine/config.hpp create mode 100644 engine/config_fwd.hpp create mode 100644 engine/config_test.cpp create mode 100644 engine/exceptions.cpp create mode 100644 engine/exceptions.hpp create mode 100644 engine/exceptions_test.cpp create mode 100644 engine/filters.cpp create mode 100644 engine/filters.hpp create mode 100644 engine/filters_fwd.hpp create mode 100644 engine/filters_test.cpp create mode 100644 engine/kyuafile.cpp create mode 100644 engine/kyuafile.hpp create mode 100644 engine/kyuafile_fwd.hpp create mode 100644 engine/kyuafile_test.cpp create mode 100644 engine/plain.cpp create mode 100644 engine/plain.hpp create mode 100644 engine/plain_helpers.cpp create mode 100644 engine/plain_test.cpp create mode 100644 engine/requirements.cpp create mode 100644 engine/requirements.hpp create mode 100644 engine/requirements_test.cpp create mode 100644 engine/scanner.cpp create mode 100644 engine/scanner.hpp create mode 100644 engine/scanner_fwd.hpp create mode 100644 engine/scanner_test.cpp create mode 100644 engine/scheduler.cpp create mode 100644 engine/scheduler.hpp create mode 100644 engine/scheduler_fwd.hpp create mode 100644 engine/scheduler_test.cpp create mode 100644 engine/tap.cpp create mode 100644 engine/tap.hpp create mode 100644 engine/tap_helpers.cpp create mode 100644 engine/tap_parser.cpp create mode 100644 engine/tap_parser.hpp create mode 100644 engine/tap_parser_fwd.hpp create mode 100644 engine/tap_parser_test.cpp create mode 100644 engine/tap_test.cpp create mode 100644 examples/Kyuafile create mode 100644 examples/Kyuafile.top create mode 100644 examples/Makefile.am.inc create mode 100644 examples/kyua.conf create mode 100644 examples/syntax_test.cpp create mode 100644 integration/Kyuafile create mode 100644 integration/Makefile.am.inc create mode 100755 integration/cmd_about_test.sh create mode 100755 integration/cmd_config_test.sh create mode 100755 integration/cmd_db_exec_test.sh create mode 100755 integration/cmd_db_migrate_test.sh create mode 100755 integration/cmd_debug_test.sh create mode 100755 integration/cmd_help_test.sh create mode 100755 integration/cmd_list_test.sh create mode 100755 integration/cmd_report_html_test.sh create mode 100755 integration/cmd_report_junit_test.sh create mode 100755 integration/cmd_report_test.sh create mode 100755 integration/cmd_test_test.sh create mode 100755 integration/global_test.sh create mode 100644 integration/helpers/.gitignore create mode 100644 integration/helpers/Makefile.am.inc create mode 100644 integration/helpers/bad_test_program.cpp create mode 100644 integration/helpers/bogus_test_cases.cpp create mode 100644 integration/helpers/config.cpp create mode 100644 integration/helpers/dump_env.cpp create mode 100644 integration/helpers/expect_all_pass.cpp create mode 100644 integration/helpers/expect_some_fail.cpp create mode 100644 integration/helpers/interrupts.cpp create mode 100644 integration/helpers/metadata.cpp create mode 100644 integration/helpers/race.cpp create mode 100644 integration/helpers/simple_all_pass.cpp create mode 100644 integration/helpers/simple_some_fail.cpp create mode 100755 integration/utils.sh create mode 100644 m4/ax_cxx_compile_stdcxx.m4 create mode 100644 m4/compiler-features.m4 create mode 100644 m4/compiler-flags.m4 create mode 100644 m4/developer-mode.m4 create mode 100644 m4/doxygen.m4 create mode 100644 m4/fs.m4 create mode 100644 m4/getopt.m4 create mode 100644 m4/memory.m4 create mode 100644 m4/signals.m4 create mode 100644 m4/uname.m4 create mode 100644 main.cpp create mode 100644 misc/Makefile.am.inc create mode 100644 misc/context.html create mode 100644 misc/index.html create mode 100644 misc/report.css create mode 100644 misc/test_result.html create mode 100644 model/Kyuafile create mode 100644 model/Makefile.am.inc create mode 100644 model/README create mode 100644 model/context.cpp create mode 100644 model/context.hpp create mode 100644 model/context_fwd.hpp create mode 100644 model/context_test.cpp create mode 100644 model/exceptions.cpp create mode 100644 model/exceptions.hpp create mode 100644 model/exceptions_test.cpp create mode 100644 model/metadata.cpp create mode 100644 model/metadata.hpp create mode 100644 model/metadata_fwd.hpp create mode 100644 model/metadata_test.cpp create mode 100644 model/test_case.cpp create mode 100644 model/test_case.hpp create mode 100644 model/test_case_fwd.hpp create mode 100644 model/test_case_test.cpp create mode 100644 model/test_program.cpp create mode 100644 model/test_program.hpp create mode 100644 model/test_program_fwd.hpp create mode 100644 model/test_program_test.cpp create mode 100644 model/test_result.cpp create mode 100644 model/test_result.hpp create mode 100644 model/test_result_fwd.hpp create mode 100644 model/test_result_test.cpp create mode 100644 model/types.hpp create mode 100644 store/Kyuafile create mode 100644 store/Makefile.am.inc create mode 100644 store/dbtypes.cpp create mode 100644 store/dbtypes.hpp create mode 100644 store/dbtypes_test.cpp create mode 100644 store/exceptions.cpp create mode 100644 store/exceptions.hpp create mode 100644 store/exceptions_test.cpp create mode 100644 store/layout.cpp create mode 100644 store/layout.hpp create mode 100644 store/layout_fwd.hpp create mode 100644 store/layout_test.cpp create mode 100644 store/metadata.cpp create mode 100644 store/metadata.hpp create mode 100644 store/metadata_fwd.hpp create mode 100644 store/metadata_test.cpp create mode 100644 store/migrate.cpp create mode 100644 store/migrate.hpp create mode 100644 store/migrate_test.cpp create mode 100644 store/migrate_v1_v2.sql create mode 100644 store/migrate_v2_v3.sql create mode 100644 store/read_backend.cpp create mode 100644 store/read_backend.hpp create mode 100644 store/read_backend_fwd.hpp create mode 100644 store/read_backend_test.cpp create mode 100644 store/read_transaction.cpp create mode 100644 store/read_transaction.hpp create mode 100644 store/read_transaction_fwd.hpp create mode 100644 store/read_transaction_test.cpp create mode 100644 store/schema_inttest.cpp create mode 100644 store/schema_v1.sql create mode 100644 store/schema_v2.sql create mode 100644 store/schema_v3.sql create mode 100644 store/testdata_v1.sql create mode 100644 store/testdata_v2.sql create mode 100644 store/testdata_v3_1.sql create mode 100644 store/testdata_v3_2.sql create mode 100644 store/testdata_v3_3.sql create mode 100644 store/testdata_v3_4.sql create mode 100644 store/transaction_test.cpp create mode 100644 store/write_backend.cpp create mode 100644 store/write_backend.hpp create mode 100644 store/write_backend_fwd.hpp create mode 100644 store/write_backend_test.cpp create mode 100644 store/write_transaction.cpp create mode 100644 store/write_transaction.hpp create mode 100644 store/write_transaction_fwd.hpp create mode 100644 store/write_transaction_test.cpp create mode 100644 utils/.gitignore create mode 100644 utils/Kyuafile create mode 100644 utils/Makefile.am.inc create mode 100644 utils/auto_array.hpp create mode 100644 utils/auto_array.ipp create mode 100644 utils/auto_array_fwd.hpp create mode 100644 utils/auto_array_test.cpp create mode 100644 utils/cmdline/Kyuafile create mode 100644 utils/cmdline/Makefile.am.inc create mode 100644 utils/cmdline/base_command.cpp create mode 100644 utils/cmdline/base_command.hpp create mode 100644 utils/cmdline/base_command.ipp create mode 100644 utils/cmdline/base_command_fwd.hpp create mode 100644 utils/cmdline/base_command_test.cpp create mode 100644 utils/cmdline/commands_map.hpp create mode 100644 utils/cmdline/commands_map.ipp create mode 100644 utils/cmdline/commands_map_fwd.hpp create mode 100644 utils/cmdline/commands_map_test.cpp create mode 100644 utils/cmdline/exceptions.cpp create mode 100644 utils/cmdline/exceptions.hpp create mode 100644 utils/cmdline/exceptions_test.cpp create mode 100644 utils/cmdline/globals.cpp create mode 100644 utils/cmdline/globals.hpp create mode 100644 utils/cmdline/globals_test.cpp create mode 100644 utils/cmdline/options.cpp create mode 100644 utils/cmdline/options.hpp create mode 100644 utils/cmdline/options_fwd.hpp create mode 100644 utils/cmdline/options_test.cpp create mode 100644 utils/cmdline/parser.cpp create mode 100644 utils/cmdline/parser.hpp create mode 100644 utils/cmdline/parser.ipp create mode 100644 utils/cmdline/parser_fwd.hpp create mode 100644 utils/cmdline/parser_test.cpp create mode 100644 utils/cmdline/ui.cpp create mode 100644 utils/cmdline/ui.hpp create mode 100644 utils/cmdline/ui_fwd.hpp create mode 100644 utils/cmdline/ui_mock.cpp create mode 100644 utils/cmdline/ui_mock.hpp create mode 100644 utils/cmdline/ui_test.cpp create mode 100644 utils/config/Kyuafile create mode 100644 utils/config/Makefile.am.inc create mode 100644 utils/config/exceptions.cpp create mode 100644 utils/config/exceptions.hpp create mode 100644 utils/config/exceptions_test.cpp create mode 100644 utils/config/keys.cpp create mode 100644 utils/config/keys.hpp create mode 100644 utils/config/keys_fwd.hpp create mode 100644 utils/config/keys_test.cpp create mode 100644 utils/config/lua_module.cpp create mode 100644 utils/config/lua_module.hpp create mode 100644 utils/config/lua_module_test.cpp create mode 100644 utils/config/nodes.cpp create mode 100644 utils/config/nodes.hpp create mode 100644 utils/config/nodes.ipp create mode 100644 utils/config/nodes_fwd.hpp create mode 100644 utils/config/nodes_test.cpp create mode 100644 utils/config/parser.cpp create mode 100644 utils/config/parser.hpp create mode 100644 utils/config/parser_fwd.hpp create mode 100644 utils/config/parser_test.cpp create mode 100644 utils/config/tree.cpp create mode 100644 utils/config/tree.hpp create mode 100644 utils/config/tree.ipp create mode 100644 utils/config/tree_fwd.hpp create mode 100644 utils/config/tree_test.cpp create mode 100644 utils/datetime.cpp create mode 100644 utils/datetime.hpp create mode 100644 utils/datetime_fwd.hpp create mode 100644 utils/datetime_test.cpp create mode 100644 utils/defs.hpp.in create mode 100644 utils/env.cpp create mode 100644 utils/env.hpp create mode 100644 utils/env_test.cpp create mode 100644 utils/format/Kyuafile create mode 100644 utils/format/Makefile.am.inc create mode 100644 utils/format/containers.hpp create mode 100644 utils/format/containers.ipp create mode 100644 utils/format/containers_test.cpp create mode 100644 utils/format/exceptions.cpp create mode 100644 utils/format/exceptions.hpp create mode 100644 utils/format/exceptions_test.cpp create mode 100644 utils/format/formatter.cpp create mode 100644 utils/format/formatter.hpp create mode 100644 utils/format/formatter.ipp create mode 100644 utils/format/formatter_fwd.hpp create mode 100644 utils/format/formatter_test.cpp create mode 100644 utils/format/macros.hpp create mode 100644 utils/fs/Kyuafile create mode 100644 utils/fs/Makefile.am.inc create mode 100644 utils/fs/auto_cleaners.cpp create mode 100644 utils/fs/auto_cleaners.hpp create mode 100644 utils/fs/auto_cleaners_fwd.hpp create mode 100644 utils/fs/auto_cleaners_test.cpp create mode 100644 utils/fs/directory.cpp create mode 100644 utils/fs/directory.hpp create mode 100644 utils/fs/directory_fwd.hpp create mode 100644 utils/fs/directory_test.cpp create mode 100644 utils/fs/exceptions.cpp create mode 100644 utils/fs/exceptions.hpp create mode 100644 utils/fs/exceptions_test.cpp create mode 100644 utils/fs/lua_module.cpp create mode 100644 utils/fs/lua_module.hpp create mode 100644 utils/fs/lua_module_test.cpp create mode 100644 utils/fs/operations.cpp create mode 100644 utils/fs/operations.hpp create mode 100644 utils/fs/operations_test.cpp create mode 100644 utils/fs/path.cpp create mode 100644 utils/fs/path.hpp create mode 100644 utils/fs/path_fwd.hpp create mode 100644 utils/fs/path_test.cpp create mode 100644 utils/logging/Kyuafile create mode 100644 utils/logging/Makefile.am.inc create mode 100644 utils/logging/macros.hpp create mode 100644 utils/logging/macros_test.cpp create mode 100644 utils/logging/operations.cpp create mode 100644 utils/logging/operations.hpp create mode 100644 utils/logging/operations_fwd.hpp create mode 100644 utils/logging/operations_test.cpp create mode 100644 utils/memory.cpp create mode 100644 utils/memory.hpp create mode 100644 utils/memory_test.cpp create mode 100644 utils/noncopyable.hpp create mode 100644 utils/optional.hpp create mode 100644 utils/optional.ipp create mode 100644 utils/optional_fwd.hpp create mode 100644 utils/optional_test.cpp create mode 100644 utils/passwd.cpp create mode 100644 utils/passwd.hpp create mode 100644 utils/passwd_fwd.hpp create mode 100644 utils/passwd_test.cpp create mode 100644 utils/process/.gitignore create mode 100644 utils/process/Kyuafile create mode 100644 utils/process/Makefile.am.inc create mode 100644 utils/process/child.cpp create mode 100644 utils/process/child.hpp create mode 100644 utils/process/child.ipp create mode 100644 utils/process/child_fwd.hpp create mode 100644 utils/process/child_test.cpp create mode 100644 utils/process/deadline_killer.cpp create mode 100644 utils/process/deadline_killer.hpp create mode 100644 utils/process/deadline_killer_fwd.hpp create mode 100644 utils/process/deadline_killer_test.cpp create mode 100644 utils/process/exceptions.cpp create mode 100644 utils/process/exceptions.hpp create mode 100644 utils/process/exceptions_test.cpp create mode 100644 utils/process/executor.cpp create mode 100644 utils/process/executor.hpp create mode 100644 utils/process/executor.ipp create mode 100644 utils/process/executor_fwd.hpp create mode 100644 utils/process/executor_test.cpp create mode 100644 utils/process/fdstream.cpp create mode 100644 utils/process/fdstream.hpp create mode 100644 utils/process/fdstream_fwd.hpp create mode 100644 utils/process/fdstream_test.cpp create mode 100644 utils/process/helpers.cpp create mode 100644 utils/process/isolation.cpp create mode 100644 utils/process/isolation.hpp create mode 100644 utils/process/isolation_test.cpp create mode 100644 utils/process/operations.cpp create mode 100644 utils/process/operations.hpp create mode 100644 utils/process/operations_fwd.hpp create mode 100644 utils/process/operations_test.cpp create mode 100644 utils/process/status.cpp create mode 100644 utils/process/status.hpp create mode 100644 utils/process/status_fwd.hpp create mode 100644 utils/process/status_test.cpp create mode 100644 utils/process/system.cpp create mode 100644 utils/process/system.hpp create mode 100644 utils/process/systembuf.cpp create mode 100644 utils/process/systembuf.hpp create mode 100644 utils/process/systembuf_fwd.hpp create mode 100644 utils/process/systembuf_test.cpp create mode 100644 utils/sanity.cpp create mode 100644 utils/sanity.hpp create mode 100644 utils/sanity_fwd.hpp create mode 100644 utils/sanity_test.cpp create mode 100644 utils/signals/Kyuafile create mode 100644 utils/signals/Makefile.am.inc create mode 100644 utils/signals/exceptions.cpp create mode 100644 utils/signals/exceptions.hpp create mode 100644 utils/signals/exceptions_test.cpp create mode 100644 utils/signals/interrupts.cpp create mode 100644 utils/signals/interrupts.hpp create mode 100644 utils/signals/interrupts_fwd.hpp create mode 100644 utils/signals/interrupts_test.cpp create mode 100644 utils/signals/misc.cpp create mode 100644 utils/signals/misc.hpp create mode 100644 utils/signals/misc_test.cpp create mode 100644 utils/signals/programmer.cpp create mode 100644 utils/signals/programmer.hpp create mode 100644 utils/signals/programmer_fwd.hpp create mode 100644 utils/signals/programmer_test.cpp create mode 100644 utils/signals/timer.cpp create mode 100644 utils/signals/timer.hpp create mode 100644 utils/signals/timer_fwd.hpp create mode 100644 utils/signals/timer_test.cpp create mode 100644 utils/sqlite/Kyuafile create mode 100644 utils/sqlite/Makefile.am.inc create mode 100644 utils/sqlite/c_gate.cpp create mode 100644 utils/sqlite/c_gate.hpp create mode 100644 utils/sqlite/c_gate_fwd.hpp create mode 100644 utils/sqlite/c_gate_test.cpp create mode 100644 utils/sqlite/database.cpp create mode 100644 utils/sqlite/database.hpp create mode 100644 utils/sqlite/database_fwd.hpp create mode 100644 utils/sqlite/database_test.cpp create mode 100644 utils/sqlite/exceptions.cpp create mode 100644 utils/sqlite/exceptions.hpp create mode 100644 utils/sqlite/exceptions_test.cpp create mode 100644 utils/sqlite/statement.cpp create mode 100644 utils/sqlite/statement.hpp create mode 100644 utils/sqlite/statement.ipp create mode 100644 utils/sqlite/statement_fwd.hpp create mode 100644 utils/sqlite/statement_test.cpp create mode 100644 utils/sqlite/test_utils.hpp create mode 100644 utils/sqlite/transaction.cpp create mode 100644 utils/sqlite/transaction.hpp create mode 100644 utils/sqlite/transaction_fwd.hpp create mode 100644 utils/sqlite/transaction_test.cpp create mode 100644 utils/stacktrace.cpp create mode 100644 utils/stacktrace.hpp create mode 100644 utils/stacktrace_helper.cpp create mode 100644 utils/stacktrace_test.cpp create mode 100644 utils/stream.cpp create mode 100644 utils/stream.hpp create mode 100644 utils/stream_test.cpp create mode 100644 utils/test_utils.ipp create mode 100644 utils/text/Kyuafile create mode 100644 utils/text/Makefile.am.inc create mode 100644 utils/text/exceptions.cpp create mode 100644 utils/text/exceptions.hpp create mode 100644 utils/text/exceptions_test.cpp create mode 100644 utils/text/operations.cpp create mode 100644 utils/text/operations.hpp create mode 100644 utils/text/operations.ipp create mode 100644 utils/text/operations_test.cpp create mode 100644 utils/text/regex.cpp create mode 100644 utils/text/regex.hpp create mode 100644 utils/text/regex_fwd.hpp create mode 100644 utils/text/regex_test.cpp create mode 100644 utils/text/table.cpp create mode 100644 utils/text/table.hpp create mode 100644 utils/text/table_fwd.hpp create mode 100644 utils/text/table_test.cpp create mode 100644 utils/text/templates.cpp create mode 100644 utils/text/templates.hpp create mode 100644 utils/text/templates_fwd.hpp create mode 100644 utils/text/templates_test.cpp create mode 100644 utils/units.cpp create mode 100644 utils/units.hpp create mode 100644 utils/units_fwd.hpp create mode 100644 utils/units_test.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..d7f1a180d6fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +*.a +*.o +*_helpers +*_inttest +*_test +*~ + +.deps +.dirstamp +Doxyfile +Makefile +Makefile.in +aclocal.m4 +api-docs +autom4te.cache +config.h +config.h.in +config.log +config.status +configure +kyua +local-kyua +stamp-h1 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000000..619dceaf3d2d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +language: cpp +sudo: required + +before_install: + - ./admin/travis-install-deps.sh + +matrix: + include: + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=distcheck AS_ROOT=no + - os: linux + dist: xenial + compiler: gcc + env: ARCH=amd64 DO=distcheck AS_ROOT=no + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=apidocs + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=style + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=distcheck AS_ROOT=yes UNPRIVILEGED_USER=no + - os: linux + dist: xenial + compiler: clang + env: ARCH=amd64 DO=distcheck AS_ROOT=yes UNPRIVILEGED_USER=yes + # TODO(ngie): reenable i386; the libraries were not available in the + # Ubuntu Xenial x86_64 docker image. + #- os: linux + # dist: xenial + # compiler: clang + # env: ARCH=i386 DO=distcheck AS_ROOT=no + #- os: linux + # dist: xenial + # compiler: gcc + # env: ARCH=i386 DO=distcheck AS_ROOT=no + +script: + - ./admin/travis-build.sh + +notifications: + email: + - kyua-log@googlegroups.com diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 000000000000..ac0998fb937c --- /dev/null +++ b/AUTHORS @@ -0,0 +1,11 @@ +# This is the official list of Kyua authors for copyright purposes. +# +# This file is distinct from the CONTRIBUTORS files; see the latter for +# an explanation. +# +# Names are sorted alphabetically and should be added to this file as: +# +# * Name +# * Organization + +* Google Inc. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000000..daa55c308e97 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,173 @@ +Contributing code to Kyua +========================= + +Want to contribute? Great! But first, please take a few minutes to read this +document in full. Doing so upfront will minimize the turnaround time required +to get your changes incorporated. + + +Legal notes +----------- + +* Before we can use your code, you must sign the + [Google Individual Contributor License + Agreement](https://developers.google.com/open-source/cla/individual), + also known as the CLA, which you can easily do online. The CLA is necessary + mainly because you own the copyright to your changes, even after your + contribution becomes part of our codebase, so we need your permission to use + and distribute your code. We also need to be sure of various other + things--for instance that you will tell us if you know that your code + infringes on other people's patents. You do not have to sign the CLA until + after you have submitted your code for review and a member has approved it, + but you must do it before we can put your code into our codebase. + +* Contributions made by corporations are covered by a different agreement than + the one above: the + [Google Software Grant and Corporate Contributor License + Agreement](https://developers.google.com/open-source/cla/corporate). + Please get your company to sign this agreement instead if your contribution is + on their behalf. + +* Unless you have a strong reason not to, please assign copyright of your + changes to Google Inc. and use the 3-clause BSD license text included + throughout the codebase (see [LICENSE](LICENSE)). Keeping the whole project + owned by a single entity is important, particularly to avoid the problem of + having to replicate potentially hundreds of different copyright notes in + documentation materials, etc. + + +Communication +------------- + +* Before you start working on a larger contribution, you should get in touch + with us first through the + [kyua-discuss mailing + list](https://groups.google.com/forum/#!forum/kyua-discuss) + with your idea so that we can help out and possibly guide you. Coordinating + upfront makes it much easier to avoid frustration later on. + +* Subscribe to the + [kyua-log mailing list](https://groups.google.com/forum/#!forum/kyua-log) to + get notifications on new commits, Travis CI results, or changes to bugs. + + +Git workflow +------------ + +* Always work on a non-master branch. + +* Make sure the history of your branch is clean. (Ab)use `git rebase -i master` + to ensure the sequence of commits you want pulled is easy to follow and that + every commit does one (and only one) thing. In particular, commits of the + form `Fix previous` or `Fix build` should never ever exist; merge those fixes + into the relevant commits so that the history is clean at pull time. + +* Always trigger Travis CI builds for your changes (hence why working on a + branch is important). Push your branch to GitHub so that Travis CI picks it + up and performs a build. If you have forked the repository, you may need to + enable Travis CI builds on your end. Wait for a green result. + +* It is OK and expected for you to `git push --force` on **non-master** + branches. This is required if you need to go through the commit/test cycle + more than once for any given branch after you have "fixed-up" commits to + correct problems spotted in earlier builds. + +* Do not send pull requests that subsume other/older pull requests. Each major + change being submitted belongs in a different pull request, which is trivial + to achieve if you use one branch per change as requested in this workflow. + + +Code reviews +------------ + +* All changes will be subject to code reviews pre-merge time. In other words: + all pull requests will be carefully inspected before being accepted and they + will be returned to you with comments if there are issues to be fixed. + +* Be careful of stylistic errors in your code (see below for style guidelines). + Style violations hinder the review process and distract from the actual code. + By keeping your code clean of style issues upfront, you will speed up the + review process and avoid frustration along the way. + +* Whenever you are ready to submit a pull request, review the *combined diff* + you are requesting to be pulled and look for issues. This is the diff that + will be subject to review, not necessarily the individual commits. You can + view this diff in GitHub at the bottom of the `Open a pull request` form that + appears when you click the button to file a pull request, or you can see the + diff by typing `git diff master`. + + +Commit messages +--------------- + +* Follow standard Git commit message guidelines. The first line has a maximum + length of 50 characters, does not terminate in a period, and has to summarize + the whole commit. Then a blank line comes, and then multiple plain-text + paragraphs provide details on the commit if necessary with a maximum length of + 72-75 characters per line. Vim has syntax highlighting for Git commit + messages and will let you know when you go above the maximum line lengths. + +* Use the imperative tense. Say `Add foo-bar` or `Fix baz` instead of `Adding + blah`, `Adds bleh`, or `Added bloh`. + + +Handling bug tracker issues +--------------------------- + +* All changes pushed to `master` should cross-reference one or more issues in + the bug tracker. This is particularly important for bug fixes, but also + applies to major feature improvements. + +* Unless you have a good reason to do otherwise, name your branch `issue-N` + where `N` is the number of the issue being fixed. + +* If the fix to the issue can be done *in a single commit*, terminate the commit + message with `Fixes #N.` where `N` is the number of the issue being fixed and + include a note in `NEWS` about the issue in the same commit. Such fixes can + be merged onto master using fast-forward (the default behavior of `git + merge`). + +* If the fix to the issue requires *more than one commit*, do **not** include + `Fixes #N.` in any of the individual commit messages of the branch nor include + any changes to the `NEWS` file in those commits. These "announcement" changes + belong in the merge commit onto `master`, which is done by `git merge --no-ff + --no-commit your-branch`, followed by an edit of `NEWS`, and terminated with a + `git commit -a` with the proper note on the bug being fixed. + + +Style guide +----------- + +These notes are generic and certainly *non-exhaustive*: + +* Respect formatting of existing files. Note where braces are placed, number of + blank lines between code chunks, how continuation lines are indented, how + docstrings are typed, etc. + +* Indentation is *always* done using spaces, not tabs. The only exception is in + `Makefile`s, where any continuation line within a target must be prefixed by a + *single tab*. + +* [Be mindful of spelling and + grammar.](http://julipedia.meroh.net/2013/06/readability-mind-your-typos-and-grammar.html) + Mistakes of this kind are enough of a reason to return a pull request. + +* Use proper punctuation for all sentences. Always start with a capital letter + and terminate with a period. + +* Respect lexicographical sorting wherever possible. + +* Lines must not be over 80 characters. + +* No trailing whitespace. + +* Two spaces after end-of-sentence periods. + +* Two blank lines between functions. If there are two blank lines among code + blocks, they usually exist for a reason: keep them. + +* In C++ code, prefix all C identifiers (those coming from `extern "C"` + includes) with `::`. + +* Getter functions/methods only need to be documented via `\return`. A + redundant summary is not necessary. diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 000000000000..faf726a4fefd --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,20 @@ +# This is the list of people who have agreed to one of the CLAs and can +# contribute patches to the Kyua project. +# +# The AUTHORS file lists the copyright holders; this file lists people. +# For example: Google employees are listed here but not in AUTHORS +# because Google holds the copyright. +# +# See the following links for details on the CLA: +# +# https://developers.google.com/open-source/cla/individual +# https://developers.google.com/open-source/cla/corporate +# +# Names are sorted by last name and should be added as: +# +# * Name + +* Sergey Bronnikov +* Enji Cooper +* Julio Merino +* Craig Rodrigues diff --git a/Doxyfile.in b/Doxyfile.in new file mode 100644 index 000000000000..e28d82f8999a --- /dev/null +++ b/Doxyfile.in @@ -0,0 +1,59 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +BUILTIN_STL_SUPPORT = YES +ENABLE_PREPROCESSING = YES +EXCLUDE_SYMBOLS = "ATF_TC*" +EXTRACT_ANON_NSPACES = YES +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_PRIVATE = YES +EXTRACT_STATIC = YES +EXPAND_ONLY_PREDEF = YES +EXTENSION_MAPPING = ipp = C++ +FILE_PATTERNS = *.c *.h *.cpp *.hpp *.ipp +GENERATE_LATEX = NO +GENERATE_TAGFILE = @top_builddir@/api-docs/api-docs.tag +HIDE_FRIEND_COMPOUNDS = YES +INPUT = @top_srcdir@ +INPUT_ENCODING = ISO-8859-1 +JAVADOC_AUTOBRIEF = YES +MACRO_EXPANSION = YES +OUTPUT_DIRECTORY = @top_builddir@/api-docs +OUTPUT_LANGUAGE = English +PREDEFINED += "KYUA_DEFS_NORETURN=" +PREDEFINED += "KYUA_DEFS_FORMAT_PRINTF(x, y)=" +PROJECT_NAME = "@PACKAGE_NAME@" +PROJECT_NUMBER = @VERSION@ +QUIET = YES +RECURSIVE = YES +SORT_BY_SCOPE_NAME = YES +SORT_MEMBERS_CTORS_1ST = YES +WARN_IF_DOC_ERROR = YES +WARN_IF_UNDOCUMENTED = YES +WARN_NO_PARAMDOC = YES +WARNINGS = YES diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 000000000000..d3dcab49cb74 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,268 @@ +Installation instructions +========================= + +Kyua uses the GNU Automake, GNU Autoconf and GNU Libtool utilities as +its build system. These are used only when compiling the application +from the source code package. If you want to install Kyua from a binary +package, you do not need to read this document. + +For the impatient: + + $ ./configure + $ make + $ make check + Gain root privileges + # make install + Drop root privileges + $ make installcheck + +Or alternatively, install as a regular user into your home directory: + + $ ./configure --prefix ~/local + $ make + $ make check + $ make install + $ make installcheck + + +Dependencies +------------ + +To build and use Kyua successfully you need: + +* A standards-compliant C and C++ complier. +* Lutok 0.4. +* pkg-config. +* SQLite 3.6.22. + +To build the Kyua tests, you optionally need: + +* The Automated Testing Framework (ATF), version 0.15 or greater. This + is required if you want to create a distribution file. + +If you are building Kyua from the code on the repository, you will also +need the following tools: + +* GNU Autoconf. +* GNU Automake. +* GNU Libtool. + + +Regenerating the build system +----------------------------- + +This is not necessary if you are building from a formal release +distribution file. + +On the other hand, if you are building Kyua from code extracted from the +repository, you must first regenerate the files used by the build +system. You will also need to do this if you modify `configure.ac`, +`Makefile.am` or any of the other build system files. To do this, simply +run: + + $ autoreconf -i -s + +If ATF is installed in a different prefix than Autoconf, you will also +need to tell autoreconf where the ATF M4 macros are located. Otherwise, +the configure script will be incomplete and will show confusing syntax +errors mentioning, for example, `ATF_CHECK_SH`. To fix this, you have +to run autoreconf in the following manner, replacing `` with +the appropriate path: + + $ autoreconf -i -s -I /share/aclocal + + +General build procedure +----------------------- + +To build and install the source package, you must follow these steps: + +1. Configure the sources to adapt to your operating system. This is + done using the `configure` script located on the sources' top + directory, and it is usually invoked without arguments unless you + want to change the installation prefix. More details on this + procedure are given on a later section. + +2. Build the sources to generate the binaries and scripts. Simply run + `make` on the sources' top directory after configuring them. No + problems should arise. + +3. Check that the built programs work by running `make check`. You do + not need to be root to do this, but if you are not, some checks will + be skipped. + +4. Install the program by running `make install`. You may need to + become root to issue this step. + +5. Issue any manual installation steps that may be required. These are + described later in their own section. + +6. Check that the installed programs work by running `make + installcheck`. You do not need to be root to do this, but if you are + not, some checks will be skipped. + + +Configuration flags +------------------- + +The most common, standard flags given to `configure` are: + +* `--prefix=directory`: + **Possible values:** Any path. + **Default:** `/usr/local`. + + Specifies where the program (binaries and all associated files) will + be installed. + +* `--sysconfdir=directory`: + **Possible values:** Any path. + **Default:** `/usr/local/etc`. + + Specifies where the installed programs will look for configuration + files. `/kyua` will be appended to the given path unless + `KYUA_CONFSUBDIR` is redefined as explained later on. + +* `--help`: + + Shows information about all available flags and exits immediately, + without running any configuration tasks. + +The following environment variables are specific to Kyua's `configure` +script: + +* `GDB`: + **Possible values:** empty, absolute path to GNU GDB. + **Default:** empty. + + Specifies the path to the GNU GDB binary that Kyua will use to gather a + stack trace of a crashing test program. If empty, the configure script + will try to find a suitable binary for you and, if not found, Kyua will + attempt to do the search at run time. + +* `KYUA_ARCHITECTURE`: + **Possible values:** name of a CPU architecture (e.g. `x86_64`, `powerpc`). + **Default:** autodetected; typically the output of `uname -p`. + + Specifies the name of the CPU architecture on which Kyua will run. + This value is used at run-time to determine tests that are not + applicable to the host system. + +* `KYUA_CONFSUBDIR`: + **Possible values:** empty, a relative path. + **Default:** `kyua`. + + Specifies the subdirectory of the configuration directory (given by + the `--sysconfdir` argument) under which Kyua will search for its + configuration files. + +* `KYUA_CONFIG_FILE_FOR_CHECK`: + **Possible values:** none, an absolute path to an existing file. + **Default:** none. + + Specifies the `kyua.conf` configuration file to use when running any + of the `check`, `installcheck` or `distcheck` targets on this source + tree. This setting is exclusively used to customize the test runs of + Kyua itself and has no effect whatsoever on the built product. + +* `KYUA_MACHINE`: + **Possible values:** name of a machine type (e.g. `amd64`, `macppc`). + **Default:** autodetected; typically the output of `uname -m`. + + Specifies the name of the machine type on which Kyua will run. This + value is used at run-time to determine tests that are not applicable + to the host system. + +* `KYUA_TMPDIR`: + **Possible values:** an absolute path to a temporary directory. + **Default:** `/tmp`. + + Specifies the path that Kyua will use to create temporary directories + in by default. + +The following flags are specific to Kyua's `configure` script: + +* `--enable-developer`: + **Possible values:** `yes`, `no`. + **Default:** `yes` in Git `HEAD` builds; `no` in formal releases. + + Enables several features useful for development, such as the inclusion + of debugging symbols in all objects or the enforcement of compilation + warnings. + + The compiler will be executed with an exhaustive collection of warning + detection features regardless of the value of this flag. However, such + warnings are only fatal when `--enable-developer` is `yes`. + +* `--with-atf`: + **Possible values:** `yes`, `no`, `auto`. + **Default:** `auto`. + + Enables usage of ATF to build (and later install) the tests. + + Setting this to `yes` causes the configure script to look for ATF + unconditionally and abort if not found. Setting this to `auto` lets + configure perform the best decision based on availability of ATF. + Setting this to `no` explicitly disables ATF usage. + + When support for tests is enabled, the build process will generate the + test programs and will later install them into the tests tree. + Running `make check` or `make installcheck` from within the source + directory will cause these tests to be run with Kyua. + +* `--with-doxygen`: + **Possible values:** `yes`, `no`, `auto` or a path. + **Default:** `auto`. + + Enables usage of Doxygen to generate documentation for internal APIs. + This documentation is *not* installed and is only provided to help the + developer of this package. Therefore, enabling or disabling Doxygen + causes absolutely no differences on the files installed by this + package. + + Setting this to `yes` causes the configure script to look for Doxygen + unconditionally and abort if not found. Setting this to `auto` lets + configure perform the best decision based on availability of Doxygen. + Setting this to `no` explicitly disables Doxygen usage. And, lastly, + setting this to a path forces configure to use a specific Doxygen + binary, which must exist. + + +Post-installation steps +----------------------- + +Copy the `Kyuafile.top` file installed in the examples directory to the +root of your tests hierarchy and name it `Kyuafile`. For example: + + # cp /usr/local/share/kyua/examples/Kyuafile.top \ + /usr/local/tests/Kyuafile + +This will allow you to simply go into `/usr/tests` and run the tests +from there. + + +Run the tests! +-------------- + +Lastly, after a successful installation, you should periodically run the +tests from the final location to ensure things remain stable. Do so as +follows: + + $ cd /usr/local/kyua && kyua test + +The following configuration variables are specific to the 'kyua' test +suite and can be given to Kyua with arguments of the form +`-v test_suites.kyua.=`: + +* `run_coredump_tests`: + **Possible values:** `true` or `false`. + **Default:** `true`. + + Avoids running tests that crash subprocesses on purpose to make them + dump core. Such tests are particularly slow on macOS, and it is + sometimes handy to disable them for quicker development iteration. + +If you see any tests fail, do not hesitate to report them in: + + https://github.com/jmmv/kyua/issues/ + +Thank you! diff --git a/Kyuafile b/Kyuafile new file mode 100644 index 000000000000..e986218a45f4 --- /dev/null +++ b/Kyuafile @@ -0,0 +1,18 @@ +syntax(2) + +test_suite("kyua") + +include("bootstrap/Kyuafile") +include("cli/Kyuafile") +if fs.exists("doc/Kyuafile") then + -- The tests for the docs are not installed because they only cover the + -- build-time process of the manual pages. + include("doc/Kyuafile") +end +include("drivers/Kyuafile") +include("engine/Kyuafile") +include("examples/Kyuafile") +include("integration/Kyuafile") +include("model/Kyuafile") +include("store/Kyuafile") +include("utils/Kyuafile") diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..ffb8e3da7d86 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright 2010-2015 The Kyua Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of Google Inc. nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 000000000000..d7f3cd27e73b --- /dev/null +++ b/Makefile.am @@ -0,0 +1,186 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +ACLOCAL_AMFLAGS = -I m4 + +CHECK_BOOTSTRAP_DEPS = +CHECK_KYUA_DEPS = +CHECK_LOCAL = +CLEAN_TARGETS = +DIST_HOOKS = +PHONY_TARGETS = +CLEANFILES = + +EXTRA_DIST = +noinst_DATA = +noinst_LIBRARIES = +noinst_SCRIPTS = + +doc_DATA = AUTHORS CONTRIBUTING.md CONTRIBUTORS LICENSE NEWS.md +noinst_DATA += INSTALL.md README.md +EXTRA_DIST += $(doc_DATA) INSTALL.md README.md + +if WITH_ATF +tests_topdir = $(pkgtestsdir) + +tests_top_DATA = Kyuafile +EXTRA_DIST += $(tests_top_DATA) +endif + +include admin/Makefile.am.inc +include bootstrap/Makefile.am.inc +include cli/Makefile.am.inc +include doc/Makefile.am.inc +include drivers/Makefile.am.inc +include engine/Makefile.am.inc +include examples/Makefile.am.inc +include integration/Makefile.am.inc +include misc/Makefile.am.inc +include model/Makefile.am.inc +include store/Makefile.am.inc +include utils/Makefile.am.inc + +bin_PROGRAMS = kyua +kyua_SOURCES = main.cpp +kyua_CXXFLAGS = $(CLI_CFLAGS) $(ENGINE_CFLAGS) $(UTILS_CFLAGS) +kyua_LDADD = $(CLI_LIBS) $(ENGINE_LIBS) $(UTILS_LIBS) + +CHECK_ENVIRONMENT = KYUA_CONFDIR="/non-existent" \ + KYUA_DOCDIR="$(abs_top_srcdir)" \ + KYUA_EXAMPLESDIR="$(abs_top_srcdir)/examples" \ + KYUA_MISCDIR="$(abs_top_srcdir)/misc" \ + KYUA_STOREDIR="$(abs_top_srcdir)/store" \ + KYUA_STORETESTDATADIR="$(abs_top_srcdir)/store" \ + PATH="$(abs_top_builddir):$${PATH}" +INSTALLCHECK_ENVIRONMENT = KYUA_CONFDIR="/non-existent" \ + PATH="$(prefix)/bin:$${PATH}" + +# Generate local-kyua, a wrapper shell script to run the just-built 'kyua' +# binary by pointing it to the possibly not-yet-installed data files in the +# build tree. +noinst_SCRIPTS += local-kyua +CLEANFILES += local-kyua local-kyua.tmp +local-kyua: Makefile + $(AM_V_GEN)echo '#!/bin/sh' >local-kyua.tmp; \ + echo 'env $(CHECK_ENVIRONMENT) $(TESTS_ENVIRONMENT)' \ + '"$(abs_top_builddir)/kyua" \ + --config='$(KYUA_CONFIG_FILE_FOR_CHECK)' \ + "$${@}"' >>local-kyua.tmp; \ + chmod +x local-kyua.tmp; \ + mv -f local-kyua.tmp local-kyua + +if WITH_ATF +CHECK_LOCAL += dump-ulimits check-kyua +PHONY_TARGETS += check-kyua +check-kyua: $(CHECK_KYUA_DEPS) + @failed=no; \ + ./local-kyua test \ + --kyuafile='$(top_srcdir)/Kyuafile' --build-root='$(top_builddir)' \ + || failed=yes; \ + if [ "$${failed}" = yes ]; then \ + ./local-kyua report --results-file='$(abs_top_srcdir)' \ + --verbose --results-filter=broken,failed; \ + exit 1; \ + fi + +installcheck-local: dump-ulimits installcheck-kyua +PHONY_TARGETS += installcheck-kyua +installcheck-kyua: + @failed=no; \ + cd $(pkgtestsdir) && $(INSTALLCHECK_ENVIRONMENT) $(TESTS_ENVIRONMENT) \ + kyua --config='$(KYUA_CONFIG_FILE_FOR_CHECK)' test \ + || failed=yes; \ + if [ "$${failed}" = yes ]; then \ + cd $(pkgtestsdir) && $(INSTALLCHECK_ENVIRONMENT) \ + $(TESTS_ENVIRONMENT) \ + kyua --config='$(KYUA_CONFIG_FILE_FOR_CHECK)' report \ + --verbose --results-filter=broken,failed; \ + exit 1; \ + fi + +# TODO(jmmv): kyua should probably be recording this information itself as part +# of the execution context, just as we record environment variables. +PHONY_TARGETS += dump-ulimits +dump-ulimits: + @echo "Resource limits:" + @{ \ + ulimit -a | sed -e 's,$$, (soft),'; \ + ulimit -a -H | sed -e 's,$$, (hard),'; \ + } | sort | sed -e 's,^, ,' + @echo +else +DIST_HOOKS += forbid-dist +PHONY_TARGETS += forbid-dist +forbid-dist: + @echo "Sorry; cannot make dist without atf." + @false +endif +check-local: $(CHECK_LOCAL) + +if WITH_DOXYGEN +# Runs doxygen on the source tree and validates the contents of the docstrings. +# We do not do this by default, even if doxygen has been enabled, because this +# step takes a long time. Instead, we just rely on a Travis CI build to catch +# inconsistencies. +PHONY_TARGETS += check-api-docs +check-api-docs: api-docs/api-docs.tag + @$(AWK) -f $(srcdir)/admin/check-api-docs.awk api-docs/doxygen.out + +api-docs/api-docs.tag: $(builddir)/Doxyfile $(SOURCES) + @$(MKDIR_P) api-docs + @rm -f api-docs/doxygen.out api-docs/doxygen.out.tmp + $(AM_V_GEN)$(DOXYGEN) $(builddir)/Doxyfile \ + >api-docs/doxygen.out.tmp 2>&1 && \ + mv api-docs/doxygen.out.tmp api-docs/doxygen.out + +CLEAN_TARGETS += clean-api-docs +clean-api-docs: + rm -rf api-docs +endif + +# Replace Automake's builtin check-news functionality so that we can validate +# the NEWS.md file instead of NEWS. +DIST_HOOKS += check-news +PHONY_TARGETS += check-news +check-news: + @case "$$(sed 15q "$(srcdir)/NEWS.md")" in \ + *"$(VERSION)"*) : ;; \ + *) \ + echo "NEWS.md not updated; not releasing" 1>&2; \ + exit 1 \ + ;; \ + esac + +clean-local: $(CLEAN_TARGETS) +dist-hook: $(DIST_HOOKS) + +PHONY_TARGETS += clean-all +clean-all: + GIT="$(GIT)" $(SH) $(srcdir)/admin/clean-all.sh + +.PHONY: $(PHONY_TARGETS) diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 000000000000..304cfe94695a --- /dev/null +++ b/NEWS.md @@ -0,0 +1,622 @@ +Major changes between releases +============================== + + +Changes in version 0.14 +----------------------- + +**NOT RELEASED YET; STILL UNDER DEVELOPMENT.** + +* Explicitly require C++11 language features when compiling Kyua. + + +Changes in version 0.13 +----------------------- + +**Released on August 26th, 2016.** + +* Fixed execution of test cases as an unprivileged user, at least under + NetBSD 7.0. Kyua-level failures were probably a regression introduced + in Kyua 0.12, but the underlying may have existed for much longer: + test cases might have previously failed for mysterious reasons when + running under an unprivileged user. + +* Issue #134: Fixed metadata test broken on 32-bit platforms. + +* Issue #139: Added per-test case start/end timestamps to all reports. + +* Issue #156: Fixed crashes due to the invalid handling of cleanup + routine data and triggered by the reuse of PIDs in long-running Kyua + instances. + +* Issue #159: Fixed TAP parser to ignore case while matching `TODO` and + `SKIP` directives, and to also recognize `Skipped`. + +* Fixed potential crash due to a race condition in the unprogramming of + timers to control test deadlines. + + +Changes in version 0.12 +----------------------- + +**Released on November 22nd, 2015.** + +This is a huge release and marks a major milestone for Kyua as it finally +implements a long-standing feature request: the ability to execute test +cases in parallel. This is a big deal because test cases are rarely +CPU-bound: running them in parallel yields much faster execution times for +large test suites, allowing faster iteration of changes during development. + +As an example: the FreeBSD test suite as of this date contains 3285 test +cases. With sequential execution, a full test suite run takes around 12 +minutes to complete, whereas on a 4-core machine with a high level of +parallelism it takes a little over 1 minute. + +Implementing parallel execution required rewriting most of Kyua's core and +partly explains explains why there has not been a new release for over a +year. The current implementation is purely subprocess-based, which works +but has some limitations and has resulted in a core that is really complex +and difficult to understand. Future versions will investigate the use of +threads instead for a simplified programming model and additional +parallelization possibilities. + +* Issue #2: Implemented support to execute test cases in parallel when + invoking `kyua test`. Parallel execution is *only* enabled when the new + `parallelism` configuration variable is set to a value greater than `1`. + The default behavior is still to run tests sequentially because some test + suites contain test cases with side-effects that might fail when run in + parallel. To resolve this, the new metadata property `is_exclusive` can + be set to `true` on a test basis to indicate that the test must be run on + its own. + +* Known regression: Running `kyua debug` on a TAP-based test program does + not currently report the output in real time. The output will only be + displayed once the test program completes. This is a shortcoming of + the new parallel execution engine and will be resolved. + +* Removed the external C-based testers code in favor of the new built-in + implementations. The new approach feels significantly faster than the + previous one. + +* Fixed the handling of relative paths in the `fs.*` functions available + in `Kyuafile`s. All paths are now resolved relative to the location of + the caller `Kyuafile`. `Kyuafile.top` has been updated with these + changes and you should update custom copies of this file with the new + version. + +* Changed temporary directory creation to always grant search + permissions on temporary directories. This is to prevent potential + problems when running Kyua as root and executing test cases that require + dropping privileges (as they may later be unable to use absolute paths + that point inside their work directory). + +* The cleanup of work directories does not longer attempt to deal with + mount points. If a test case mounts a file system and forgets to unmount + it, the mount point will be left behind. It is now the responsibility of + the test case to clean after itself. The reasons for this change are + simplicity and clarity: there are many more things that a test case can + do that have side-effects on the system and Kyua cannot protect against + them all, so it is better to just have the test undo anything it might + have done. + +* Improved `kyua report --verbose` to properly handle environment + variables with continuation lines in them, and fixed the integration + tests for this command to avoid false negatives. + +* Changed the configuration file format to accept the definition of + unknown variables without declaring them local. The syntax version + number remains at 2. This is to allow configuration files for newer Kyua + versions to work on older Kyua versions, as there is no reason to forbid + this. + +* Fixed stacktrace gathering with FreeBSD's ancient version of GDB. + GDB 6.1.1 (circa 2004) does not have the `-ex` flag so we need to + generate a temporary GDB script and feed it to GDB with `-x` instead. + +* Issue #136: Fixed the XML escaping in the JUnit output so that + non-printable characters are properly handled when they appear in the + process's stdout or stderr. + +* Issue #141: Improved reporting of errors triggered by sqlite3. In + particular, all error messages are now tagged with their corresponding + database filename and, if they are API-level errors, the name of the + sqlite3 function that caused them. + +* Issue #144: Improved documentation on the support for custom properties + in the test metadata. + +* Converted the `INSTALL`, `NEWS`, and `README` distribution documents to + Markdown for better formatting online. + + +Changes in version 0.11 +----------------------- + +**Released on October 23rd, 2014.** + +* Added support to print the details of all test cases (metadata and + their output) to `report`. This is via a new `--verbose` flag which + replaces the previous `--show-context`. + +* Added support to specify the amount of physical disk space required + by a test case. This is in the form of a new `required_disk_space` + metadata property, which can also be provided by ATF test cases as + `require.diskspace`. + +* Assimilated the contents of all the `kyua-*-tester(1)` and + `kyua-*-interface(7)` manual pages into more relevant places. In + particular, added more details on test program registration and their + metadata to `kyuafile(5)`, and added `kyua-test-isolation(7)` + describing the isolation features of the test execution. + +* Assimilated the contents of all auxiliary manual pages, including + `kyua-build-root(7)`, `kyua-results-files(7)`, `kyua-test-filters(7)` + and `kyua-test-isolation(7)`, into the relevant command-specific + manual pages. This is for easier discoverability of relevant + information when reading how specific Kyua commands work. + +* Issue #30: Plumbed through support to query configuration variables + from ATF's test case heads. This resolves the confusing situation + where test cases could only do this from their body and cleanup + routines. + +* Issue #49: Extended `report` to support test case filters as + command-line arguments. Combined with `--verbose`, this allows + inspecting the details of a test case failure after execution. + +* Issue #55: Deprecated support for specifying `test_suite` overrides on + a test program basis. This idiom should not be used but support for + it remains in place. + +* Issue #72: Added caching support to the `getcwd(3)` test in configure + so that the result can be overriden for cross-compilation purposes. + +* Issue #83: Changed manual page headings to include a `kyua` prefix in + their name. This prevents some possible confusion when displaying, + for example, the `kyua-test` manual page with a plain name of `test`. + +* Issue #84: Started passing test-suite configuration variables to plain + and TAP test programs via the environment. The name of the + environment variables set this way is prefixed by `TEST_ENV_`, so a + configuration variable of the form + `test_suites.some_name.allow_unsafe_ops=yes` in `kyua.conf` becomes + `TEST_ENV_allow_unsafe_ops=YES` in the environment. + +* Issues #97 and #116: Fixed the build on Illumos. + +* Issue #102: Set `TMPDIR` to the test case's work directory when running + the test case. If the test case happens to use the `mktemp(3)` family + of functions (due to misunderstandings on how Kyua works or due to + the reuse of legacy test code), we don't want it to easily escape the + automanaged work directory. + +* Issue #103: Started being more liberal in the parsing of TAP test + results by treating the number in `ok` and `not ok` lines as optional. + +* Issue #105: Started using tmpfs instead of md as a temporary file + system for tests in FreeBSD so that we do not leak `md(4)` devices. + +* Issue #109: Changed the privilege dropping code to start properly + dropping group privileges when `unprivileged_user` is set. Also fixes + `testers/run_test:fork_wait__unprivileged_group`. + +* Issue #110: Changed `help` to display version information and clarified + the purpose of the `about` command in its documentation. + +* Issue #111: Fixed crash when defining a test program in a `Kyuafile` + that has not yet specified the test suite name. + +* Issue #114: Improved the `kyuafile(5)` manual page by clarifying the + restrictions of the `include()` directive and by adding abundant + examples. + + +Changes in version 0.10 +----------------------- + +**Experimental version released on August 14th, 2014.** + +* Merged `kyua-cli` and `kyua-testers` into a single `kyua` package. + +* Dropped the `kyua-atf-compat` package. + +* Issue #100: Do not try to drop privileges to `unprivileged_user` when we + are already running as an unprivileged user. Doing so is not possible + and thus causes spurious test failures when the current user is not + root and the current user and `unprivileged_user` do not match. + +* Issue #79: Mention `kyua.conf(5)` in the *See also* section of `kyua(1)`. + +* Issue #75: Change the `rewrite__expected_signal__bad_arg` test in + `testers/atf_result_test` to use a different signal value. This is to + prevent triggering a core dump that made the test fail in some platforms. + + +Changes in kyua-cli version 0.9 +------------------------------- + +**Experimental version released on August 8th, 2014.** + +Major changes: + +The internal architecture of Kyua to record the results of test suite +runs has completely changed in this release. Kyua no longer stores all +the different test suite run results as different "actions" within the +single `store.db` database. Instead, Kyua now generates a separate +results file inside `~/.kyua/store/` for every test suite run. + +Due to the complexity involved in the migration process and the little +need for it, this is probably going to be the only release where the +`db-migrate` command is able to convert an old `store.db` file to the +new scheme. + +Changes in more detail: + +* Added the `report-junit` command to generate JUnit XML result files. + The output has been verified to work within Jenkins. + +* Switched to results files specific to their corresponding test suite + run. The unified `store.db` file is now gone: `kyua test` creates a + new results file for every invocation under `~/.kyua/store/` and the + `kyua report*` commands are able to locate the latest file for a + corresponding test suite automatically. + +* The `db-migrate` command takes an old `store.db` file and generates + one results file for every previously-recorded action, later deleting + the `store.db` file. + +* The `--action` flag has been removed from all commands that accepted + it. This has been superseded by the tests results files. + +* The `--store` flag that many commands took has been renamed to + `--results-file` in line with the semantical changes. + +* The `db-exec` command no longer creates an empty database when none + is found. This command is now intended to run only over existing + files. + + +Changes in kyua-testers version 0.3 +----------------------------------- + +**Experimental version released on August 8th, 2014.** + +* Made the testers set a "sanitized" value for the `HOME` environment + variable where, for example, consecutive and trailing slashes have + been cleared. Mac OS X has a tendency to append a trailing slash to + the value of `TMPDIR`, which can cause third-party tests to fail if + they compare `${HOME}` with `$(pwd)`. + +* Issues #85, #86, #90 and #92: Made the TAP parser more complete: mark + test cases reported as `TODO` or `SKIP` as passed; handle skip plans; + ignore lines that look like `ok` and `not ok` but aren't results; and + handle test programs that report a pass but exit with a non-zero code. + + +Changes in kyua-cli version 0.8 +------------------------------- + +**Experimental version released on December 7th, 2013.** + +* Added support for Lutok 0.4. + +* Issue #24: Plug the bootstrap tests back into the test suite. Fixes + in `kyua-testers` 0.2 to isolate test cases into their own sessions + should allow these to run fine. + +* Issue #74: Changed the `kyuafile(5)` parser to automatically discover + existing tester interfaces. The various `*_test_program()` functions + will now exist (or not) based on tester availability, which simplifies + the addition of new testers or the selective installation of them. + + +Changes in kyua-testers version 0.2 +----------------------------------- + +**Experimental version released on December 7th, 2013.** + +* Issue #74: Added the `kyua-tap-tester`, a new backend to interact with + test programs that comply with the Test Anything Protocol. + +* Issue #69: Cope with the lack of `AM_PROG_AR` in `configure.ac`, which + first appeared in Automake 1.11.2. Fixes a problem in Ubuntu 10.04 + LTS, which appears stuck in 1.11.1. + +* Issue #24: Improve test case isolation by confining the tests to their + own session instead of just to their own process group. + + +Changes in kyua-cli version 0.7 +------------------------------- + +**Experimental version released on October 18th, 2013.** + +* Made failures from testers more resilent. If a tester fails, the + corresponding test case will be marked as broken instead of causing + kyua to exit. + +* Added the `--results-filter` option to the `report-html` command and + set its default value to skip passed results from HTML reports. This + is to keep these reports more succint and to avoid generating tons of + detail files that will be, in general, useless. + +* Switched to use Lutok 0.3 to gain compatibility with Lua 5.2. + +* Issue #69: Cope with the lack of `AM_PROG_AR` in `configure.ac`, which + first appeared in Automake 1.11.2. Fixes a problem in Ubuntu 10.04 + LTS, which appears stuck in 1.11.1. + + +Changes in kyua-cli version 0.6 +------------------------------- + +**Experimental version released on February 22nd, 2013.** + +* Issue #36: Changed `kyua help` to not fail when the configuration file + is bogus. Help should always work. + +* Issue #37: Simplified the `syntax()` calls in configuration and + `Kyuafile` files to only specify the requested version instead of also + the format name. The format name is implied by the file being loaded, so + there is no use in the caller having to specify it. The version number + of these file formats has been bumped to 2. + +* Issue #39: Added per-test-case metadata values to the HTML reports. + +* Issue #40: Rewrote the documentation as manual pages and removed the + previous GNU Info document. + +* Issue #47: Started using the independent testers in the `kyua-testers` + package to run the test cases. Kyua does not implement the logic to + invoke test cases any more, which provides for better modularity, + extensibility and robustness. + +* Issue #57: Added support to specify arbitrary metadata properties for + test programs right from the `Kyuafile`. This is to make plain test + programs more versatile, by allowing them to specify any of the + requirements (allowed architectures, required files, etc.) supported + by Kyua. + +* Reduced automatic screen line wrapping of messages to the `help` + command and the output of tables by `db-exec`. Wrapping any other + messages (specially anything going to stderr) was very annoying + because it prevented natural copy/pasting of text. + +* Increased the granularity of the error codes returned by `kyua(1)` to + denote different error conditions. This avoids the overload of `1` to + indicate both "expected" errors from specific subcommands and + unexpected errors caused by the internals of the code. The manual now + correctly explain how the exit codes behave on a command basis. + +* Optimized the database schema to make report generation almost + instantaneous. + +* Bumped the database schema to 2. The database now records the + metadata of both test programs and test cases generically, without + knowledge of their interface. + +* Added the `db-migrate` command to provide a mechanism to upgrade a + database with an old schema to the current schema. + +* Removed the GDB build-time configuration variable. This is now part + of the `kyua-testers` package. + +* Issue #31: Rewrote the `Kyuafile` parsing code in C++, which results in + a much simpler implementation. As a side-effect, this gets rid of the + external Lua files required by `kyua`, which in turn make the tool + self-contained. + +* Added caching of various configure test results (particularly in those + tests that need to execute a test program) so that cross-compilers can + predefine the results of the tests without having to run the + executables. + + +Changes in kyua-testers version 0.1 +----------------------------------- + +**Experimental version released on February 19th, 2013.** + +This is the first public release of the `kyua-testers` package. + +The goal of this first release is to adopt all the test case execution +code of `kyua-cli` 0.5 and ship it as a collection of independent tester +binaries. The `kyua-cli` package will rely on these binaries to run the +tests, which provides better modularity and simplicity to the +architecture of Kyua. + +The code in this package is all C as opposed to the current C++ codebase +of `kyua-cli`, which means that the overall build times of Kyua are now +reduced. + + +Changes in kyua-cli version 0.5 +------------------------------- + +**Experimental version released on July 10th, 2012.** + +* Issue #15: Added automatic stacktrace gathering of crashing test cases. + This relies on GDB and is a best-effort operation. + +* Issue #32: Added the `--build-root` option to the debug, list and test + commands. This allows executing test programs from a different + directory than where the `Kyuafile` scripts live. See the *Build roots* + section in the manual for more details. + +* Issue #33: Removed the `kyuaify.sh` script. This has been renamed to + atf2kyua and moved to the `kyua-atf-compat` module, where it ships as a + first-class utility (with a manual page and tests). + +* Issue #34: Changed the HTML reports to include the stdout and stderr of + every test case. + +* Fixed the build when using a "build directory" and a clean source tree + from the repository. + + +Changes in kyua-cli version 0.4 +------------------------------- + +**Experimental version released on June 6th, 2012.** + +* Added the `report-html` command to generate HTML reports of the + execution of any recorded action. + +* Changed the `--output` flag of the `report` command to only take a + path to the target file, not its format. Different formats are better + supported by implementing different subcommands, as the options they + may receive will vary from format to format. + +* Added a `--with-atf` flag to the configure script to control whether + the ATF tests get built or not. May be useful for packaging systems + that do not have ATF in them yet. Disabling ATF also cuts down the + build time of Kyua significantly, but with the obvious drawbacks. + +* Grouped `kyua` subcommands by topic both in the output of `help` and + in the documentation. In general, the user needs to be aware of + commands that rely on a current project and those commands that rely + purely on the database to generate reports. + +* Made `help` print the descriptions of options and commands properly + tabulated. + +* Changed most informational messages to automatically wrap on screen + boundaries. + +* Rewrote the configuration file parsing module for extensibility. This + will allow future versions of Kyua to provide additional user-facing + options in the configuration file. + + No syntax changes have been made, so existing configuration files + (version 1) will continue to be parsed without problems. There is one + little exception though: all variables under the top-level + `test_suites` tree must be declared as strings. + + Similarly, the `-v` and `--variable` flags to the command line must + now carry a `test_suites.` prefix when referencing any variables under + such tree. + + +Changes in kyua-cli version 0.3 +------------------------------- + +**Experimental version released on February 24th, 2012.** + +* Made the `test` command record the results of the executed test + cases into a SQLite database. As a side effect, `test` now supports a + `--store` option to indicate where the database lives. + +* Added the `report` command to generate plain-text reports of the + test results stored in the database. The interface of this command is + certainly subject to change at this point. + +* Added the `db-exec` command to directly interact with the store + database. + +* Issue #28: Added support for the `require.memory` test case property + introduced in ATF 0.15. + +* Renamed the user-specific configuration file from `~/.kyuarc` to + `~/.kyua/kyua.conf` for consistency with other files stored in the + `~/.kyua/` subdirectory. + +* Switched to use Lutok instead of our own wrappers over the Lua C + library. Lutok is just what used to be our own utils::lua module, but + is now distributed separately. + +* Removed the `Atffile`s from the source tree. Kyua is stable enough + to generate trustworthy reports, and we do not want to give the + impression that atf-run / atf-report are still supported. + +* Enabled logging to stderr for our own test programs. This makes it + slightly easier to debug problems in our own code when we get a + failing test. + + +Changes in kyua-cli version 0.2 +------------------------------- + +**Experimental version released on August 24th, 2011.** + +The biggest change in this release is the ability for Kyua to run test +programs implemented using different frameworks. What this means is +that, now, a Kyua test suite can include not only ATF-based test +programs, but also "legacy" (aka plain) test programs that do not use +any framework. I.e. if you have tests that are simple programs that +exit with 0 on success and 1 on failure, you can plug them in into a +Kyua test suite. + +Other than this, there have been several user-visible changes. The most +important are the addition of the new `config` and `debug` subcommands +to the `kyua` binary. The former can be used to inspect the runtime +configuration of Kyua after parsing, and the latter is useful to +interact with failing tests cases in order to get more data about the +failure itself. + +Without further ado, here comes the itemized list of changes: + +* Generalized the run-time engine to support executing test programs + that implement different interfaces. Test programs that use the ATF + libraries are just a special case of this. (Issue #18.) + +* Added support to the engine to run `plain` test programs: i.e. test + programs that do not use any framework and report their pass/fail + status as an exit code. This is to simplify the integration of legacy + test programs into a test suite, and also to demonstrate that the + run-time engine is generic enough to support different test + interfaces. (Issue #18.) + +* Added the `debug` subcommand. This command allows end users to tweak + the execution of a specific test case and to poke into the behavior of + its execution. At the moment, all this command allows is to view the + stdout and stderr of the command in real time (which the `test` + command currently completely hides). + +* Added the `config` subcommand. This command allows the end user to + inspect the current configuration variables after evaluation, without + having to read through configuration files. (Issue #11.) + +* Removed the `test_suites_var` function from configuration files. This + was used to set the value of test-suite-sepecific variables, but it + was ugly-looking. It is now possible to use the more natural syntax + `test_suites.. = `. (Issue #11.) + +* Added a mechanism to disable the loading of configuration files + altogether. Needed for testing purposes and for scriptability. + Available by passing the `--config=none` flag. + +* Enabled detection of unused parameters and variables in the code and + fixed all warnings. (Issue #23.) + +* Changed the behavior of "developer mode". Compiler warnings are now + enabled unconditionally regardless of whether we are in developer mode + or not; developer mode is now only used to perform strict warning + checks and to enable assertions. Additionally, developer mode is now + only automatically enabled when building from the repository, not for + formal releases. (Issue #22.) + +* Fixed many build and portability problems to Debian sid with GCC 4.6.3 + and Ubuntu 10.04.1 LTS. (Issues #20, #21, #26.) + + +Changes in kyua-cli version 0.1 +------------------------------- + +**Experimental version released on June 23rd, 2011.** + +This is the first public release of the `kyua-cli` package. + +The scope of this release is to provide functional replacement for the +`atf-run` utility included in the atf package. At this point, `kyua` +can reliably run the NetBSD 5.99.53 test suite delivering the same +results as `atf-run`. + +The reporting facilities of this release are quite limited. There is +no replacement for `atf-report` yet, and there is no easy way of +debugging failing test programs other than running them by hand. These +features will mark future milestones and therefore be part of other +releases. + +Be aware that this release has suffered very limited field testing. +The test suite for `kyua-cli` is quite comprehensive, but some bugs may +be left in any place. diff --git a/README.md b/README.md new file mode 100644 index 000000000000..eb34c0fd4550 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +Welcome to the Kyua project! +============================ + +Kyua is a **testing framework** for infrastructure software, originally +designed to equip BSD-based operating systems with a test suite. This +means that Kyua is lightweight and simple, and that Kyua integrates well +with various build systems and continuous integration frameworks. + +Kyua features an **expressive test suite definition language**, a **safe +runtime engine** for test suites and a **powerful report generation +engine**. + +Kyua is for **both developers *and* users**, from the developer applying a +simple fix to a library to the system administrator deploying a new release +on a production machine. + +Kyua is **able to execute test programs written with a plethora of testing +libraries and languages**. The library of choice is +[ATF](https://github.com/jmmv/atf/), for which Kyua was originally +designed, but simple, framework-less test programs and TAP-compliant test +programs can also be executed through Kyua. + +Kyua is licensed under a **[liberal BSD 3-clause license](LICENSE)**. +This is not an official Google product. + +[Read more about Kyua in the About wiki page.](../../wiki/About) + + +Download +-------- + +The latest version of Kyua is 0.13 and was released on August 26th, 2016. + +Download: [kyua-0.13](../../releases/tag/kyua-0.13). + +See the [release notes](NEWS.md) for information about the changes in this +and all previous releases. + + +Installation +------------ + +You are encouraged to install binary packages for your operating system +wherever available: + +* Fedora 20 and above: install the `kyua-cli` package with `yum install + kyua-cli`. + +* FreeBSD 10.0 and above: install the `kyua` package with `pkg install kyua`. + +* NetBSD with pkgsrc: install the `pkgsrc/devel/kyua` package. + +* OpenBSD with packages: install the `kyua` package with `pkg_add kyua`. + +* OS X (with Homebrew): install the `kyua` package with `brew install kyua`. + +Should you want to build and install Kyua from the source tree provided +here, follow the instructions in the +[INSTALL.md file](INSTALL.md). + +You should also install the ATF libraries to assist in the development of +test programs. To that end, see the +[ATF project page](https://github.com/jmmv/atf/). + + +Contributing +------------ + +Want to contribute? Great! But please first read the guidelines provided +in [CONTRIBUTING.md](CONTRIBUTING.md). + +If you are curious about who made this project possible, you can check out +the [list of copyright holders](AUTHORS) and the [list of +individuals](CONTRIBUTORS). + + +Support +------- + +Please use the [kyua-discuss mailing +list](https://groups.google.com/forum/#!forum/kyua-discuss) for any support +inquiries. + +*Homepage:* https://github.com/jmmv/kyua/ diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 000000000000..1b34cbb4e096 --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,6 @@ +ar-lib +compile +depcomp +install-sh +mdate-sh +missing diff --git a/admin/Makefile.am.inc b/admin/Makefile.am.inc new file mode 100644 index 000000000000..7d02b0e611c3 --- /dev/null +++ b/admin/Makefile.am.inc @@ -0,0 +1,41 @@ +# Copyright 2015 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +PHONY_TARGETS += check-style +check-style: + @$(srcdir)/admin/check-style.sh \ + -b "$(abs_top_builddir)" \ + -s "$(abs_top_srcdir)" \ + -t "$(PACKAGE_TARNAME)" + +EXTRA_DIST += admin/check-style-common.awk \ + admin/check-style-cpp.awk \ + admin/check-style-make.awk \ + admin/check-style-man.awk \ + admin/check-style-shell.awk \ + admin/check-style.sh diff --git a/admin/build-bintray-dist.sh b/admin/build-bintray-dist.sh new file mode 100755 index 000000000000..99cd439892c5 --- /dev/null +++ b/admin/build-bintray-dist.sh @@ -0,0 +1,131 @@ +#! /bin/sh +# Copyright 2017 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# \file admin/build-bintray-dist.sh +# Builds a full Kyua installation under /usr/local for Ubuntu. +# +# This script is used to create the bintray distribution packages in lieu +# of real Debian packages for Kyua. The result of this script is a +# tarball that provides the contents of /usr/local for Kyua. + +set -e -x + +err() { + echo "${@}" 1>&2 + exit 1 +} + +install_deps() { + sudo apt-get update -qq + + local pkgsuffix= + local packages= + packages="${packages} autoconf" + packages="${packages} automake" + packages="${packages} clang" + packages="${packages} g++" + packages="${packages} gdb" + packages="${packages} git" + packages="${packages} libtool" + packages="${packages} make" + if [ "${ARCH?}" = i386 ]; then + pkgsuffix=:i386 + packages="${packages} gcc-multilib" + packages="${packages} g++-multilib" + fi + packages="${packages} liblua5.2-0${pkgsuffix}" + packages="${packages} liblua5.2-dev${pkgsuffix}" + packages="${packages} libsqlite3-0${pkgsuffix}" + packages="${packages} libsqlite3-dev${pkgsuffix}" + packages="${packages} pkg-config${pkgsuffix}" + packages="${packages} sqlite3" + sudo apt-get install -y ${packages} +} + +install_from_github() { + local name="${1}"; shift + local release="${1}"; shift + + local distname="${name}-${release}" + + local baseurl="https://github.com/jmmv/${name}" + wget --no-check-certificate \ + "${baseurl}/releases/download/${distname}/${distname}.tar.gz" + tar -xzvf "${distname}.tar.gz" + + local archflags= + [ "${ARCH?}" != i386 ] || archflags=-m32 + + cd "${distname}" + ./configure \ + --disable-developer \ + --without-atf \ + --without-doxygen \ + CC="${CC?}" \ + CFLAGS="${archflags}" \ + CPPFLAGS="-I/usr/local/include" \ + CXX="${CXX?}" \ + CXXFLAGS="${archflags}" \ + LDFLAGS="-L/usr/local/lib -Wl,-R/usr/local/lib" \ + PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" + make + sudo make install + cd - + + rm -rf "${distname}" "${distname}.tar.gz" +} + +main() { + [ "${ARCH+set}" = set ] || err "ARCH must be set in the environment" + [ "${CC+set}" = set ] || err "CC must be set in the environment" + [ "${CXX+set}" = set ] || err "CXX must be set in the environment" + + [ ! -f /root/local.tgz ] || err "/root/local.tgz already exists" + tar -czf /root/local.tgz /usr/local + restore() { + rm -rf /usr/local + tar -xz -C / -f /root/local.tgz + rm /root/local.tgz + } + trap restore EXIT + rm -rf /usr/local + mkdir /usr/local + + install_deps + install_from_github atf 0.21 + install_from_github lutok 0.4 + install_from_github kyua 0.13 + + local version="$(lsb_release -rs | cut -d . -f 1-2 | tr . -)" + local name="$(date +%Y%m%d)-usr-local-kyua" + name="${name}-ubuntu-${version}-${ARCH?}-${CC?}.tar.gz" + tar -czf "${name}" /usr/local +} + +main "${@}" diff --git a/admin/check-api-docs.awk b/admin/check-api-docs.awk new file mode 100644 index 000000000000..358e3d54c177 --- /dev/null +++ b/admin/check-api-docs.awk @@ -0,0 +1,72 @@ +#! /bin/sh +# Copyright 2015 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +BEGIN { + failed = 0 +} + +# Skip empty lines. +/^$/ {next} + +# Skip lines that do not directly reference a file. +/^[^\/]/ {next} + +# Ignore known problems. As far as I can tell, all the cases listed here are +# well-documented in the code but Doxygen fails, for some reason or another, to +# properly locate the docstrings. +/engine\/kyuafile\.cpp.*no matching class member/ {next} +/engine\/scheduler\.hpp.*Member setup\(void\).*friend/ {next} +/engine\/scheduler\.hpp.*Member wait_any\(void\)/ {next} +/utils\/optional\.ipp.*no matching file member/ {next} +/utils\/optional\.hpp.*Member make_optional\(const T &\)/ {next} +/utils\/config\/nodes\.hpp.*Member set_lua\(lutok::state &, const int\)/ {next} +/utils\/config\/nodes\.hpp.*Member push_lua\(lutok::state &\)/ {next} +/utils\/config\/nodes\.hpp.*Member set_string\(const std::string &\)/ {next} +/utils\/config\/nodes\.hpp.*Member to_string\(void\)/ {next} +/utils\/config\/nodes\.hpp.*Member is_set\(void\)/ {next} +/utils\/process\/executor\.hpp.*Member spawn\(Hook.*\)/ {next} +/utils\/process\/executor\.hpp.*Member spawn_followup\(Hook.*\)/ {next} +/utils\/process\/executor\.hpp.*Member setup\(void\).*friend/ {next} +/utils\/signals\/timer\.hpp.*Member detail::invoke_do_fired.*friend/ {next} +/utils\/stacktrace_test\.cpp.*no matching class member/ {next} + +# Dump any other problems and account for the failure. +{ + failed = 1 + print +} + +END { + if (failed) { + print "ERROR: Unexpected docstring problems encountered" + exit 1 + } else { + exit 0 + } +} diff --git a/admin/check-style-common.awk b/admin/check-style-common.awk new file mode 100644 index 000000000000..39516d00d4e5 --- /dev/null +++ b/admin/check-style-common.awk @@ -0,0 +1,79 @@ +# Copyright 2015 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +{ + if (skip) + next + + if (length > 80 && NF > 1) + warn("Line too long to fit on screen") +} + +/^ *\t+/ { + if (! match(FILENAME, "Makefile")) + warn("Tab character used for indentation"); +} + +/[ \t]+$/ { + warn("Trailing spaces or tabs"); +} + +/^#![^ ]/ { + warn("Missing space after #!"); +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/admin/check-style-cpp.awk b/admin/check-style-cpp.awk new file mode 100644 index 000000000000..126789ca9262 --- /dev/null +++ b/admin/check-style-cpp.awk @@ -0,0 +1,87 @@ +# Copyright 2015 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +{ + if (skip) + next +} + +/#ifdef/ { + warn("Undesired usage of #ifdef; use #if defined()") +} + +/#ifndef/ { + warn("Undesired usage of #ifndef; use #if !defined()") +} + +/assert[ \t]*\(/ { + warn("Use the macros in sanity.hpp instead of assert"); +} + +/#.*include.*assert/ { + warn("Do not include assert.h nor cassert"); +} + +/std::endl/ { + warn("Use \\n instead of std::endl"); +} + +/\/\*/ && ! /\*\// { + warn("Do not use multi-line C-style comments"); +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/admin/check-style-make.awk b/admin/check-style-make.awk new file mode 100644 index 000000000000..9a6c532e7131 --- /dev/null +++ b/admin/check-style-make.awk @@ -0,0 +1,71 @@ +# Copyright 2015 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +{ + if (skip) + next +} + +/^\t *\t/ { + warn("Continuation lines must use a single tab"); +} + +/mkdir.*-p/ { + warn("Use $(MKDIR_P) instead of mkdir -p"); +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/admin/check-style-man.awk b/admin/check-style-man.awk new file mode 100644 index 000000000000..5c4a2c261b96 --- /dev/null +++ b/admin/check-style-man.awk @@ -0,0 +1,71 @@ +# Copyright 2015 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE|^\.Bd/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE|^\.Ed/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +/^\.\\"/ { + next +} + +{ + if (skip) + next +} + +/\.\.|e\.g\.|i\.e\./ { + next +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/admin/check-style-shell.awk b/admin/check-style-shell.awk new file mode 100644 index 000000000000..43d3472cb45b --- /dev/null +++ b/admin/check-style-shell.awk @@ -0,0 +1,95 @@ +# Copyright 2015 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +function warn(msg) { + print FILENAME "[" FNR "]: " msg > "/dev/stderr" + error = 1 +} + +BEGIN { + skip = 0 + error = 0 +} + +/CHECK_STYLE_DISABLE/ { + skip = 1 + next +} + +/CHECK_STYLE_ENABLE/ { + skip = 0 + next +} + +/CHECK_STYLE_(ENABLE|DISABLE)/ { + next +} + +{ + if (skip) + next +} + +/^[ \t]*#/ { + next +} + +/[$ \t]+_[a-zA-Z0-9]+=/ { + warn("Variable should not start with an underline") +} + +/[^\\]\$[^0-9!'"$?@#*{}(|\/,]+/ { + warn("Missing braces around variable name") +} + +/=(""|'')/ { + warn("Assignment to the empty string does not need quotes"); +} + +/basename[ \t]+/ { + warn("Use parameter expansion instead of basename"); +} + +/if[ \t]+(test|![ \t]+test)/ { + warn("Use [ instead of test"); +} + +/[ \t]+(test|\[).*==/ { + warn("test(1)'s == operator is not portable"); +} + +/if.*;[ \t]*fi$/ { + warn("Avoid using a single-line if conditional"); +} + +END { + if (skip) + warn("Missing CHECK_STYLE_ENABLE"); + if (error) + exit 1 +} diff --git a/admin/check-style.sh b/admin/check-style.sh new file mode 100755 index 000000000000..696f9247a74a --- /dev/null +++ b/admin/check-style.sh @@ -0,0 +1,170 @@ +#! /bin/sh +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# \file admin/check-style.sh +# +# Sanity checks the coding style of all source files in the project tree. + +ProgName="${0##*/}" + + +# Prints an error message and exits. +# +# \param ... Parts of the error message; concatenated using a space as the +# separator. +err() { + echo "${ProgName}:" "${@}" 1>&2 + exit 1 +} + + +# Locates all source files within the project directory. +# +# We require the project to have been configured in a directory that is separate +# from the source tree. This is to allow us to easily filter out build +# artifacts from our search. +# +# \param srcdir Absolute path to the source directory. +# \param builddir Absolute path to the build directory. +# \param tarname Basename of the project's tar file, to skip possible distfile +# directories. +find_sources() { + local srcdir="${1}"; shift + local builddir="${1}"; shift + local tarname="${1}"; shift + + ( + cd "${srcdir}" + find . -type f -a \ + \! -path "*/.git/*" \ + \! -path "*/.deps/*" \ + \! -path "*/autom4te.cache/*" \ + \! -path "*/${tarname}-[0-9]*/*" \ + \! -path "*/${builddir##*/}/*" \ + \! -name "Makefile.in" \ + \! -name "aclocal.m4" \ + \! -name "config.h.in" \ + \! -name "configure" \ + \! -name "testsuite" + ) +} + + +# Prints the style rules applicable to a given file. +# +# \param file Path to the source file. +guess_rules() { + local file="${1}"; shift + + case "${file}" in + */ax_cxx_compile_stdcxx.m4) ;; + */ltmain.sh) ;; + *Makefile*) echo common make ;; + *.[0-9]) echo common man ;; + *.cpp|*.hpp) echo common cpp ;; + *.sh) echo common shell ;; + *) echo common ;; + esac +} + + +# Validates a given file against the rules that apply to it. +# +# \param srcdir Absolute path to the source directory. +# \param file Name of the file to validate relative to srcdir. +# +# \return 0 if the file is valid; 1 otherwise, in which case the style +# violations are printed to the output. +check_file() { + local srcdir="${1}"; shift + local file="${1}"; shift + + local err=0 + for rule in $(guess_rules "${file}"); do + awk -f "${srcdir}/admin/check-style-${rule}.awk" \ + "${srcdir}/${file}" || err=1 + done + + return ${err} +} + + +# Entry point. +main() { + local builddir=. + local srcdir=. + local tarname=UNKNOWN + + local arg + while getopts :b:s:t: arg; do + case "${arg}" in + b) + builddir="${OPTARG}" + ;; + + s) + srcdir="${OPTARG}" + ;; + + t) + tarname="${OPTARG}" + ;; + + \?) + err "Unknown option -${OPTARG}" + ;; + esac + done + shift $(expr ${OPTIND} - 1) + + srcdir="$(cd "${srcdir}" && pwd -P)" + builddir="$(cd "${builddir}" && pwd -P)" + [ "${srcdir}" != "${builddir}" ] || \ + err "srcdir and builddir cannot match; reconfigure the package" \ + "in a separate directory" + + local sources + if [ ${#} -gt 0 ]; then + sources="${@}" + else + sources="$(find_sources "${srcdir}" "${builddir}" "${tarname}")" + fi + + local ok=0 + for file in ${sources}; do + local file="$(echo ${file} | sed -e "s,\\./,,")" + + check_file "${srcdir}" "${file}" || ok=1 + done + + return "${ok}" +} + + +main "${@}" diff --git a/admin/clean-all.sh b/admin/clean-all.sh new file mode 100755 index 000000000000..bc02f1e811f4 --- /dev/null +++ b/admin/clean-all.sh @@ -0,0 +1,90 @@ +#! /bin/sh +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Prog_Name=${0##*/} + +if [ ! -f ./main.cpp ]; then + echo "${Prog_Name}: must be run from the source top directory" 1>&2 + exit 1 +fi + +if [ ! -f configure ]; then + echo "${Prog_Name}: configure not found; nothing to clean?" 1>&2 + exit 1 +fi + +[ -f Makefile ] || ./configure +make distclean + +# Top-level directory. +rm -f Makefile.in +rm -f aclocal.m4 +rm -rf autom4te.cache +rm -f config.h.in +rm -f configure +rm -f mkinstalldirs +rm -f kyua-*.tar.gz + +# admin directory. +rm -f admin/ar-lib +rm -f admin/compile +rm -f admin/config.guess +rm -f admin/config.sub +rm -f admin/depcomp +rm -f admin/install-sh +rm -f admin/ltmain.sh +rm -f admin/mdate-sh +rm -f admin/missing + +# bootstrap directory. +rm -f bootstrap/package.m4 +rm -f bootstrap/testsuite + +# doc directory. +rm -f doc/*.info +rm -f doc/stamp-vti +rm -f doc/version.texi + +# m4 directory. +rm -f m4/libtool.m4 +rm -f m4/lt*.m4 + +# Files and directories spread all around the tree. +find . -name '#*' | xargs rm -rf +find . -name '*~' | xargs rm -rf +find . -name .deps | xargs rm -rf +find . -name .gdb_history | xargs rm -rf +find . -name .libs | xargs rm -rf +find . -name .tmp | xargs rm -rf + +# Show remaining files. +if [ -n "${GIT}" ]; then + echo ">>> untracked and ignored files" + "${GIT}" status --porcelain --ignored | grep -E '^(\?\?|!!)' || true +fi diff --git a/admin/travis-build.sh b/admin/travis-build.sh new file mode 100755 index 000000000000..e69f271c13f1 --- /dev/null +++ b/admin/travis-build.sh @@ -0,0 +1,98 @@ +#! /bin/sh +# Copyright 2014 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set -e -x + +run_autoreconf() { + if [ -d /usr/local/share/aclocal ]; then + autoreconf -isv -I/usr/local/share/aclocal + else + autoreconf -isv + fi +} + +do_apidocs() { + run_autoreconf || return 1 + ./configure --with-doxygen || return 1 + make check-api-docs +} + +do_distcheck() { + run_autoreconf || return 1 + ./configure || return 1 + + sudo sysctl -w "kernel.core_pattern=core.%p" + + local archflags= + [ "${ARCH?}" != i386 ] || archflags=-m32 + + cat >kyua.conf <>kyua.conf + + local f= + f="${f} CFLAGS='${archflags}'" + f="${f} CPPFLAGS='-I/usr/local/include'" + f="${f} CXXFLAGS='${archflags}'" + f="${f} LDFLAGS='-L/usr/local/lib -Wl,-R/usr/local/lib'" + f="${f} PKG_CONFIG_PATH='/usr/local/lib/pkgconfig'" + f="${f} KYUA_CONFIG_FILE_FOR_CHECK=$(pwd)/kyua.conf" + if [ "${AS_ROOT:-no}" = yes ]; then + sudo -H PATH="${PATH}" make distcheck DISTCHECK_CONFIGURE_FLAGS="${f}" + else + make distcheck DISTCHECK_CONFIGURE_FLAGS="${f}" + fi +} + +do_style() { + run_autoreconf || return 1 + mkdir build + cd build + ../configure || return 1 + make check-style +} + +main() { + if [ -z "${DO}" ]; then + echo "DO must be defined" 1>&2 + exit 1 + fi + for step in ${DO}; do + "do_${DO}" || exit 1 + done +} + +main "${@}" diff --git a/admin/travis-install-deps.sh b/admin/travis-install-deps.sh new file mode 100755 index 000000000000..9341c43895b1 --- /dev/null +++ b/admin/travis-install-deps.sh @@ -0,0 +1,83 @@ +#! /bin/sh +# Copyright 2014 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set -e -x + +install_deps() { + local pkgsuffix= + local packages= + if [ "${ARCH?}" = i386 ]; then + pkgsuffix=:i386 + packages="${packages} gcc-multilib" + packages="${packages} g++-multilib" + sudo dpkg --add-architecture i386 + fi + packages="${packages} gdb" + packages="${packages} liblua5.2-0${pkgsuffix}" + packages="${packages} liblua5.2-dev${pkgsuffix}" + packages="${packages} libsqlite3-0${pkgsuffix}" + packages="${packages} libsqlite3-dev${pkgsuffix}" + packages="${packages} pkg-config${pkgsuffix}" + packages="${packages} sqlite3" + sudo apt-get update -qq + sudo apt-get install -y ${packages} +} + +install_kyua() { + local name="20190321-usr-local-kyua-ubuntu-16-04-${ARCH?}-${CC?}.tar.gz" + wget -O "${name}" "http://dl.bintray.com/ngie-eign/kyua/${name}" || return 1 + sudo tar -xzvp -C / -f "${name}" + rm -f "${name}" +} + +do_apidocs() { + sudo apt-get install -y doxygen +} + +do_distcheck() { + : +} + +do_style() { + : +} + +main() { + if [ -z "${DO}" ]; then + echo "DO must be defined" 1>&2 + exit 1 + fi + install_deps + install_kyua + for step in ${DO}; do + "do_${DO}" || exit 1 + done +} + +main "${@}" diff --git a/bootstrap/.gitignore b/bootstrap/.gitignore new file mode 100644 index 000000000000..effaef8e6b4a --- /dev/null +++ b/bootstrap/.gitignore @@ -0,0 +1,4 @@ +atconfig +package.m4 +testsuite +testsuite.log diff --git a/bootstrap/Kyuafile b/bootstrap/Kyuafile new file mode 100644 index 000000000000..0f161b2d66eb --- /dev/null +++ b/bootstrap/Kyuafile @@ -0,0 +1,5 @@ +syntax(2) + +test_suite("kyua") + +plain_test_program{name="testsuite"} diff --git a/bootstrap/Makefile.am.inc b/bootstrap/Makefile.am.inc new file mode 100644 index 000000000000..0dbf26002ce9 --- /dev/null +++ b/bootstrap/Makefile.am.inc @@ -0,0 +1,90 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +if WITH_ATF +tests_bootstrapdir = $(pkgtestsdir)/bootstrap + +tests_bootstrap_DATA = bootstrap/Kyuafile +EXTRA_DIST += $(tests_bootstrap_DATA) + +DISTCLEANFILES = bootstrap/atconfig \ + bootstrap/testsuite.lineno \ + bootstrap/testsuite.log + +distclean-local: distclean-testsuite +distclean-testsuite: + -rm -rf bootstrap/testsuite.dir + +EXTRA_DIST += bootstrap/Kyuafile \ + bootstrap/testsuite \ + bootstrap/package.m4 \ + bootstrap/testsuite.at + +tests_bootstrap_PROGRAMS = bootstrap/atf_helpers +bootstrap_atf_helpers_SOURCES = bootstrap/atf_helpers.cpp +bootstrap_atf_helpers_CXXFLAGS = $(ATF_CXX_CFLAGS) +bootstrap_atf_helpers_LDADD = $(ATF_CXX_LIBS) + +tests_bootstrap_PROGRAMS += bootstrap/plain_helpers +bootstrap_plain_helpers_SOURCES = bootstrap/plain_helpers.cpp +bootstrap_plain_helpers_CXXFLAGS = $(UTILS_CFLAGS) + +tests_bootstrap_SCRIPTS = bootstrap/testsuite +@target_srcdir@bootstrap/package.m4: $(top_srcdir)/configure.ac + $(AM_V_GEN){ \ + echo '# Signature of the current package.'; \ + echo 'm4_define(AT_PACKAGE_NAME, @PACKAGE_NAME@)'; \ + echo 'm4_define(AT_PACKAGE_TARNAME, @PACKAGE_TARNAME@)'; \ + echo 'm4_define(AT_PACKAGE_VERSION, @PACKAGE_VERSION@)'; \ + echo 'm4_define(AT_PACKAGE_STRING, @PACKAGE_STRING@)'; \ + echo 'm4_define(AT_PACKAGE_BUGREPORT, @PACKAGE_BUGREPORT@)'; \ + } >$(srcdir)/bootstrap/package.m4 + +@target_srcdir@bootstrap/testsuite: $(srcdir)/bootstrap/testsuite.at \ + @target_srcdir@bootstrap/package.m4 + $(AM_V_GEN)autom4te --language=Autotest -I $(srcdir) \ + -I $(srcdir)/bootstrap \ + $(srcdir)/bootstrap/testsuite.at -o $@.tmp; \ + mv $@.tmp $@ + +CHECK_LOCAL += check-bootstrap +PHONY_TARGETS += check-bootstrap +check-bootstrap: @target_srcdir@bootstrap/testsuite $(check_PROGRAMS) \ + $(CHECK_BOOTSTRAP_DEPS) + cd bootstrap && $(CHECK_ENVIRONMENT) $(TESTS_ENVIRONMENT) \ + ./testsuite + +if !TARGET_SRCDIR_EMPTY +CHECK_BOOTSTRAP_DEPS += copy-bootstrap-testsuite +CHECK_KYUA_DEPS += copy-bootstrap-testsuite +PHONY_TARGETS += copy-bootstrap-testsuite +copy-bootstrap-testsuite: + cp -f @target_srcdir@bootstrap/testsuite bootstrap/testsuite +CLEANFILES += bootstrap/testsuite +endif +endif diff --git a/bootstrap/atf_helpers.cpp b/bootstrap/atf_helpers.cpp new file mode 100644 index 000000000000..6a31b4ced994 --- /dev/null +++ b/bootstrap/atf_helpers.cpp @@ -0,0 +1,71 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(fails); +ATF_TEST_CASE_BODY(fails) +{ + fail("Failed on purpose"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(passes); +ATF_TEST_CASE_BODY(passes) +{ +} + + +ATF_TEST_CASE_WITHOUT_HEAD(skips); +ATF_TEST_CASE_BODY(skips) +{ + skip("Skipped on purpose"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + std::string enabled; + + const char* tests = std::getenv("TESTS"); + if (tests == NULL) + enabled = "fails passes skips"; + else + enabled = tests; + + if (enabled.find("fails") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, fails); + if (enabled.find("passes") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, passes); + if (enabled.find("skips") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, skips); +} diff --git a/bootstrap/plain_helpers.cpp b/bootstrap/plain_helpers.cpp new file mode 100644 index 000000000000..7de629a99d4d --- /dev/null +++ b/bootstrap/plain_helpers.cpp @@ -0,0 +1,141 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include +#include + +#include "utils/defs.hpp" +#include "utils/test_utils.ipp" + + +namespace { + + +/// Prints a fake but valid test case list and then aborts. +/// +/// \param argv The original arguments of the program. +/// +/// \return Nothing because this dies before returning. +static int +helper_abort_test_cases_list(int /* argc */, char** argv) +{ + for (const char* const* arg = argv; *arg != NULL; arg++) { + if (std::strcmp(*arg, "-l") == 0) { + std::cout << "Content-Type: application/X-atf-tp; " + "version=\"1\"\n\n"; + std::cout << "ident: foo\n"; + } + } + utils::abort_without_coredump(); +} + + +/// Just returns without printing anything as the test case list. +/// +/// \return Always 0, as required for test programs. +static int +helper_empty_test_cases_list(int /* argc */, char** /* argv */) +{ + return EXIT_SUCCESS; +} + + +/// Prints a correctly-formatted test case list but empty. +/// +/// \param argv The original arguments of the program. +/// +/// \return Always 0, as required for test programs. +static int +helper_zero_test_cases(int /* argc */, char** argv) +{ + for (const char* const* arg = argv; *arg != NULL; arg++) { + if (std::strcmp(*arg, "-l") == 0) + std::cout << "Content-Type: application/X-atf-tp; " + "version=\"1\"\n\n"; + } + return EXIT_SUCCESS; +} + + +/// Mapping of the name of a helper to its implementation. +struct helper { + /// The name of the helper, as will be provided by the user on the CLI. + const char* name; + + /// A pointer to the function implementing the helper. + int (*hook)(int, char**); +}; + + +/// NULL-terminated table mapping helper names to their implementations. +static const helper helpers[] = { + { "abort_test_cases_list", helper_abort_test_cases_list, }, + { "empty_test_cases_list", helper_empty_test_cases_list, }, + { "zero_test_cases", helper_zero_test_cases, }, + { NULL, NULL, }, +}; + + +} // anonymous namespace + + +/// Entry point to the ATF-less helpers. +/// +/// The caller must select a helper to execute by defining the HELPER +/// environment variable to the name of the desired helper. Think of this main +/// method as a subprogram dispatcher, to avoid having many individual helper +/// binaries. +/// +/// \todo Maybe we should really have individual helper binaries. It would +/// avoid a significant amount of complexity here and in the tests, at the +/// expense of some extra files and extra build logic. +/// +/// \param argc The user argument count; delegated to the helper. +/// \param argv The user arguments; delegated to the helper. +/// +/// \return The exit code of the helper, which depends on the requested helper. +int +main(int argc, char** argv) +{ + const char* command = std::getenv("HELPER"); + if (command == NULL) { + std::cerr << "Usage error: HELPER must be set to a helper name\n"; + std::exit(EXIT_FAILURE); + } + + const struct helper* iter = helpers; + for (; iter->name != NULL && std::strcmp(iter->name, command) != 0; iter++) + ; + if (iter->name == NULL) { + std::cerr << "Usage error: unknown command " << command << "\n"; + std::exit(EXIT_FAILURE); + } + + return iter->hook(argc, argv); +} diff --git a/bootstrap/testsuite.at b/bootstrap/testsuite.at new file mode 100644 index 000000000000..10200a67a5ca --- /dev/null +++ b/bootstrap/testsuite.at @@ -0,0 +1,200 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +AT_INIT([bootstrapping tests]) + + +m4_define([GUESS_TOPDIR], { + old=$(pwd) + cd "$(dirname ${as_myself})" + # We need to locate a build product, not a source file, because the + # test suite may be run outside of the source tree (think distcheck). + while test $(pwd) != '/' -a ! -e bootstrap/plain_helpers; do + cd .. + done + topdir=$(pwd) + cd ${old} + echo ${topdir} +}) + + +m4_define([CREATE_ATF_HELPERS], [ + AT_DATA([Kyuafile], [ +syntax(2) +test_suite("bootstrap") +atf_test_program{name="atf_helpers"} +]) + ln -s $(GUESS_TOPDIR)/bootstrap/atf_helpers atf_helpers +]) +m4_define([RUN_ATF_HELPERS], + [HOME=$(pwd) TESTS="$1" kyua --config=none \ + test --results-file=bootstrap.db $2]) + + +m4_define([CREATE_PLAIN_HELPERS], [ + AT_DATA([Kyuafile], [ +syntax(2) +test_suite("bootstrap") +atf_test_program{name="plain_helpers"} +]) + ln -s $(GUESS_TOPDIR)/bootstrap/plain_helpers plain_helpers +]) +m4_define([RUN_PLAIN_HELPER], + [HOME=$(pwd) HELPER="$1" kyua --config=none \ + test --results-file=bootstrap.db]) + + +AT_SETUP([test program crashes in test list]) +AT_TESTED([kyua]) + +CREATE_PLAIN_HELPERS +AT_CHECK([RUN_PLAIN_HELPER([abort_test_cases_list])], [1], [stdout], []) +re='plain_helpers:__test_cases_list__.*broken.*Test program received signal' +AT_CHECK([grep "${re}" stdout], [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([test program prints an empty test list]) +AT_TESTED([kyua]) + +CREATE_PLAIN_HELPERS +AT_CHECK([RUN_PLAIN_HELPER([empty_test_cases_list])], [1], [stdout], []) +re="plain_helpers:__test_cases_list__.*broken.*Invalid header.*got ''" +AT_CHECK([grep "${re}" stdout], [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([test program with zero test cases]) +AT_TESTED([kyua]) + +CREATE_PLAIN_HELPERS +AT_CHECK([RUN_PLAIN_HELPER([zero_test_cases])], [1], [stdout], []) +re='plain_helpers:__test_cases_list__.*broken.*No test cases' +AT_CHECK([grep "${re}" stdout], [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([run test case that passes]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([passes])], [0], [stdout], []) +AT_CHECK([grep "atf_helpers:fails" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips" stdout], [1], [], []) + +AT_CLEANUP + + +AT_SETUP([run test case that fails]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([fails])], [1], [stdout], []) +AT_CHECK([grep "atf_helpers:fails.*failed.*Failed on purpose" stdout], + [0], [ignore], []) +AT_CHECK([grep "atf_helpers:passes" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:skips" stdout], [1], [], []) + +AT_CLEANUP + + +AT_SETUP([run test case that skips]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([skips])], [0], [stdout], []) +AT_CHECK([grep "atf_helpers:fails" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:passes" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:skips.*skipped.*Skipped on purpose" stdout], + [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([run two test cases, success]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([passes skips])], [0], [stdout], []) +AT_CHECK([grep "atf_helpers:fails" stdout], [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips.*skipped.*Skipped on purpose" stdout], + [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([run two test cases, failure]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([fails passes])], [1], [stdout], []) +AT_CHECK([grep "atf_helpers:fails.*failure.*Failed on purpose" stdout], + [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips" stdout], [1], [], []) + +AT_CLEANUP + + +AT_SETUP([run mixed test cases]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([RUN_ATF_HELPERS([fails passes skips])], [1], [stdout], []) +AT_CHECK([grep "atf_helpers:fails.*failure.*Failed on purpose" stdout], + [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips.*skipped.*Skipped on purpose" stdout], + [0], [ignore], []) + +AT_CLEANUP + + +AT_SETUP([run tests from build directories]) +AT_TESTED([kyua]) + +CREATE_ATF_HELPERS +AT_CHECK([mkdir src], [0], [], []) +AT_CHECK([mv Kyuafile src], [0], [], []) +AT_CHECK([mkdir obj], [0], [], []) +AT_CHECK([mv atf_helpers obj], [0], [], []) +AT_CHECK([RUN_ATF_HELPERS([fails passes skips], + [--kyuafile=src/Kyuafile --build-root=obj])], + [1], [stdout], []) +AT_CHECK([grep "atf_helpers:fails.*failure.*Failed on purpose" stdout], + [1], [], []) +AT_CHECK([grep "atf_helpers:passes.*passed" stdout], [0], [ignore], []) +AT_CHECK([grep "atf_helpers:skips.*skipped.*Skipped on purpose" stdout], + [0], [ignore], []) + +AT_CLEANUP diff --git a/cli/Kyuafile b/cli/Kyuafile new file mode 100644 index 000000000000..f5b797d760c3 --- /dev/null +++ b/cli/Kyuafile @@ -0,0 +1,14 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="cmd_about_test"} +atf_test_program{name="cmd_config_test"} +atf_test_program{name="cmd_db_exec_test"} +atf_test_program{name="cmd_debug_test"} +atf_test_program{name="cmd_help_test"} +atf_test_program{name="cmd_list_test"} +atf_test_program{name="cmd_test_test"} +atf_test_program{name="common_test"} +atf_test_program{name="config_test"} +atf_test_program{name="main_test"} diff --git a/cli/Makefile.am.inc b/cli/Makefile.am.inc new file mode 100644 index 000000000000..27872088a1b7 --- /dev/null +++ b/cli/Makefile.am.inc @@ -0,0 +1,123 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +CLI_CFLAGS = $(DRIVERS_CFLAGS) +CLI_LIBS = libcli.a $(DRIVERS_LIBS) + +noinst_LIBRARIES += libcli.a +libcli_a_SOURCES = cli/cmd_about.cpp +libcli_a_SOURCES += cli/cmd_about.hpp +libcli_a_SOURCES += cli/cmd_config.cpp +libcli_a_SOURCES += cli/cmd_config.hpp +libcli_a_SOURCES += cli/cmd_db_exec.cpp +libcli_a_SOURCES += cli/cmd_db_exec.hpp +libcli_a_SOURCES += cli/cmd_db_migrate.cpp +libcli_a_SOURCES += cli/cmd_db_migrate.hpp +libcli_a_SOURCES += cli/cmd_debug.cpp +libcli_a_SOURCES += cli/cmd_debug.hpp +libcli_a_SOURCES += cli/cmd_help.cpp +libcli_a_SOURCES += cli/cmd_help.hpp +libcli_a_SOURCES += cli/cmd_list.cpp +libcli_a_SOURCES += cli/cmd_list.hpp +libcli_a_SOURCES += cli/cmd_report.cpp +libcli_a_SOURCES += cli/cmd_report.hpp +libcli_a_SOURCES += cli/cmd_report_html.cpp +libcli_a_SOURCES += cli/cmd_report_html.hpp +libcli_a_SOURCES += cli/cmd_report_junit.cpp +libcli_a_SOURCES += cli/cmd_report_junit.hpp +libcli_a_SOURCES += cli/cmd_test.cpp +libcli_a_SOURCES += cli/cmd_test.hpp +libcli_a_SOURCES += cli/common.cpp +libcli_a_SOURCES += cli/common.hpp +libcli_a_SOURCES += cli/common.ipp +libcli_a_SOURCES += cli/config.cpp +libcli_a_SOURCES += cli/config.hpp +libcli_a_SOURCES += cli/main.cpp +libcli_a_SOURCES += cli/main.hpp +libcli_a_CPPFLAGS = -DKYUA_CONFDIR="\"$(kyua_confdir)\"" +libcli_a_CPPFLAGS += -DKYUA_DOCDIR="\"$(docdir)\"" +libcli_a_CPPFLAGS += -DKYUA_MISCDIR="\"$(miscdir)\"" +libcli_a_CPPFLAGS += $(DRIVERS_CFLAGS) +libcli_a_LIBADD = libutils.a + +if WITH_ATF +tests_clidir = $(pkgtestsdir)/cli + +tests_cli_DATA = cli/Kyuafile +EXTRA_DIST += $(tests_cli_DATA) + +tests_cli_PROGRAMS = cli/cmd_about_test +cli_cmd_about_test_SOURCES = cli/cmd_about_test.cpp +cli_cmd_about_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_about_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_config_test +cli_cmd_config_test_SOURCES = cli/cmd_config_test.cpp +cli_cmd_config_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_config_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_db_exec_test +cli_cmd_db_exec_test_SOURCES = cli/cmd_db_exec_test.cpp +cli_cmd_db_exec_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_db_exec_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_debug_test +cli_cmd_debug_test_SOURCES = cli/cmd_debug_test.cpp +cli_cmd_debug_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_debug_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_help_test +cli_cmd_help_test_SOURCES = cli/cmd_help_test.cpp +cli_cmd_help_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_help_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_list_test +cli_cmd_list_test_SOURCES = cli/cmd_list_test.cpp +cli_cmd_list_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_list_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_test_test +cli_cmd_test_test_SOURCES = cli/cmd_test_test.cpp +cli_cmd_test_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_test_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/common_test +cli_common_test_SOURCES = cli/common_test.cpp +cli_common_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_common_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/config_test +cli_config_test_SOURCES = cli/config_test.cpp +cli_config_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_config_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/main_test +cli_main_test_SOURCES = cli/main_test.cpp +cli_main_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_main_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/cli/cmd_about.cpp b/cli/cmd_about.cpp new file mode 100644 index 000000000000..f2b3f99e0ada --- /dev/null +++ b/cli/cmd_about.cpp @@ -0,0 +1,160 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_about.hpp" + +#include +#include +#include +#include + +#include "cli/common.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" +#include "utils/text/regex.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace text = utils::text; + +using cli::cmd_about; + + +namespace { + + +/// Print the contents of a document. +/// +/// If the file cannot be opened for whatever reason, an error message is +/// printed to the output of the program instead of the contents of the file. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param file The file to print. +/// \param filter_re Regular expression to match the lines to print. If empty, +/// no filtering is applied. +/// +/// \return True if the file was printed, false otherwise. +static bool +cat_file(cmdline::ui* ui, const fs::path& file, + const std::string& filter_re = "") +{ + std::ifstream input(file.c_str()); + if (!input) { + ui->err(F("Failed to open %s") % file); + return false; + } + + std::string line; + if (filter_re.empty()) { + while (std::getline(input, line).good()) { + ui->out(line); + } + } else { + const text::regex filter = text::regex::compile(filter_re, 0); + while (std::getline(input, line).good()) { + if (filter.match(line)) { + ui->out(line); + } + } + } + input.close(); + return true; +} + + +} // anonymous namespace + + +/// Default constructor for cmd_about. +cmd_about::cmd_about(void) : cli_command( + "about", "[authors|license|version]", 0, 1, + "Shows detailed authors and contributors; license; and version information") +{ +} + + +/// Entry point for the "about" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if any of the necessary documents cannot be +/// opened. +int +cmd_about::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + const fs::path docdir(utils::getenv_with_default( + "KYUA_DOCDIR", KYUA_DOCDIR)); + + bool success = true; + + static const char* list_re = "^\\* "; + + if (cmdline.arguments().empty()) { + ui->out(PACKAGE " (" PACKAGE_NAME ") " PACKAGE_VERSION); + ui->out(""); + ui->out("License terms:"); + ui->out(""); + success &= cat_file(ui, docdir / "LICENSE"); + ui->out(""); + ui->out("Brought to you by:"); + ui->out(""); + success &= cat_file(ui, docdir / "AUTHORS", list_re); + ui->out(""); + success &= cat_file(ui, docdir / "CONTRIBUTORS", list_re); + ui->out(""); + ui->out(F("Homepage: %s") % PACKAGE_URL); + } else { + const std::string& topic = cmdline.arguments()[0]; + + if (topic == "authors") { + success &= cat_file(ui, docdir / "AUTHORS", list_re); + success &= cat_file(ui, docdir / "CONTRIBUTORS", list_re); + } else if (topic == "license") { + success &= cat_file(ui, docdir / "LICENSE"); + } else if (topic == "version") { + write_version_header(ui); + } else { + throw cmdline::usage_error(F("Invalid about topic '%s'") % topic); + } + } + + return success ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/cli/cmd_about.hpp b/cli/cmd_about.hpp new file mode 100644 index 000000000000..2d1ed57a498b --- /dev/null +++ b/cli/cmd_about.hpp @@ -0,0 +1,57 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_about.hpp +/// Provides the cmd_about class. + +#if !defined(CLI_CMD_ABOUT_HPP) +#define CLI_CMD_ABOUT_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "about" subcommand. +class cmd_about : public cli_command +{ + /// Path to the directory containing the distribution documents. + const std::string _docdir; + +public: + cmd_about(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_ABOUT_HPP) diff --git a/cli/cmd_about_test.cpp b/cli/cmd_about_test.cpp new file mode 100644 index 000000000000..da75db3b3871 --- /dev/null +++ b/cli/cmd_about_test.cpp @@ -0,0 +1,306 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_about.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace fs = utils::fs; + +using cli::cmd_about; + + +ATF_TEST_CASE_WITHOUT_HEAD(all_topics__ok); +ATF_TEST_CASE_BODY(all_topics__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + + fs::mkdir(fs::path("fake-docs"), 0755); + atf::utils::create_file("fake-docs/AUTHORS", + "Content of AUTHORS\n" + "* First author\n" + " * garbage\n" + "* Second author\n"); + atf::utils::create_file("fake-docs/CONTRIBUTORS", + "Content of CONTRIBUTORS\n" + "* First contributor\n" + " * garbage\n" + "* Second contributor\n"); + atf::utils::create_file("fake-docs/LICENSE", "Content of LICENSE\n"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_VERSION, ui.out_log()[0])); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of AUTHORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First author", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second author", ui.out_log())); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of CONTRIBUTORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First contributor", + ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second contributor", + ui.out_log())); + + ATF_REQUIRE(atf::utils::grep_collection("Content of LICENSE", + ui.out_log())); + + ATF_REQUIRE(atf::utils::grep_collection("Homepage", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_topics__missing_docs); +ATF_TEST_CASE_BODY(all_topics__missing_docs) +{ + cmdline::args_vector args; + args.push_back("about"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_VERSION, ui.out_log()[0])); + + ATF_REQUIRE(atf::utils::grep_collection("Homepage", ui.out_log())); + + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*AUTHORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*CONTRIBUTORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*LICENSE", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_authors__ok); +ATF_TEST_CASE_BODY(topic_authors__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("authors"); + + fs::mkdir(fs::path("fake-docs"), 0755); + atf::utils::create_file("fake-docs/AUTHORS", + "Content of AUTHORS\n" + "* First author\n" + " * garbage\n" + "* Second author\n"); + atf::utils::create_file("fake-docs/CONTRIBUTORS", + "Content of CONTRIBUTORS\n" + "* First contributor\n" + " * garbage\n" + "* Second contributor\n"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(!atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of AUTHORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First author", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second author", ui.out_log())); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of CONTRIBUTORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First contributor", + ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second contributor", + ui.out_log())); + + ATF_REQUIRE(!atf::utils::grep_collection("LICENSE", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Homepage", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_authors__missing_doc); +ATF_TEST_CASE_BODY(topic_authors__missing_doc) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("authors"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE_EQ(0, ui.out_log().size()); + + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*AUTHORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*CONTRIBUTORS", + ui.err_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Failed to open.*LICENSE", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_license__ok); +ATF_TEST_CASE_BODY(topic_license__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("license"); + + fs::mkdir(fs::path("fake-docs"), 0755); + atf::utils::create_file("fake-docs/LICENSE", "Content of LICENSE\n"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(!atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(!atf::utils::grep_collection("AUTHORS", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("CONTRIBUTORS", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("Content of LICENSE", + ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Homepage", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_license__missing_doc); +ATF_TEST_CASE_BODY(topic_license__missing_doc) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("license"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE_EQ(0, ui.out_log().size()); + + ATF_REQUIRE(!atf::utils::grep_collection("Failed to open.*AUTHORS", + ui.err_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Failed to open.*CONTRIBUTORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*LICENSE", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_version__ok); +ATF_TEST_CASE_BODY(topic_version__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("version"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_VERSION, ui.out_log()[0])); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_args); +ATF_TEST_CASE_BODY(invalid_args) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("first"); + args.push_back("second"); + + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Too many arguments", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_topic); +ATF_TEST_CASE_BODY(invalid_topic) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("foo"); + + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Invalid about topic 'foo'", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, all_topics__ok); + ATF_ADD_TEST_CASE(tcs, all_topics__missing_docs); + ATF_ADD_TEST_CASE(tcs, topic_authors__ok); + ATF_ADD_TEST_CASE(tcs, topic_authors__missing_doc); + ATF_ADD_TEST_CASE(tcs, topic_license__ok); + ATF_ADD_TEST_CASE(tcs, topic_license__missing_doc); + ATF_ADD_TEST_CASE(tcs, topic_version__ok); + ATF_ADD_TEST_CASE(tcs, invalid_args); + ATF_ADD_TEST_CASE(tcs, invalid_topic); +} diff --git a/cli/cmd_config.cpp b/cli/cmd_config.cpp new file mode 100644 index 000000000000..947449aacc2d --- /dev/null +++ b/cli/cmd_config.cpp @@ -0,0 +1,122 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_config.hpp" + +#include + +#include "cli/common.ipp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_config; + + +namespace { + + +/// Prints all configuration variables. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param properties The key/value map representing all the configuration +/// variables. +/// +/// \return 0 for success. +static int +print_all(cmdline::ui* ui, const config::properties_map& properties) +{ + for (config::properties_map::const_iterator iter = properties.begin(); + iter != properties.end(); iter++) + ui->out(F("%s = %s") % (*iter).first % (*iter).second); + return EXIT_SUCCESS; +} + + +/// Prints the configuration variables that the user requests. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param properties The key/value map representing all the configuration +/// variables. +/// \param filters The names of the configuration variables to print. +/// +/// \return 0 if all specified filters are valid; 1 otherwise. +static int +print_some(cmdline::ui* ui, const config::properties_map& properties, + const cmdline::args_vector& filters) +{ + bool ok = true; + + for (cmdline::args_vector::const_iterator iter = filters.begin(); + iter != filters.end(); iter++) { + const config::properties_map::const_iterator match = + properties.find(*iter); + if (match == properties.end()) { + cmdline::print_warning(ui, F("'%s' is not defined.") % *iter); + ok = false; + } else + ui->out(F("%s = %s") % (*match).first % (*match).second); + } + + return ok ? EXIT_SUCCESS : EXIT_FAILURE; +} + + +} // anonymous namespace + + +/// Default constructor for cmd_config. +cmd_config::cmd_config(void) : cli_command( + "config", "[variable1 .. variableN]", 0, -1, + "Inspects the values of configuration variables") +{ +} + + +/// Entry point for the "config" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime configuration of the program. +/// +/// \return 0 if everything is OK, 1 if any of the necessary documents cannot be +/// opened. +int +cmd_config::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + const config::properties_map properties = user_config.all_properties(); + if (cmdline.arguments().empty()) + return print_all(ui, properties); + else + return print_some(ui, properties, cmdline.arguments()); +} diff --git a/cli/cmd_config.hpp b/cli/cmd_config.hpp new file mode 100644 index 000000000000..42f5abd90c28 --- /dev/null +++ b/cli/cmd_config.hpp @@ -0,0 +1,54 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_config.hpp +/// Provides the cmd_config class. + +#if !defined(CLI_CMD_CONFIG_HPP) +#define CLI_CMD_CONFIG_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "config" subcommand. +class cmd_config : public cli_command +{ +public: + cmd_config(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_CONFIG_HPP) diff --git a/cli/cmd_config_test.cpp b/cli/cmd_config_test.cpp new file mode 100644 index 000000000000..f084f99bb90a --- /dev/null +++ b/cli/cmd_config_test.cpp @@ -0,0 +1,144 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_config.hpp" + +#include + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" +#include "utils/optional.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_config; +using utils::none; + + +namespace { + + +/// Instantiates a fake user configuration for testing purposes. +/// +/// The user configuration is populated with a collection of test-suite +/// properties and some hardcoded values for the generic configuration options. +/// +/// \return A new user configuration object. +static config::tree +fake_config(void) +{ + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "the-architecture"); + user_config.set_string("parallelism", "128"); + user_config.set_string("platform", "the-platform"); + //user_config.set_string("unprivileged_user", ""); + user_config.set_string("test_suites.foo.bar", "first"); + user_config.set_string("test_suites.foo.baz", "second"); + return user_config; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(all); +ATF_TEST_CASE_BODY(all) +{ + cmdline::args_vector args; + args.push_back("config"); + + cmd_config cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); + + ATF_REQUIRE_EQ(5, ui.out_log().size()); + ATF_REQUIRE_EQ("architecture = the-architecture", ui.out_log()[0]); + ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[1]); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[2]); + ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[3]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[4]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some__ok); +ATF_TEST_CASE_BODY(some__ok) +{ + cmdline::args_vector args; + args.push_back("config"); + args.push_back("platform"); + args.push_back("test_suites.foo.baz"); + + cmd_config cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); + + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[0]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[1]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some__fail); +ATF_TEST_CASE_BODY(some__fail) +{ + cmdline::args_vector args; + args.push_back("config"); + args.push_back("platform"); + args.push_back("unknown"); + args.push_back("test_suites.foo.baz"); + + cmdline::init("progname"); + + cmd_config cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, fake_config())); + + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[0]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[1]); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE(atf::utils::grep_string("unknown.*not defined", + ui.err_log()[0])); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, all); + ATF_ADD_TEST_CASE(tcs, some__ok); + ATF_ADD_TEST_CASE(tcs, some__fail); +} diff --git a/cli/cmd_db_exec.cpp b/cli/cmd_db_exec.cpp new file mode 100644 index 000000000000..54304e6643de --- /dev/null +++ b/cli/cmd_db_exec.cpp @@ -0,0 +1,200 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_db_exec.hpp" + +#include +#include +#include +#include +#include + +#include "cli/common.ipp" +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "store/read_backend.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace sqlite = utils::sqlite; + +using cli::cmd_db_exec; + + +namespace { + + +/// Concatenates a vector into a string using ' ' as a separator. +/// +/// \param args The objects to join. This cannot be empty. +/// +/// \return The concatenation of all the objects in the set. +static std::string +flatten_args(const cmdline::args_vector& args) +{ + std::ostringstream output; + std::copy(args.begin(), args.end(), + std::ostream_iterator< std::string >(output, " ")); + + std::string result = output.str(); + result.erase(result.end() - 1); + return result; +} + + +} // anonymous namespace + + +/// Formats a particular cell of a statement result. +/// +/// \param stmt The statement whose cell to format. +/// \param index The index of the cell to format. +/// +/// \return A textual representation of the cell. +std::string +cli::format_cell(sqlite::statement& stmt, const int index) +{ + switch (stmt.column_type(index)) { + case sqlite::type_blob: { + const sqlite::blob blob = stmt.column_blob(index); + return F("BLOB of %s bytes") % blob.size; + } + + case sqlite::type_float: + return F("%s") % stmt.column_double(index); + + case sqlite::type_integer: + return F("%s") % stmt.column_int64(index); + + case sqlite::type_null: + return "NULL"; + + case sqlite::type_text: + return stmt.column_text(index); + } + + UNREACHABLE; +} + + +/// Formats the column names of a statement for output as CSV. +/// +/// \param stmt The statement whose columns to format. +/// +/// \return A comma-separated list of column names. +std::string +cli::format_headers(sqlite::statement& stmt) +{ + std::string output; + int i = 0; + for (; i < stmt.column_count() - 1; ++i) + output += stmt.column_name(i) + ','; + output += stmt.column_name(i); + return output; +} + + +/// Formats a row of a statement for output as CSV. +/// +/// \param stmt The statement whose current row to format. +/// +/// \return A comma-separated list of values. +std::string +cli::format_row(sqlite::statement& stmt) +{ + std::string output; + int i = 0; + for (; i < stmt.column_count() - 1; ++i) + output += cli::format_cell(stmt, i) + ','; + output += cli::format_cell(stmt, i); + return output; +} + + +/// Default constructor for cmd_db_exec. +cmd_db_exec::cmd_db_exec(void) : cli_command( + "db-exec", "sql_statement", 1, -1, + "Executes an arbitrary SQL statement in a results file and prints " + "the resulting table") +{ + add_option(results_file_open_option); + add_option(cmdline::bool_option("no-headers", "Do not show headers in the " + "output table")); +} + + +/// Entry point for the "db-exec" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_db_exec::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + try { + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + // TODO(jmmv): Shouldn't be using store::detail here... + sqlite::database db = store::detail::open_and_setup( + results_file, sqlite::open_readwrite); + sqlite::statement stmt = db.create_statement( + flatten_args(cmdline.arguments())); + + if (stmt.step()) { + if (!cmdline.has_option("no-headers")) + ui->out(cli::format_headers(stmt)); + do + ui->out(cli::format_row(stmt)); + while (stmt.step()); + } + + return EXIT_SUCCESS; + } catch (const sqlite::error& e) { + cmdline::print_error(ui, F("SQLite error: %s.") % e.what()); + return EXIT_FAILURE; + } catch (const store::error& e) { + cmdline::print_error(ui, F("%s.") % e.what()); + return EXIT_FAILURE; + } +} diff --git a/cli/cmd_db_exec.hpp b/cli/cmd_db_exec.hpp new file mode 100644 index 000000000000..18aa16108553 --- /dev/null +++ b/cli/cmd_db_exec.hpp @@ -0,0 +1,61 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_db_exec.hpp +/// Provides the cmd_db_exec class. + +#if !defined(CLI_CMD_DB_EXEC_HPP) +#define CLI_CMD_DB_EXEC_HPP + +#include + +#include "cli/common.hpp" +#include "utils/sqlite/statement_fwd.hpp" + +namespace cli { + + +std::string format_cell(utils::sqlite::statement&, const int); +std::string format_headers(utils::sqlite::statement&); +std::string format_row(utils::sqlite::statement&); + + +/// Implementation of the "db-exec" subcommand. +class cmd_db_exec : public cli_command +{ +public: + cmd_db_exec(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + +#endif // !defined(CLI_CMD_DB_EXEC_HPP) diff --git a/cli/cmd_db_exec_test.cpp b/cli/cmd_db_exec_test.cpp new file mode 100644 index 000000000000..1bf6b2e074a9 --- /dev/null +++ b/cli/cmd_db_exec_test.cpp @@ -0,0 +1,165 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_db_exec.hpp" + +#include + +#include + +#include "utils/format/macros.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/statement.ipp" + +namespace sqlite = utils::sqlite; + + +namespace { + + +/// Performs a test for the cli::format_cell() function. +/// +/// \tparam Cell The type of the value to insert into the test column. +/// \param column_type The SQL type of the test column. +/// \param value The value to insert into the test column. +/// \param exp_value The expected return value of cli::format_cell(). +template< class Cell > +static void +do_format_cell_test(const std::string column_type, + const Cell& value, const std::string& exp_value) +{ + sqlite::database db = sqlite::database::in_memory(); + + sqlite::statement create = db.create_statement( + F("CREATE TABLE test (column %s)") % column_type); + create.step_without_results(); + + sqlite::statement insert = db.create_statement( + "INSERT INTO test (column) VALUES (:column)"); + insert.bind(":column", value); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + ATF_REQUIRE(query.step()); + ATF_REQUIRE_EQ(exp_value, cli::format_cell(query, 0)); + ATF_REQUIRE(!query.step()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__blob); +ATF_TEST_CASE_BODY(format_cell__blob) +{ + const char* contents = "Some random contents"; + do_format_cell_test( + "BLOB", sqlite::blob(contents, std::strlen(contents)), + F("BLOB of %s bytes") % strlen(contents)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__float); +ATF_TEST_CASE_BODY(format_cell__float) +{ + do_format_cell_test("FLOAT", 3.5, "3.5"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__integer); +ATF_TEST_CASE_BODY(format_cell__integer) +{ + do_format_cell_test("INTEGER", 123456, "123456"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__null); +ATF_TEST_CASE_BODY(format_cell__null) +{ + do_format_cell_test("TEXT", sqlite::null(), "NULL"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__text); +ATF_TEST_CASE_BODY(format_cell__text) +{ + do_format_cell_test("TEXT", "Hello, world", "Hello, world"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_headers); +ATF_TEST_CASE_BODY(format_headers) +{ + sqlite::database db = sqlite::database::in_memory(); + + sqlite::statement create = db.create_statement( + "CREATE TABLE test (c1 TEXT, c2 TEXT, c3 TEXT)"); + create.step_without_results(); + + sqlite::statement query = db.create_statement( + "SELECT c1, c2, c3 AS c3bis FROM test"); + ATF_REQUIRE_EQ("c1,c2,c3bis", cli::format_headers(query)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_row); +ATF_TEST_CASE_BODY(format_row) +{ + sqlite::database db = sqlite::database::in_memory(); + + sqlite::statement create = db.create_statement( + "CREATE TABLE test (c1 TEXT, c2 BLOB)"); + create.step_without_results(); + + const char* memory = "BLOB contents"; + sqlite::statement insert = db.create_statement( + "INSERT INTO test VALUES (:v1, :v2)"); + insert.bind(":v1", "A string"); + insert.bind(":v2", sqlite::blob(memory, std::strlen(memory))); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + query.step(); + ATF_REQUIRE_EQ( + (F("A string,BLOB of %s bytes") % std::strlen(memory)).str(), + cli::format_row(query)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, format_cell__blob); + ATF_ADD_TEST_CASE(tcs, format_cell__float); + ATF_ADD_TEST_CASE(tcs, format_cell__integer); + ATF_ADD_TEST_CASE(tcs, format_cell__null); + ATF_ADD_TEST_CASE(tcs, format_cell__text); + + ATF_ADD_TEST_CASE(tcs, format_headers); + + ATF_ADD_TEST_CASE(tcs, format_row); +} diff --git a/cli/cmd_db_migrate.cpp b/cli/cmd_db_migrate.cpp new file mode 100644 index 000000000000..c6076c6afa4d --- /dev/null +++ b/cli/cmd_db_migrate.cpp @@ -0,0 +1,82 @@ +// Copyright 2013 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_db_migrate.hpp" + +#include + +#include "cli/common.ipp" +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "store/migrate.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace layout = store::layout; + +using cli::cmd_db_migrate; + + +/// Default constructor for cmd_db_migrate. +cmd_db_migrate::cmd_db_migrate(void) : cli_command( + "db-migrate", "", 0, 0, + "Upgrades the schema of an existing results file to the currently " + "implemented version. A backup of the results file is created, but " + "this operation is not reversible") +{ + add_option(results_file_open_option); +} + + +/// Entry point for the "db-migrate" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_db_migrate::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + try { + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + store::migrate_schema(results_file); + return EXIT_SUCCESS; + } catch (const store::error& e) { + cmdline::print_error(ui, F("Migration failed: %s.") % e.what()); + return EXIT_FAILURE; + } +} diff --git a/cli/cmd_db_migrate.hpp b/cli/cmd_db_migrate.hpp new file mode 100644 index 000000000000..ebbe2b8a4ba4 --- /dev/null +++ b/cli/cmd_db_migrate.hpp @@ -0,0 +1,54 @@ +// Copyright 2013 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_db_migrate.hpp +/// Provides the cmd_db_migrate class. + +#if !defined(CLI_CMD_DB_MIGRATE_HPP) +#define CLI_CMD_DB_MIGRATE_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "db-migrate" subcommand. +class cmd_db_migrate : public cli_command +{ +public: + cmd_db_migrate(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_DB_MIGRATE_HPP) diff --git a/cli/cmd_debug.cpp b/cli/cmd_debug.cpp new file mode 100644 index 000000000000..b7a29b7ab804 --- /dev/null +++ b/cli/cmd_debug.cpp @@ -0,0 +1,94 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_debug.hpp" + +#include + +#include "cli/common.ipp" +#include "drivers/debug_test.hpp" +#include "engine/filters.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/format/macros.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_debug; + + +/// Default constructor for cmd_debug. +cmd_debug::cmd_debug(void) : cli_command( + "debug", "test_case", 1, 1, + "Executes a single test case providing facilities for debugging") +{ + add_option(build_root_option); + add_option(kyuafile_option); + + add_option(cmdline::path_option( + "stdout", "Where to direct the standard output of the test case", + "path", "/dev/stdout")); + + add_option(cmdline::path_option( + "stderr", "Where to direct the standard error of the test case", + "path", "/dev/stderr")); +} + + +/// Entry point for the "debug" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime debuguration of the program. +/// +/// \return 0 if everything is OK, 1 if any of the necessary documents cannot be +/// opened. +int +cmd_debug::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + const std::string& test_case_name = cmdline.arguments()[0]; + if (test_case_name.find(':') == std::string::npos) + throw cmdline::usage_error(F("'%s' is not a test case identifier " + "(missing ':'?)") % test_case_name); + const engine::test_filter filter = engine::test_filter::parse( + test_case_name); + + const drivers::debug_test::result result = drivers::debug_test::drive( + kyuafile_path(cmdline), build_root_path(cmdline), filter, user_config, + cmdline.get_option< cmdline::path_option >("stdout"), + cmdline.get_option< cmdline::path_option >("stderr")); + + ui->out(F("%s -> %s") % cli::format_test_case_id(result.test_case) % + cli::format_result(result.test_result)); + + return result.test_result.good() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/cli/cmd_debug.hpp b/cli/cmd_debug.hpp new file mode 100644 index 000000000000..2d9e8dee1797 --- /dev/null +++ b/cli/cmd_debug.hpp @@ -0,0 +1,54 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_debug.hpp +/// Provides the cmd_debug class. + +#if !defined(CLI_CMD_DEBUG_HPP) +#define CLI_CMD_DEBUG_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "debug" subcommand. +class cmd_debug : public cli_command +{ +public: + cmd_debug(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_DEBUG_HPP) diff --git a/cli/cmd_debug_test.cpp b/cli/cmd_debug_test.cpp new file mode 100644 index 000000000000..28137e028962 --- /dev/null +++ b/cli/cmd_debug_test.cpp @@ -0,0 +1,82 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_debug.hpp" + +#include + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_filter); +ATF_TEST_CASE_BODY(invalid_filter) +{ + cmdline::args_vector args; + args.push_back("debug"); + args.push_back("incorrect:"); + + cli::cmd_debug cmd; + cmdline::ui_mock ui; + // TODO(jmmv): This error should really be cmdline::usage_error. + ATF_REQUIRE_THROW_RE(std::runtime_error, "Test case.*'incorrect:'.*empty", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filter_without_test_case); +ATF_TEST_CASE_BODY(filter_without_test_case) +{ + cmdline::args_vector args; + args.push_back("debug"); + args.push_back("program"); + + cli::cmd_debug cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::error, "'program'.*not a test case", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, invalid_filter); + ATF_ADD_TEST_CASE(tcs, filter_without_test_case); +} diff --git a/cli/cmd_help.cpp b/cli/cmd_help.cpp new file mode 100644 index 000000000000..9ebe6f50c852 --- /dev/null +++ b/cli/cmd_help.cpp @@ -0,0 +1,250 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_help.hpp" + +#include +#include + +#include "cli/common.ipp" +#include "utils/cmdline/commands_map.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" +#include "utils/text/table.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace text = utils::text; + +using cli::cmd_help; + + +namespace { + + +/// Creates a table with the help of a set of options. +/// +/// \param options The set of options to describe. May be empty. +/// +/// \return A 2-column wide table with the description of the options. +static text::table +options_help(const cmdline::options_vector& options) +{ + text::table table(2); + + for (cmdline::options_vector::const_iterator iter = options.begin(); + iter != options.end(); iter++) { + const cmdline::base_option* option = *iter; + + std::string description = option->description(); + if (option->needs_arg() && option->has_default_value()) + description += F(" (default: %s)") % option->default_value(); + + text::table_row row; + + if (option->has_short_name()) + row.push_back(F("%s, %s") % option->format_short_name() % + option->format_long_name()); + else + row.push_back(F("%s") % option->format_long_name()); + row.push_back(F("%s.") % description); + + table.add_row(row); + } + + return table; +} + + +/// Prints the summary of commands and generic options. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param options The set of program-wide options for which to print help. +/// \param commands The set of commands for which to print help. +static void +general_help(cmdline::ui* ui, const cmdline::options_vector* options, + const cmdline::commands_map< cli::cli_command >* commands) +{ + PRE(!commands->empty()); + + cli::write_version_header(ui); + ui->out(""); + ui->out_tag_wrap( + "Usage: ", + F("%s [general_options] command [command_options] [args]") % + cmdline::progname(), false); + + const text::table options_table = options_help(*options); + text::widths_vector::value_type first_width = + options_table.column_width(0); + + std::map< std::string, text::table > command_tables; + + for (cmdline::commands_map< cli::cli_command >::const_iterator + iter = commands->begin(); iter != commands->end(); iter++) { + const std::string& category = (*iter).first; + const std::set< std::string >& command_names = (*iter).second; + + command_tables.insert(std::map< std::string, text::table >::value_type( + category, text::table(2))); + text::table& table = command_tables.find(category)->second; + + for (std::set< std::string >::const_iterator i2 = command_names.begin(); + i2 != command_names.end(); i2++) { + const cli::cli_command* command = commands->find(*i2); + text::table_row row; + row.push_back(command->name()); + row.push_back(F("%s.") % command->short_description()); + table.add_row(row); + } + + if (table.column_width(0) > first_width) + first_width = table.column_width(0); + } + + text::table_formatter formatter; + formatter.set_column_width(0, first_width); + formatter.set_column_width(1, text::table_formatter::width_refill); + formatter.set_separator(" "); + + if (!options_table.empty()) { + ui->out_wrap(""); + ui->out_wrap("Available general options:"); + ui->out_table(options_table, formatter, " "); + } + + // Iterate using the same loop as above to preserve ordering. + for (cmdline::commands_map< cli::cli_command >::const_iterator + iter = commands->begin(); iter != commands->end(); iter++) { + const std::string& category = (*iter).first; + ui->out_wrap(""); + ui->out_wrap(F("%s commands:") % + (category.empty() ? "Generic" : category)); + ui->out_table(command_tables.find(category)->second, formatter, " "); + } + + ui->out_wrap(""); + ui->out_wrap("See kyua(1) for more details."); +} + + +/// Prints help for a particular subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param general_options The options that apply to all commands. +/// \param command Pointer to the command to describe. +static void +subcommand_help(cmdline::ui* ui, + const utils::cmdline::options_vector* general_options, + const cli::cli_command* command) +{ + cli::write_version_header(ui); + ui->out(""); + ui->out_tag_wrap( + "Usage: ", F("%s [general_options] %s%s%s") % + cmdline::progname() % command->name() % + (command->options().empty() ? "" : " [command_options]") % + (command->arg_list().empty() ? "" : (" " + command->arg_list())), + false); + ui->out_wrap(""); + ui->out_wrap(F("%s.") % command->short_description()); + + const text::table general_table = options_help(*general_options); + const text::table command_table = options_help(command->options()); + + const text::widths_vector::value_type first_width = + std::max(general_table.column_width(0), command_table.column_width(0)); + text::table_formatter formatter; + formatter.set_column_width(0, first_width); + formatter.set_column_width(1, text::table_formatter::width_refill); + formatter.set_separator(" "); + + if (!general_table.empty()) { + ui->out_wrap(""); + ui->out_wrap("Available general options:"); + ui->out_table(general_table, formatter, " "); + } + + if (!command_table.empty()) { + ui->out_wrap(""); + ui->out_wrap("Available command options:"); + ui->out_table(command_table, formatter, " "); + } + + ui->out_wrap(""); + ui->out_wrap(F("See kyua-%s(1) for more details.") % command->name()); +} + + +} // anonymous namespace + + +/// Default constructor for cmd_help. +/// +/// \param options_ The set of program-wide options for which to provide help. +/// \param commands_ The set of commands for which to provide help. +cmd_help::cmd_help(const cmdline::options_vector* options_, + const cmdline::commands_map< cli_command >* commands_) : + cli_command("help", "[subcommand]", 0, 1, "Shows usage information"), + _options(options_), + _commands(commands_) +{ +} + + +/// Entry point for the "help" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 to indicate success. +int +cmd_help::run(utils::cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + if (cmdline.arguments().empty()) { + general_help(ui, _options, _commands); + } else { + INV(cmdline.arguments().size() == 1); + const std::string& cmdname = cmdline.arguments()[0]; + const cli::cli_command* command = _commands->find(cmdname); + if (command == NULL) + throw cmdline::usage_error(F("The command %s does not exist") % + cmdname); + else + subcommand_help(ui, _options, command); + } + + return EXIT_SUCCESS; +} diff --git a/cli/cmd_help.hpp b/cli/cmd_help.hpp new file mode 100644 index 000000000000..5f3b19db901d --- /dev/null +++ b/cli/cmd_help.hpp @@ -0,0 +1,62 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_help.hpp +/// Provides the cmd_help class. + +#if !defined(CLI_CMD_HELP_HPP) +#define CLI_CMD_HELP_HPP + +#include "cli/common.hpp" +#include "utils/cmdline/commands_map_fwd.hpp" + +namespace cli { + + +/// Implementation of the "help" subcommand. +class cmd_help : public cli_command +{ + /// The set of program-wide options for which to provide help. + const utils::cmdline::options_vector* _options; + + /// The set of commands for which to provide help. + const utils::cmdline::commands_map< cli_command >* _commands; + +public: + cmd_help(const utils::cmdline::options_vector*, + const utils::cmdline::commands_map< cli_command >*); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_HELP_HPP) diff --git a/cli/cmd_help_test.cpp b/cli/cmd_help_test.cpp new file mode 100644 index 000000000000..d292090be451 --- /dev/null +++ b/cli/cmd_help_test.cpp @@ -0,0 +1,347 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_help.hpp" + +#include +#include +#include + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/commands_map.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" +#include "utils/defs.hpp" +#include "utils/sanity.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_help; + + +namespace { + + +/// Mock command with a simple definition (no options, no arguments). +/// +/// Attempting to run this command will result in a crash. It is only provided +/// to validate the generation of interactive help. +class cmd_mock_simple : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// \param name_ The name of the command to create. + cmd_mock_simple(const char* name_) : cli::cli_command( + name_, "", 0, 0, "Simple command") + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function is never called. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + UNREACHABLE; + } +}; + + +/// Mock command with a complex definition (some options, some arguments). +/// +/// Attempting to run this command will result in a crash. It is only provided +/// to validate the generation of interactive help. +class cmd_mock_complex : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// \param name_ The name of the command to create. + cmd_mock_complex(const char* name_) : cli::cli_command( + name_, "[arg1 .. argN]", 0, 2, "Complex command") + { + add_option(cmdline::bool_option("flag_a", "Flag A")); + add_option(cmdline::bool_option('b', "flag_b", "Flag B")); + add_option(cmdline::string_option('c', "flag_c", "Flag C", "c_arg")); + add_option(cmdline::string_option("flag_d", "Flag D", "d_arg", "foo")); + } + + /// Runs the mock command. + /// + /// \return Nothing because this function is never called. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + UNREACHABLE; + } +}; + + +/// Initializes the cmdline library and generates the set of test commands. +/// +/// \param [out] commands A mapping that is updated to contain the commands to +/// use for testing. +static void +setup(cmdline::commands_map< cli::cli_command >& commands) +{ + cmdline::init("progname"); + + commands.insert(new cmd_mock_simple("mock_simple")); + commands.insert(new cmd_mock_complex("mock_complex")); + + commands.insert(new cmd_mock_simple("mock_simple_2"), "First"); + commands.insert(new cmd_mock_complex("mock_complex_2"), "First"); + + commands.insert(new cmd_mock_simple("mock_simple_3"), "Second"); +} + + +/// Performs a test on the global help (not that of a subcommand). +/// +/// \param general_options The genral options supported by the tool, if any. +/// \param expected_options Expected lines of help output documenting the +/// options in general_options. +/// \param ui The cmdline::mock_ui object to which to write the output. +static void +global_test(const cmdline::options_vector& general_options, + const std::vector< std::string >& expected_options, + cmdline::ui_mock& ui) +{ + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + + cmd_help cmd(&general_options, &mock_commands); + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + + std::vector< std::string > expected; + + expected.push_back(PACKAGE " (" PACKAGE_NAME ") " PACKAGE_VERSION); + expected.push_back(""); + expected.push_back("Usage: progname [general_options] command " + "[command_options] [args]"); + if (!general_options.empty()) { + expected.push_back(""); + expected.push_back("Available general options:"); + std::copy(expected_options.begin(), expected_options.end(), + std::back_inserter(expected)); + } + expected.push_back(""); + expected.push_back("Generic commands:"); + expected.push_back(" mock_complex Complex command."); + expected.push_back(" mock_simple Simple command."); + expected.push_back(""); + expected.push_back("First commands:"); + expected.push_back(" mock_complex_2 Complex command."); + expected.push_back(" mock_simple_2 Simple command."); + expected.push_back(""); + expected.push_back("Second commands:"); + expected.push_back(" mock_simple_3 Simple command."); + expected.push_back(""); + expected.push_back("See kyua(1) for more details."); + + ATF_REQUIRE(expected == ui.out_log()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(global__no_options); +ATF_TEST_CASE_BODY(global__no_options) +{ + cmdline::ui_mock ui; + + cmdline::options_vector general_options; + + global_test(general_options, std::vector< std::string >(), ui); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(global__some_options); +ATF_TEST_CASE_BODY(global__some_options) +{ + cmdline::ui_mock ui; + + cmdline::options_vector general_options; + const cmdline::bool_option flag_a("flag_a", "Flag A"); + general_options.push_back(&flag_a); + const cmdline::string_option flag_c('c', "lc", "Flag C", "X"); + general_options.push_back(&flag_c); + + std::vector< std::string > expected; + expected.push_back(" --flag_a Flag A."); + expected.push_back(" -c X, --lc=X Flag C."); + + global_test(general_options, expected, ui); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommand__simple); +ATF_TEST_CASE_BODY(subcommand__simple) +{ + cmdline::options_vector general_options; + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("mock_simple"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(atf::utils::grep_collection( + "^kyua.*" PACKAGE_VERSION, ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^Usage: progname \\[general_options\\] mock_simple$", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection( + "Available.*options", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^See kyua-mock_simple\\(1\\) for more details.", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommand__complex); +ATF_TEST_CASE_BODY(subcommand__complex) +{ + cmdline::options_vector general_options; + const cmdline::bool_option global_a("global_a", "Global A"); + general_options.push_back(&global_a); + const cmdline::string_option global_c('c', "global_c", "Global C", + "c_global"); + general_options.push_back(&global_c); + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("mock_complex"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(atf::utils::grep_collection( + "^kyua.*" PACKAGE_VERSION, ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^Usage: progname \\[general_options\\] mock_complex " + "\\[command_options\\] \\[arg1 .. argN\\]$", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("Available general options", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("--global_a", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("--global_c=c_global", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("Available command options", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("--flag_a *Flag A", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("-b.*--flag_b *Flag B", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "-c c_arg.*--flag_c=c_arg *Flag C", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "--flag_d=d_arg *Flag D.*default.*foo", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^See kyua-mock_complex\\(1\\) for more details.", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommand__unknown); +ATF_TEST_CASE_BODY(subcommand__unknown) +{ + cmdline::options_vector general_options; + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("foobar"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "command foobar.*not exist", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_args); +ATF_TEST_CASE_BODY(invalid_args) +{ + cmdline::options_vector general_options; + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("mock_simple"); + args.push_back("mock_complex"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Too many arguments", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, global__no_options); + ATF_ADD_TEST_CASE(tcs, global__some_options); + ATF_ADD_TEST_CASE(tcs, subcommand__simple); + ATF_ADD_TEST_CASE(tcs, subcommand__complex); + ATF_ADD_TEST_CASE(tcs, subcommand__unknown); + ATF_ADD_TEST_CASE(tcs, invalid_args); +} diff --git a/cli/cmd_list.cpp b/cli/cmd_list.cpp new file mode 100644 index 000000000000..ed0e4980fc47 --- /dev/null +++ b/cli/cmd_list.cpp @@ -0,0 +1,161 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_list.hpp" + +#include +#include +#include + +#include "cli/common.ipp" +#include "drivers/list_tests.hpp" +#include "engine/filters.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/types.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Hooks for list_tests to print test cases as they come. +class progress_hooks : public drivers::list_tests::base_hooks { + /// The ui object to which to print the test cases. + cmdline::ui* _ui; + + /// Whether to print test case details or just their names. + bool _verbose; + +public: + /// Initializes the hooks. + /// + /// \param ui_ The ui object to which to print the test cases. + /// \param verbose_ Whether to print test case details or just their names. + progress_hooks(cmdline::ui* ui_, const bool verbose_) : + _ui(ui_), + _verbose(verbose_) + { + } + + /// Reports a test case as soon as it is found. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the located test case. + void + got_test_case(const model::test_program& test_program, + const std::string& test_case_name) + { + cli::detail::list_test_case(_ui, _verbose, test_program, + test_case_name); + } +}; + + +} // anonymous namespace + + +/// Lists a single test case. +/// +/// \param [out] ui Object to interact with the I/O of the program. +/// \param verbose Whether to be verbose or not. +/// \param test_program The test program containing the test case to print. +/// \param test_case_name The name of the test case to print. +void +cli::detail::list_test_case(cmdline::ui* ui, const bool verbose, + const model::test_program& test_program, + const std::string& test_case_name) +{ + const model::test_case& test_case = test_program.find(test_case_name); + + const std::string id = format_test_case_id(test_program, test_case_name); + if (!verbose) { + ui->out(id); + } else { + ui->out(F("%s (%s)") % id % test_program.test_suite_name()); + + // TODO(jmmv): Running these for every test case is probably not the + // fastest thing to do. + const model::metadata default_md = model::metadata_builder().build(); + const model::properties_map default_props = default_md.to_properties(); + + const model::metadata& test_md = test_case.get_metadata(); + const model::properties_map test_props = test_md.to_properties(); + + for (model::properties_map::const_iterator iter = test_props.begin(); + iter != test_props.end(); iter++) { + const model::properties_map::const_iterator default_iter = + default_props.find((*iter).first); + if (default_iter == default_props.end() || + (*iter).second != (*default_iter).second) + ui->out(F(" %s = %s") % (*iter).first % (*iter).second); + } + } +} + + +/// Default constructor for cmd_list. +cli::cmd_list::cmd_list(void) : + cli_command("list", "[test-program ...]", 0, -1, + "Lists test cases and their meta-data") +{ + add_option(build_root_option); + add_option(kyuafile_option); + add_option(cmdline::bool_option('v', "verbose", "Show properties")); +} + + +/// Entry point for the "list" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime configuration of the program. +/// +/// \return 0 to indicate success. +int +cli::cmd_list::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + progress_hooks hooks(ui, cmdline.has_option("verbose")); + const drivers::list_tests::result result = drivers::list_tests::drive( + kyuafile_path(cmdline), build_root_path(cmdline), + parse_filters(cmdline.arguments()), user_config, hooks); + + return report_unused_filters(result.unused_filters, ui) ? + EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/cli/cmd_list.hpp b/cli/cmd_list.hpp new file mode 100644 index 000000000000..cbdc084a6e16 --- /dev/null +++ b/cli/cmd_list.hpp @@ -0,0 +1,65 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_list.hpp +/// Provides the cmd_list class. + +#if !defined(CLI_CMD_LIST_HPP) +#define CLI_CMD_LIST_HPP + +#include + +#include "cli/common.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace cli { + + +namespace detail { + +void list_test_case(utils::cmdline::ui*, const bool, const model::test_program&, + const std::string&); + +} // namespace detail + + +/// Implementation of the "list" subcommand. +class cmd_list : public cli_command +{ +public: + cmd_list(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + +#endif // !defined(CLI_CMD_LIST_HPP) diff --git a/cli/cmd_list_test.cpp b/cli/cmd_list_test.cpp new file mode 100644 index 000000000000..19078abd7d48 --- /dev/null +++ b/cli/cmd_list_test.cpp @@ -0,0 +1,112 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_list.hpp" + +#include + +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(list_test_case__no_verbose); +ATF_TEST_CASE_BODY(list_test_case__no_verbose) +{ + const model::metadata md = model::metadata_builder() + .set_description("This should not be shown") + .build(); + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("the/test-program"), fs::path("root"), "suite") + .add_test_case("abc", md) + .set_metadata(md) + .build(); + + cmdline::ui_mock ui; + cli::detail::list_test_case(&ui, false, test_program, "abc"); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("the/test-program:abc", ui.out_log()[0]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_test_case__verbose__no_properties); +ATF_TEST_CASE_BODY(list_test_case__verbose__no_properties) +{ + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("hello/world"), fs::path("root"), "the-suite") + .add_test_case("my_name") + .build(); + + cmdline::ui_mock ui; + cli::detail::list_test_case(&ui, true, test_program, "my_name"); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("hello/world:my_name (the-suite)", ui.out_log()[0]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_test_case__verbose__some_properties); +ATF_TEST_CASE_BODY(list_test_case__verbose__some_properties) +{ + const model::metadata md = model::metadata_builder() + .add_custom("my-property", "value") + .set_description("Some description") + .set_has_cleanup(true) + .build(); + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("hello/world"), fs::path("root"), "the-suite") + .add_test_case("my_name", md) + .set_metadata(md) + .build(); + + cmdline::ui_mock ui; + cli::detail::list_test_case(&ui, true, test_program, "my_name"); + ATF_REQUIRE_EQ(4, ui.out_log().size()); + ATF_REQUIRE_EQ("hello/world:my_name (the-suite)", ui.out_log()[0]); + ATF_REQUIRE_EQ(" custom.my-property = value", ui.out_log()[1]); + ATF_REQUIRE_EQ(" description = Some description", ui.out_log()[2]); + ATF_REQUIRE_EQ(" has_cleanup = true", ui.out_log()[3]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, list_test_case__no_verbose); + ATF_ADD_TEST_CASE(tcs, list_test_case__verbose__no_properties); + ATF_ADD_TEST_CASE(tcs, list_test_case__verbose__some_properties); + + // Tests for cmd_list::run are located in integration/cmd_list_test. +} diff --git a/cli/cmd_report.cpp b/cli/cmd_report.cpp new file mode 100644 index 000000000000..27827e893de7 --- /dev/null +++ b/cli/cmd_report.cpp @@ -0,0 +1,421 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_report.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "cli/common.ipp" +#include "drivers/scan_results.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "model/types.hpp" +#include "store/layout.hpp" +#include "store/read_transaction.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" +#include "utils/text/operations.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace text = utils::text; + +using cli::cmd_report; +using utils::optional; + + +namespace { + + +/// Generates a plain-text report intended to be printed to the console. +class report_console_hooks : public drivers::scan_results::base_hooks { + /// Stream to which to write the report. + std::ostream& _output; + + /// Whether to include details in the report or not. + const bool _verbose; + + /// Collection of result types to include in the report. + const cli::result_types& _results_filters; + + /// Path to the results file being read. + const fs::path& _results_file; + + /// The start time of the first test. + optional< utils::datetime::timestamp > _start_time; + + /// The end time of the last test. + optional< utils::datetime::timestamp > _end_time; + + /// The total run time of the tests. Note that we cannot subtract _end_time + /// from _start_time to compute this due to parallel execution. + utils::datetime::delta _runtime; + + /// Representation of a single result. + struct result_data { + /// The relative path to the test program. + utils::fs::path binary_path; + + /// The name of the test case. + std::string test_case_name; + + /// The result of the test case. + model::test_result result; + + /// The duration of the test case execution. + utils::datetime::delta duration; + + /// Constructs a new results data. + /// + /// \param binary_path_ The relative path to the test program. + /// \param test_case_name_ The name of the test case. + /// \param result_ The result of the test case. + /// \param duration_ The duration of the test case execution. + result_data(const utils::fs::path& binary_path_, + const std::string& test_case_name_, + const model::test_result& result_, + const utils::datetime::delta& duration_) : + binary_path(binary_path_), test_case_name(test_case_name_), + result(result_), duration(duration_) + { + } + }; + + /// Results received, broken down by their type. + /// + /// Note that this may not include all results, as keeping the whole list in + /// memory may be too much. + std::map< model::test_result_type, std::vector< result_data > > _results; + + /// Pretty-prints the value of an environment variable. + /// + /// \param indent Prefix for the lines to print. Continuation lines + /// use this indentation twice. + /// \param name Name of the variable. + /// \param value Value of the variable. Can have newlines. + void + print_env_var(const char* indent, const std::string& name, + const std::string& value) + { + const std::vector< std::string > lines = text::split(value, '\n'); + if (lines.size() == 0) { + _output << F("%s%s=\n") % indent % name;; + } else { + _output << F("%s%s=%s\n") % indent % name % lines[0]; + for (std::vector< std::string >::size_type i = 1; + i < lines.size(); ++i) { + _output << F("%s%s%s\n") % indent % indent % lines[i]; + } + } + } + + /// Prints the execution context to the output. + /// + /// \param context The context to dump. + void + print_context(const model::context& context) + { + _output << "===> Execution context\n"; + + _output << F("Current directory: %s\n") % context.cwd(); + const std::map< std::string, std::string >& env = context.env(); + if (env.empty()) + _output << "No environment variables recorded\n"; + else { + _output << "Environment variables:\n"; + for (std::map< std::string, std::string >::const_iterator + iter = env.begin(); iter != env.end(); iter++) { + print_env_var(" ", (*iter).first, (*iter).second); + } + } + } + + /// Dumps a detailed view of the test case. + /// + /// \param result_iter Results iterator pointing at the test case to be + /// dumped. + void + print_test_case_and_result(const store::results_iterator& result_iter) + { + const model::test_case& test_case = + result_iter.test_program()->find(result_iter.test_case_name()); + const model::properties_map props = + test_case.get_metadata().to_properties(); + + _output << F("===> %s:%s\n") % + result_iter.test_program()->relative_path() % + result_iter.test_case_name(); + _output << F("Result: %s\n") % + cli::format_result(result_iter.result()); + _output << F("Start time: %s\n") % + result_iter.start_time().to_iso8601_in_utc(); + _output << F("End time: %s\n") % + result_iter.end_time().to_iso8601_in_utc(); + _output << F("Duration: %s\n") % + cli::format_delta(result_iter.end_time() - + result_iter.start_time()); + + _output << "\n"; + _output << "Metadata:\n"; + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + if ((*iter).second.empty()) { + _output << F(" %s is empty\n") % (*iter).first; + } else { + _output << F(" %s = %s\n") % (*iter).first % (*iter).second; + } + } + + const std::string stdout_contents = result_iter.stdout_contents(); + if (!stdout_contents.empty()) { + _output << "\n" + << "Standard output:\n" + << stdout_contents; + } + + const std::string stderr_contents = result_iter.stderr_contents(); + if (!stderr_contents.empty()) { + _output << "\n" + << "Standard error:\n" + << stderr_contents; + } + } + + /// Counts how many results of a given type have been received. + /// + /// \param type Test result type to count results for. + /// + /// \return The number of test results with \p type. + std::size_t + count_results(const model::test_result_type type) + { + const std::map< model::test_result_type, + std::vector< result_data > >::const_iterator iter = + _results.find(type); + if (iter == _results.end()) + return 0; + else + return (*iter).second.size(); + } + + /// Prints a set of results. + /// + /// \param type Test result type to print results for. + /// \param title Title used when printing results. + void + print_results(const model::test_result_type type, + const char* title) + { + const std::map< model::test_result_type, + std::vector< result_data > >::const_iterator iter2 = + _results.find(type); + if (iter2 == _results.end()) + return; + const std::vector< result_data >& all = (*iter2).second; + + _output << F("===> %s\n") % title; + for (std::vector< result_data >::const_iterator iter = all.begin(); + iter != all.end(); iter++) { + _output << F("%s:%s -> %s [%s]\n") % (*iter).binary_path % + (*iter).test_case_name % + cli::format_result((*iter).result) % + cli::format_delta((*iter).duration); + } + } + +public: + /// Constructor for the hooks. + /// + /// \param [out] output_ Stream to which to write the report. + /// \param verbose_ Whether to include details in the output or not. + /// \param results_filters_ The result types to include in the report. + /// Cannot be empty. + /// \param results_file_ Path to the results file being read. + report_console_hooks(std::ostream& output_, const bool verbose_, + const cli::result_types& results_filters_, + const fs::path& results_file_) : + _output(output_), + _verbose(verbose_), + _results_filters(results_filters_), + _results_file(results_file_) + { + PRE(!results_filters_.empty()); + } + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + void + got_context(const model::context& context) + { + if (_verbose) + print_context(context); + } + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. + void + got_result(store::results_iterator& iter) + { + if (!_start_time || _start_time.get() > iter.start_time()) + _start_time = iter.start_time(); + if (!_end_time || _end_time.get() < iter.end_time()) + _end_time = iter.end_time(); + + const datetime::delta duration = iter.end_time() - iter.start_time(); + + _runtime += duration; + const model::test_result result = iter.result(); + _results[result.type()].push_back( + result_data(iter.test_program()->relative_path(), + iter.test_case_name(), iter.result(), duration)); + + if (_verbose) { + // TODO(jmmv): _results_filters is a list and is small enough for + // std::find to not be an expensive operation here (probably). But + // we should be using a std::set instead. + if (std::find(_results_filters.begin(), _results_filters.end(), + iter.result().type()) != _results_filters.end()) { + print_test_case_and_result(iter); + } + } + } + + /// Prints the tests summary. + void + end(const drivers::scan_results::result& /* r */) + { + typedef std::map< model::test_result_type, const char* > types_map; + + types_map titles; + titles[model::test_result_broken] = "Broken tests"; + titles[model::test_result_expected_failure] = "Expected failures"; + titles[model::test_result_failed] = "Failed tests"; + titles[model::test_result_passed] = "Passed tests"; + titles[model::test_result_skipped] = "Skipped tests"; + + for (cli::result_types::const_iterator iter = _results_filters.begin(); + iter != _results_filters.end(); ++iter) { + const types_map::const_iterator match = titles.find(*iter); + INV_MSG(match != titles.end(), "Conditional does not match user " + "input validation in parse_types()"); + print_results((*match).first, (*match).second); + } + + const std::size_t broken = count_results(model::test_result_broken); + const std::size_t failed = count_results(model::test_result_failed); + const std::size_t passed = count_results(model::test_result_passed); + const std::size_t skipped = count_results(model::test_result_skipped); + const std::size_t xfail = count_results( + model::test_result_expected_failure); + const std::size_t total = broken + failed + passed + skipped + xfail; + + _output << "===> Summary\n"; + _output << F("Results read from %s\n") % _results_file; + _output << F("Test cases: %s total, %s skipped, %s expected failures, " + "%s broken, %s failed\n") % + total % skipped % xfail % broken % failed; + if (_verbose && _start_time) { + INV(_end_time); + _output << F("Start time: %s\n") % + _start_time.get().to_iso8601_in_utc(); + _output << F("End time: %s\n") % + _end_time.get().to_iso8601_in_utc(); + } + _output << F("Total time: %s\n") % cli::format_delta(_runtime); + } +}; + + +} // anonymous namespace + + +/// Default constructor for cmd_report. +cmd_report::cmd_report(void) : cli_command( + "report", "", 0, -1, + "Generates a report with the results of a test suite run") +{ + add_option(results_file_open_option); + add_option(cmdline::bool_option( + "verbose", "Include the execution context and the details of each test " + "case in the report")); + add_option(cmdline::path_option("output", "Path to the output file", "path", + "/dev/stdout")); + add_option(results_filter_option); +} + + +/// Entry point for the "report" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_report::run(cmdline::ui* ui, + const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + std::auto_ptr< std::ostream > output = utils::open_ostream( + cmdline.get_option< cmdline::path_option >("output")); + + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + const result_types types = get_result_types(cmdline); + report_console_hooks hooks(*output.get(), cmdline.has_option("verbose"), + types, results_file); + const drivers::scan_results::result result = drivers::scan_results::drive( + results_file, parse_filters(cmdline.arguments()), hooks); + + return report_unused_filters(result.unused_filters, ui) ? + EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/cli/cmd_report.hpp b/cli/cmd_report.hpp new file mode 100644 index 000000000000..3d73c592ed9b --- /dev/null +++ b/cli/cmd_report.hpp @@ -0,0 +1,54 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_report.hpp +/// Provides the cmd_report class. + +#if !defined(CLI_CMD_REPORT_HPP) +#define CLI_CMD_REPORT_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "report" subcommand. +class cmd_report : public cli_command +{ +public: + cmd_report(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_REPORT_HPP) diff --git a/cli/cmd_report_html.cpp b/cli/cmd_report_html.cpp new file mode 100644 index 000000000000..b2133a8de047 --- /dev/null +++ b/cli/cmd_report_html.cpp @@ -0,0 +1,474 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_report_html.hpp" + +#include +#include +#include +#include +#include + +#include "cli/common.ipp" +#include "drivers/scan_results.hpp" +#include "engine/filters.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "store/read_transaction.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/text/templates.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace text = utils::text; + +using utils::optional; + + +namespace { + + +/// Creates the report's top directory and fails if it exists. +/// +/// \param directory The directory to create. +/// \param force Whether to wipe an existing directory or not. +/// +/// \throw std::runtime_error If the directory already exists; this is a user +/// error that the user must correct. +/// \throw fs::error If the directory creation fails for any other reason. +static void +create_top_directory(const fs::path& directory, const bool force) +{ + if (force) { + if (fs::exists(directory)) + fs::rm_r(directory); + } + + try { + fs::mkdir(directory, 0755); + } catch (const fs::system_error& e) { + if (e.original_errno() == EEXIST) + throw std::runtime_error(F("Output directory '%s' already exists; " + "maybe use --force?") % + directory); + else + throw e; + } +} + + +/// Generates a flat unique filename for a given test case. +/// +/// \param test_program The test program for which to genereate the name. +/// \param test_case_name The test case name. +/// +/// \return A filename unique within a directory with a trailing HTML extension. +static std::string +test_case_filename(const model::test_program& test_program, + const std::string& test_case_name) +{ + static const char* special_characters = "/:"; + + std::string name = cli::format_test_case_id(test_program, test_case_name); + std::string::size_type pos = name.find_first_of(special_characters); + while (pos != std::string::npos) { + name.replace(pos, 1, "_"); + pos = name.find_first_of(special_characters, pos + 1); + } + return name + ".html"; +} + + +/// Adds a string to string map to the templates. +/// +/// \param [in,out] templates The templates to add the map to. +/// \param props The map to add to the templates. +/// \param key_vector Name of the template vector that holds the keys. +/// \param value_vector Name of the template vector that holds the values. +static void +add_map(text::templates_def& templates, const config::properties_map& props, + const std::string& key_vector, const std::string& value_vector) +{ + templates.add_vector(key_vector); + templates.add_vector(value_vector); + + for (config::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + templates.add_to_vector(key_vector, (*iter).first); + templates.add_to_vector(value_vector, (*iter).second); + } +} + + +/// Generates an HTML report. +class html_hooks : public drivers::scan_results::base_hooks { + /// User interface object where to report progress. + cmdline::ui* _ui; + + /// The top directory in which to create the HTML files. + fs::path _directory; + + /// Collection of result types to include in the report. + const cli::result_types& _results_filters; + + /// The start time of the first test. + optional< utils::datetime::timestamp > _start_time; + + /// The end time of the last test. + optional< utils::datetime::timestamp > _end_time; + + /// The total run time of the tests. Note that we cannot subtract _end_time + /// from _start_time to compute this due to parallel execution. + utils::datetime::delta _runtime; + + /// Templates accumulator to generate the index.html file. + text::templates_def _summary_templates; + + /// Mapping of result types to the amount of tests with such result. + std::map< model::test_result_type, std::size_t > _types_count; + + /// Generates a common set of templates for all of our files. + /// + /// \return A new templates object with common parameters. + static text::templates_def + common_templates(void) + { + text::templates_def templates; + templates.add_variable("css", "report.css"); + return templates; + } + + /// Adds a test case result to the summary. + /// + /// \param test_program The test program with the test case to be added. + /// \param test_case_name Name of the test case. + /// \param result The result of the test case. + /// \param has_detail If true, the result of the test case has not been + /// filtered and therefore there exists a separate file for the test + /// with all of its information. + void + add_to_summary(const model::test_program& test_program, + const std::string& test_case_name, + const model::test_result& result, + const bool has_detail) + { + ++_types_count[result.type()]; + + if (!has_detail) + return; + + std::string test_cases_vector; + std::string test_cases_file_vector; + switch (result.type()) { + case model::test_result_broken: + test_cases_vector = "broken_test_cases"; + test_cases_file_vector = "broken_test_cases_file"; + break; + + case model::test_result_expected_failure: + test_cases_vector = "xfail_test_cases"; + test_cases_file_vector = "xfail_test_cases_file"; + break; + + case model::test_result_failed: + test_cases_vector = "failed_test_cases"; + test_cases_file_vector = "failed_test_cases_file"; + break; + + case model::test_result_passed: + test_cases_vector = "passed_test_cases"; + test_cases_file_vector = "passed_test_cases_file"; + break; + + case model::test_result_skipped: + test_cases_vector = "skipped_test_cases"; + test_cases_file_vector = "skipped_test_cases_file"; + break; + } + INV(!test_cases_vector.empty()); + INV(!test_cases_file_vector.empty()); + + _summary_templates.add_to_vector( + test_cases_vector, + cli::format_test_case_id(test_program, test_case_name)); + _summary_templates.add_to_vector( + test_cases_file_vector, + test_case_filename(test_program, test_case_name)); + } + + /// Instantiate a template to generate an HTML file in the output directory. + /// + /// \param templates The templates to use. + /// \param template_name The name of the template. This is automatically + /// searched for in the installed directory, so do not provide a path. + /// \param output_name The name of the output file. This is a basename to + /// be created within the output directory. + /// + /// \throw text::error If there is any problem applying the templates. + void + generate(const text::templates_def& templates, + const std::string& template_name, + const std::string& output_name) const + { + const fs::path miscdir(utils::getenv_with_default( + "KYUA_MISCDIR", KYUA_MISCDIR)); + const fs::path template_file = miscdir / template_name; + const fs::path output_path(_directory / output_name); + + _ui->out(F("Generating %s") % output_path); + text::instantiate(templates, template_file, output_path); + } + + /// Gets the number of tests with a given result type. + /// + /// \param type The type to be queried. + /// + /// \return The number of tests of the given type, or 0 if none have yet + /// been registered by add_to_summary(). + std::size_t + get_count(const model::test_result_type type) const + { + const std::map< model::test_result_type, std::size_t >::const_iterator + iter = _types_count.find(type); + if (iter == _types_count.end()) + return 0; + else + return (*iter).second; + } + +public: + /// Constructor for the hooks. + /// + /// \param ui_ User interface object where to report progress. + /// \param directory_ The directory in which to create the HTML files. + /// \param results_filters_ The result types to include in the report. + /// Cannot be empty. + html_hooks(cmdline::ui* ui_, const fs::path& directory_, + const cli::result_types& results_filters_) : + _ui(ui_), + _directory(directory_), + _results_filters(results_filters_), + _summary_templates(common_templates()) + { + PRE(!results_filters_.empty()); + + // Keep in sync with add_to_summary(). + _summary_templates.add_vector("broken_test_cases"); + _summary_templates.add_vector("broken_test_cases_file"); + _summary_templates.add_vector("xfail_test_cases"); + _summary_templates.add_vector("xfail_test_cases_file"); + _summary_templates.add_vector("failed_test_cases"); + _summary_templates.add_vector("failed_test_cases_file"); + _summary_templates.add_vector("passed_test_cases"); + _summary_templates.add_vector("passed_test_cases_file"); + _summary_templates.add_vector("skipped_test_cases"); + _summary_templates.add_vector("skipped_test_cases_file"); + } + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + void + got_context(const model::context& context) + { + text::templates_def templates = common_templates(); + templates.add_variable("cwd", context.cwd().str()); + add_map(templates, context.env(), "env_var", "env_var_value"); + generate(templates, "context.html", "context.html"); + } + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. + void + got_result(store::results_iterator& iter) + { + const model::test_program_ptr test_program = iter.test_program(); + const std::string& test_case_name = iter.test_case_name(); + const model::test_result result = iter.result(); + + if (std::find(_results_filters.begin(), _results_filters.end(), + result.type()) == _results_filters.end()) { + add_to_summary(*test_program, test_case_name, result, false); + return; + } + + add_to_summary(*test_program, test_case_name, result, true); + + if (!_start_time || _start_time.get() > iter.start_time()) + _start_time = iter.start_time(); + if (!_end_time || _end_time.get() < iter.end_time()) + _end_time = iter.end_time(); + + const datetime::delta duration = iter.end_time() - iter.start_time(); + + _runtime += duration; + + text::templates_def templates = common_templates(); + templates.add_variable("test_case", + cli::format_test_case_id(*test_program, + test_case_name)); + templates.add_variable("test_program", + test_program->absolute_path().str()); + templates.add_variable("result", cli::format_result(result)); + templates.add_variable("start_time", + iter.start_time().to_iso8601_in_utc()); + templates.add_variable("end_time", + iter.end_time().to_iso8601_in_utc()); + templates.add_variable("duration", cli::format_delta(duration)); + + const model::test_case& test_case = test_program->find(test_case_name); + add_map(templates, test_case.get_metadata().to_properties(), + "metadata_var", "metadata_value"); + + { + const std::string stdout_text = iter.stdout_contents(); + if (!stdout_text.empty()) + templates.add_variable("stdout", stdout_text); + } + { + const std::string stderr_text = iter.stderr_contents(); + if (!stderr_text.empty()) + templates.add_variable("stderr", stderr_text); + } + + generate(templates, "test_result.html", + test_case_filename(*test_program, test_case_name)); + } + + /// Writes the index.html file in the output directory. + /// + /// This should only be called once all the processing has been done; + /// i.e. when the scan_results driver returns. + void + write_summary(void) + { + const std::size_t n_passed = get_count(model::test_result_passed); + const std::size_t n_failed = get_count(model::test_result_failed); + const std::size_t n_skipped = get_count(model::test_result_skipped); + const std::size_t n_xfail = get_count( + model::test_result_expected_failure); + const std::size_t n_broken = get_count(model::test_result_broken); + + const std::size_t n_bad = n_broken + n_failed; + + if (_start_time) { + INV(_end_time); + _summary_templates.add_variable( + "start_time", _start_time.get().to_iso8601_in_utc()); + _summary_templates.add_variable( + "end_time", _end_time.get().to_iso8601_in_utc()); + } else { + _summary_templates.add_variable("start_time", "No tests run"); + _summary_templates.add_variable("end_time", "No tests run"); + } + _summary_templates.add_variable("duration", + cli::format_delta(_runtime)); + _summary_templates.add_variable("passed_tests_count", + F("%s") % n_passed); + _summary_templates.add_variable("failed_tests_count", + F("%s") % n_failed); + _summary_templates.add_variable("skipped_tests_count", + F("%s") % n_skipped); + _summary_templates.add_variable("xfail_tests_count", + F("%s") % n_xfail); + _summary_templates.add_variable("broken_tests_count", + F("%s") % n_broken); + _summary_templates.add_variable("bad_tests_count", F("%s") % n_bad); + + generate(text::templates_def(), "report.css", "report.css"); + generate(_summary_templates, "index.html", "index.html"); + } +}; + + +} // anonymous namespace + + +/// Default constructor for cmd_report_html. +cli::cmd_report_html::cmd_report_html(void) : cli_command( + "report-html", "", 0, 0, + "Generates an HTML report with the result of a test suite run") +{ + add_option(results_file_open_option); + add_option(cmdline::bool_option( + "force", "Wipe the output directory before generating the new report; " + "use care")); + add_option(cmdline::path_option( + "output", "The directory in which to store the HTML files", + "path", "html")); + add_option(cmdline::list_option( + "results-filter", "Comma-separated list of result types to include in " + "the report", "types", "skipped,xfail,broken,failed")); +} + + +/// Entry point for the "report-html" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cli::cmd_report_html::run(cmdline::ui* ui, + const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + const result_types types = get_result_types(cmdline); + + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + const fs::path directory = + cmdline.get_option< cmdline::path_option >("output"); + create_top_directory(directory, cmdline.has_option("force")); + html_hooks hooks(ui, directory, types); + drivers::scan_results::drive(results_file, + std::set< engine::test_filter >(), + hooks); + hooks.write_summary(); + + return EXIT_SUCCESS; +} diff --git a/cli/cmd_report_html.hpp b/cli/cmd_report_html.hpp new file mode 100644 index 000000000000..fadc138293ad --- /dev/null +++ b/cli/cmd_report_html.hpp @@ -0,0 +1,55 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_report_html.hpp +/// Provides the cmd_report_html class. + +#if !defined(CLI_CMD_REPORT_HTML_HPP) +#define CLI_CMD_REPORT_HTML_HPP + +#include "cli/common.hpp" +#include "utils/cmdline/ui_fwd.hpp" + +namespace cli { + + +/// Implementation of the "report-html" subcommand. +class cmd_report_html : public cli_command +{ +public: + cmd_report_html(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_REPORT_HTML_HPP) diff --git a/cli/cmd_report_junit.cpp b/cli/cmd_report_junit.cpp new file mode 100644 index 000000000000..c4846c8795f2 --- /dev/null +++ b/cli/cmd_report_junit.cpp @@ -0,0 +1,89 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_report_junit.hpp" + +#include +#include +#include + +#include "cli/common.ipp" +#include "drivers/report_junit.hpp" +#include "drivers/scan_results.hpp" +#include "engine/filters.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/defs.hpp" +#include "utils/optional.ipp" +#include "utils/stream.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace layout = store::layout; + +using cli::cmd_report_junit; +using utils::optional; + + +/// Default constructor for cmd_report. +cmd_report_junit::cmd_report_junit(void) : cli_command( + "report-junit", "", 0, 0, + "Generates a JUnit report with the result of a test suite run") +{ + add_option(results_file_open_option); + add_option(cmdline::path_option("output", "Path to the output file", "path", + "/dev/stdout")); +} + + +/// Entry point for the "report" subcommand. +/// +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_report_junit::run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + std::auto_ptr< std::ostream > output = utils::open_ostream( + cmdline.get_option< cmdline::path_option >("output")); + + drivers::report_junit_hooks hooks(*output.get()); + drivers::scan_results::drive(results_file, + std::set< engine::test_filter >(), + hooks); + + return EXIT_SUCCESS; +} diff --git a/cli/cmd_report_junit.hpp b/cli/cmd_report_junit.hpp new file mode 100644 index 000000000000..1dc0bb731645 --- /dev/null +++ b/cli/cmd_report_junit.hpp @@ -0,0 +1,54 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_report_junit.hpp +/// Provides the cmd_report_junit class. + +#if !defined(CLI_CMD_REPORT_JUNIT_HPP) +#define CLI_CMD_REPORT_JUNIT_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "report-junit" subcommand. +class cmd_report_junit : public cli_command +{ +public: + cmd_report_junit(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_REPORT_JUNIT_HPP) diff --git a/cli/cmd_test.cpp b/cli/cmd_test.cpp new file mode 100644 index 000000000000..cfaeec9b74cc --- /dev/null +++ b/cli/cmd_test.cpp @@ -0,0 +1,186 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_test.hpp" + +#include + +#include "cli/common.ipp" +#include "drivers/run_tests.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + +using cli::cmd_test; + + +namespace { + + +/// Hooks to print a progress report of the execution of the tests. +class print_hooks : public drivers::run_tests::base_hooks { + /// Object to interact with the I/O of the program. + cmdline::ui* _ui; + + /// Whether the tests are executed in parallel or not. + bool _parallel; + +public: + /// The amount of positive test results found so far. + unsigned long good_count; + + /// The amount of negative test results found so far. + unsigned long bad_count; + + /// Constructor for the hooks. + /// + /// \param ui_ Object to interact with the I/O of the program. + /// \param parallel_ True if we are executing more than one test at once. + print_hooks(cmdline::ui* ui_, const bool parallel_) : + _ui(ui_), + _parallel(parallel_), + good_count(0), + bad_count(0) + { + } + + /// Called when the processing of a test case begins. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the test case being executed. + virtual void + got_test_case(const model::test_program& test_program, + const std::string& test_case_name) + { + if (!_parallel) { + _ui->out(F("%s -> ") % + cli::format_test_case_id(test_program, test_case_name), + false); + } + } + + /// Called when a result of a test case becomes available. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the test case being executed. + /// \param result The result of the execution of the test case. + /// \param duration The time it took to run the test. + virtual void + got_result(const model::test_program& test_program, + const std::string& test_case_name, + const model::test_result& result, + const datetime::delta& duration) + { + if (_parallel) { + _ui->out(F("%s -> ") % + cli::format_test_case_id(test_program, test_case_name), + false); + } + _ui->out(F("%s [%s]") % cli::format_result(result) % + cli::format_delta(duration)); + if (result.good()) + good_count++; + else + bad_count++; + } +}; + + +} // anonymous namespace + + +/// Default constructor for cmd_test. +cmd_test::cmd_test(void) : cli_command( + "test", "[test-program ...]", 0, -1, "Run tests") +{ + add_option(build_root_option); + add_option(kyuafile_option); + add_option(results_file_create_option); +} + + +/// Entry point for the "test" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime configuration of the program. +/// +/// \return 0 if all tests passed, 1 otherwise. +int +cmd_test::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + const layout::results_id_file_pair results = layout::new_db( + results_file_create(cmdline), kyuafile_path(cmdline).branch_path()); + + const bool parallel = (user_config.lookup< config::positive_int_node >( + "parallelism") > 1); + + print_hooks hooks(ui, parallel); + const drivers::run_tests::result result = drivers::run_tests::drive( + kyuafile_path(cmdline), build_root_path(cmdline), results.second, + parse_filters(cmdline.arguments()), user_config, hooks); + + int exit_code; + if (hooks.good_count > 0 || hooks.bad_count > 0) { + ui->out(""); + if (!results.first.empty()) { + ui->out(F("Results file id is %s") % results.first); + } + ui->out(F("Results saved to %s") % results.second); + ui->out(""); + + ui->out(F("%s/%s passed (%s failed)") % hooks.good_count % + (hooks.good_count + hooks.bad_count) % hooks.bad_count); + + exit_code = (hooks.bad_count == 0 ? EXIT_SUCCESS : EXIT_FAILURE); + } else { + // TODO(jmmv): Delete created empty file; it's useless! + if (!results.first.empty()) { + ui->out(F("Results file id is %s") % results.first); + } + ui->out(F("Results saved to %s") % results.second); + exit_code = EXIT_SUCCESS; + } + + return report_unused_filters(result.unused_filters, ui) ? + EXIT_FAILURE : exit_code; +} diff --git a/cli/cmd_test.hpp b/cli/cmd_test.hpp new file mode 100644 index 000000000000..22d8422cb293 --- /dev/null +++ b/cli/cmd_test.hpp @@ -0,0 +1,54 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/cmd_test.hpp +/// Provides the cmd_test class. + +#if !defined(CLI_CMD_TEST_HPP) +#define CLI_CMD_TEST_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "test" subcommand. +class cmd_test : public cli_command +{ +public: + cmd_test(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_TEST_HPP) diff --git a/cli/cmd_test_test.cpp b/cli/cmd_test_test.cpp new file mode 100644 index 000000000000..fb623323dd86 --- /dev/null +++ b/cli/cmd_test_test.cpp @@ -0,0 +1,63 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_test.hpp" + +#include + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_filter); +ATF_TEST_CASE_BODY(invalid_filter) +{ + cmdline::args_vector args; + args.push_back("test"); + args.push_back("correct"); + args.push_back("incorrect:"); + + cli::cmd_test cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::error, "Test case.*'incorrect:'.*empty", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, invalid_filter); +} diff --git a/cli/common.cpp b/cli/common.cpp new file mode 100644 index 000000000000..dbb7f12f18e0 --- /dev/null +++ b/cli/common.cpp @@ -0,0 +1,411 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/common.hpp" + +#include +#include +#include +#include + +#include "engine/filters.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +namespace cmdline = utils::cmdline; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + +using utils::none; +using utils::optional; + + +/// Standard definition of the option to specify the build root. +const cmdline::path_option cli::build_root_option( + "build-root", + "Path to the built test programs, if different from the location of the " + "Kyuafile scripts", + "path"); + + +/// Standard definition of the option to specify a Kyuafile. +const cmdline::path_option cli::kyuafile_option( + 'k', "kyuafile", + "Path to the test suite definition", + "file", "Kyuafile"); + + +/// Standard definition of the option to specify filters on test results. +const cmdline::list_option cli::results_filter_option( + "results-filter", "Comma-separated list of result types to include in " + "the report", "types", "skipped,xfail,broken,failed"); + + +/// Standard definition of the option to specify the results file. +/// +/// TODO(jmmv): Should support a git-like syntax to go back in time, like +/// --results-file=LATEST^N where N indicates how many runs to go back to. +const cmdline::string_option cli::results_file_create_option( + 'r', "results-file", + "Path to the results file to create; if left to the default value, the " + "name of the file is automatically computed for the current test suite", + "file", layout::results_auto_create_name); + + +/// Standard definition of the option to specify the results file. +/// +/// TODO(jmmv): Should support a git-like syntax to go back in time, like +/// --results-file=LATEST^N where N indicates how many runs to go back to. +const cmdline::string_option cli::results_file_open_option( + 'r', "results-file", + "Path to the results file to open or the identifier of the current test " + "suite or a previous results file for automatic lookup; if left to the " + "default value, uses the current directory as the test suite name", + "file", layout::results_auto_open_name); + + +namespace { + + +/// Gets the path to the historical database if it exists. +/// +/// TODO(jmmv): This function should go away. It only exists as a temporary +/// transitional path to force the use of the stale ~/.kyua/store.db if it +/// exists. +/// +/// \return A path if the file is found; none otherwise. +static optional< fs::path > +get_historical_db(void) +{ + optional< fs::path > home = utils::get_home(); + if (home) { + const fs::path old_db = home.get() / ".kyua/store.db"; + if (fs::exists(old_db)) { + if (old_db.is_absolute()) + return utils::make_optional(old_db); + else + return utils::make_optional(old_db.to_absolute()); + } else { + return none; + } + } else { + return none; + } +} + + +/// Converts a set of result type names to identifiers. +/// +/// \param names The collection of names to process; may be empty. +/// +/// \return The result type identifiers corresponding to the input names. +/// +/// \throw std::runtime_error If any name in the input names is invalid. +static cli::result_types +parse_types(const std::vector< std::string >& names) +{ + typedef std::map< std::string, model::test_result_type > types_map; + types_map valid_types; + valid_types["broken"] = model::test_result_broken; + valid_types["failed"] = model::test_result_failed; + valid_types["passed"] = model::test_result_passed; + valid_types["skipped"] = model::test_result_skipped; + valid_types["xfail"] = model::test_result_expected_failure; + + cli::result_types types; + for (std::vector< std::string >::const_iterator iter = names.begin(); + iter != names.end(); ++iter) { + const types_map::const_iterator match = valid_types.find(*iter); + if (match == valid_types.end()) + throw std::runtime_error(F("Unknown result type '%s'") % *iter); + else + types.push_back((*match).second); + } + return types; +} + + +} // anonymous namespace + + +/// Gets the path to the build root, if any. +/// +/// This is just syntactic sugar to simplify quierying the 'build_root_option'. +/// +/// \param cmdline The parsed command line. +/// +/// \return The path to the build root, if specified; none otherwise. +optional< fs::path > +cli::build_root_path(const cmdline::parsed_cmdline& cmdline) +{ + optional< fs::path > build_root; + if (cmdline.has_option(build_root_option.long_name())) + build_root = cmdline.get_option< cmdline::path_option >( + build_root_option.long_name()); + return build_root; +} + + +/// Gets the path to the Kyuafile to be loaded. +/// +/// This is just syntactic sugar to simplify quierying the 'kyuafile_option'. +/// +/// \param cmdline The parsed command line. +/// +/// \return The path to the Kyuafile to be loaded. +fs::path +cli::kyuafile_path(const cmdline::parsed_cmdline& cmdline) +{ + return cmdline.get_option< cmdline::path_option >( + kyuafile_option.long_name()); +} + + +/// Gets the value of the results-file flag for the creation of a new file. +/// +/// \param cmdline The parsed command line from which to extract any possible +/// override for the location of the database via the --results-file flag. +/// +/// \return The path to the database to be used. +/// +/// \throw cmdline::error If the value passed to the flag is invalid. +std::string +cli::results_file_create(const cmdline::parsed_cmdline& cmdline) +{ + std::string results_file = cmdline.get_option< cmdline::string_option >( + results_file_create_option.long_name()); + if (results_file == results_file_create_option.default_value()) { + const optional< fs::path > historical_db = get_historical_db(); + if (historical_db) + results_file = historical_db.get().str(); + } else { + try { + (void)fs::path(results_file); + } catch (const fs::error& e) { + throw cmdline::usage_error(F("Invalid value passed to --%s") % + results_file_create_option.long_name()); + } + } + return results_file; +} + + +/// Gets the value of the results-file flag for the lookup of the file. +/// +/// \param cmdline The parsed command line from which to extract any possible +/// override for the location of the database via the --results-file flag. +/// +/// \return The path to the database to be used. +/// +/// \throw cmdline::error If the value passed to the flag is invalid. +std::string +cli::results_file_open(const cmdline::parsed_cmdline& cmdline) +{ + std::string results_file = cmdline.get_option< cmdline::string_option >( + results_file_open_option.long_name()); + if (results_file == results_file_open_option.default_value()) { + const optional< fs::path > historical_db = get_historical_db(); + if (historical_db) + results_file = historical_db.get().str(); + } else { + try { + (void)fs::path(results_file); + } catch (const fs::error& e) { + throw cmdline::usage_error(F("Invalid value passed to --%s") % + results_file_open_option.long_name()); + } + } + return results_file; +} + + +/// Gets the filters for the result types. +/// +/// \param cmdline The parsed command line. +/// +/// \return A collection of result types to be used for filtering. +/// +/// \throw std::runtime_error If any of the user-provided filters is invalid. +cli::result_types +cli::get_result_types(const utils::cmdline::parsed_cmdline& cmdline) +{ + result_types types = parse_types( + cmdline.get_option< cmdline::list_option >("results-filter")); + if (types.empty()) { + types.push_back(model::test_result_passed); + types.push_back(model::test_result_skipped); + types.push_back(model::test_result_expected_failure); + types.push_back(model::test_result_broken); + types.push_back(model::test_result_failed); + } + return types; +} + + +/// Parses a set of command-line arguments to construct test filters. +/// +/// \param args The command-line arguments representing test filters. +/// +/// \return A set of test filters. +/// +/// \throw cmdline:error If any of the arguments is invalid, or if they +/// represent a non-disjoint collection of filters. +std::set< engine::test_filter > +cli::parse_filters(const cmdline::args_vector& args) +{ + std::set< engine::test_filter > filters; + + try { + for (cmdline::args_vector::const_iterator iter = args.begin(); + iter != args.end(); iter++) { + const engine::test_filter filter(engine::test_filter::parse(*iter)); + if (filters.find(filter) != filters.end()) + throw cmdline::error(F("Duplicate filter '%s'") % filter.str()); + filters.insert(filter); + } + check_disjoint_filters(filters); + } catch (const std::runtime_error& e) { + throw cmdline::error(e.what()); + } + + return filters; +} + + +/// Reports the filters that have not matched any tests as errors. +/// +/// \param unused The collection of unused filters to report. +/// \param ui The user interface object through which errors are to be reported. +/// +/// \return True if there are any unused filters. The caller should report this +/// as an error to the user by means of a non-successful exit code. +bool +cli::report_unused_filters(const std::set< engine::test_filter >& unused, + cmdline::ui* ui) +{ + for (std::set< engine::test_filter >::const_iterator iter = unused.begin(); + iter != unused.end(); iter++) { + cmdline::print_warning(ui, F("No test cases matched by the filter " + "'%s'.") % (*iter).str()); + } + + return !unused.empty(); +} + + +/// Formats a time delta for user presentation. +/// +/// \param delta The time delta to format. +/// +/// \return A user-friendly representation of the time delta. +std::string +cli::format_delta(const datetime::delta& delta) +{ + return F("%.3ss") % (delta.seconds + (delta.useconds / 1000000.0)); +} + + +/// Formats a test case result for user presentation. +/// +/// \param result The result to format. +/// +/// \return A user-friendly representation of the result. +std::string +cli::format_result(const model::test_result& result) +{ + std::string text; + + switch (result.type()) { + case model::test_result_broken: text = "broken"; break; + case model::test_result_expected_failure: text = "expected_failure"; break; + case model::test_result_failed: text = "failed"; break; + case model::test_result_passed: text = "passed"; break; + case model::test_result_skipped: text = "skipped"; break; + } + INV(!text.empty()); + + if (!result.reason().empty()) + text += ": " + result.reason(); + + return text; +} + + +/// Formats the identifier of a test case for user presentation. +/// +/// \param test_program The test program containing the test case. +/// \param test_case_name The name of the test case. +/// +/// \return A string representing the test case uniquely within a test suite. +std::string +cli::format_test_case_id(const model::test_program& test_program, + const std::string& test_case_name) +{ + return F("%s:%s") % test_program.relative_path() % test_case_name; +} + + +/// Formats a filter using the same syntax of a test case. +/// +/// \param test_filter The filter to format. +/// +/// \return A string representing the test filter. +std::string +cli::format_test_case_id(const engine::test_filter& test_filter) +{ + return F("%s:%s") % test_filter.test_program % test_filter.test_case; +} + + +/// Prints the version header information to the interface output. +/// +/// \param ui Interface to which to write the version details. +void +cli::write_version_header(utils::cmdline::ui* ui) +{ + ui->out(PACKAGE " (" PACKAGE_NAME ") " PACKAGE_VERSION); +} diff --git a/cli/common.hpp b/cli/common.hpp new file mode 100644 index 000000000000..15a7e9fa3344 --- /dev/null +++ b/cli/common.hpp @@ -0,0 +1,104 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/common.hpp +/// Utility functions to implement CLI subcommands. + +#if !defined(CLI_COMMON_HPP) +#define CLI_COMMON_HPP + +#include +#include +#include + +#include "engine/filters_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result.hpp" +#include "utils/cmdline/base_command.hpp" +#include "utils/cmdline/options_fwd.hpp" +#include "utils/cmdline/parser_fwd.hpp" +#include "utils/cmdline/ui_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace cli { + + +extern const utils::cmdline::path_option build_root_option; +extern const utils::cmdline::path_option kyuafile_option; +extern const utils::cmdline::string_option results_file_create_option; +extern const utils::cmdline::string_option results_file_open_option; +extern const utils::cmdline::list_option results_filter_option; +extern const utils::cmdline::property_option variable_option; + + +/// Base type for commands defined in the cli module. +/// +/// All commands in Kyua receive a configuration object as their runtime +/// data parameter because the configuration file applies to all the +/// commands. +typedef utils::cmdline::base_command< utils::config::tree > cli_command; + + +/// Scoped, strictly owned pointer to a cli_command. +typedef std::auto_ptr< cli_command > cli_command_ptr; + + +/// Collection of result types. +/// +/// This is a vector rather than a set because we want to respect the order in +/// which the user provided the types. +typedef std::vector< model::test_result_type > result_types; + + +utils::optional< utils::fs::path > build_root_path( + const utils::cmdline::parsed_cmdline&); +utils::fs::path kyuafile_path(const utils::cmdline::parsed_cmdline&); +std::string results_file_create(const utils::cmdline::parsed_cmdline&); +std::string results_file_open(const utils::cmdline::parsed_cmdline&); +result_types get_result_types(const utils::cmdline::parsed_cmdline&); + +std::set< engine::test_filter > parse_filters( + const utils::cmdline::args_vector&); +bool report_unused_filters(const std::set< engine::test_filter >&, + utils::cmdline::ui*); + +std::string format_delta(const utils::datetime::delta&); +std::string format_result(const model::test_result&); +std::string format_test_case_id(const model::test_program&, const std::string&); +std::string format_test_case_id(const engine::test_filter&); + + +void write_version_header(utils::cmdline::ui*); + + +} // namespace cli + +#endif // !defined(CLI_COMMON_HPP) diff --git a/cli/common.ipp b/cli/common.ipp new file mode 100644 index 000000000000..c0de4e44ccc1 --- /dev/null +++ b/cli/common.ipp @@ -0,0 +1,30 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/common.hpp" +#include "utils/cmdline/base_command.ipp" diff --git a/cli/common_test.cpp b/cli/common_test.cpp new file mode 100644 index 000000000000..05bb187ace22 --- /dev/null +++ b/cli/common_test.cpp @@ -0,0 +1,488 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/common.hpp" + +#include + +#include + +#include "engine/exceptions.hpp" +#include "engine/filters.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + +using utils::optional; + + +namespace { + + +/// Syntactic sugar to instantiate engine::test_filter objects. +/// +/// \param test_program Test program. +/// \param test_case Test case. +/// +/// \return A \code test_filter \endcode object, based on \p test_program and +/// \p test_case. +inline engine::test_filter +mkfilter(const char* test_program, const char* test_case) +{ + return engine::test_filter(fs::path(test_program), test_case); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(build_root_path__default); +ATF_TEST_CASE_BODY(build_root_path__default) +{ + std::map< std::string, std::vector< std::string > > options; + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE(!cli::build_root_path(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(build_root_path__explicit); +ATF_TEST_CASE_BODY(build_root_path__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["build-root"].push_back("/my//path"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE(cli::build_root_path(mock_cmdline)); + ATF_REQUIRE_EQ("/my/path", cli::build_root_path(mock_cmdline).get().str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile_path__default); +ATF_TEST_CASE_BODY(kyuafile_path__default) +{ + std::map< std::string, std::vector< std::string > > options; + options["kyuafile"].push_back(cli::kyuafile_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ(cli::kyuafile_option.default_value(), + cli::kyuafile_path(mock_cmdline).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile_path__explicit); +ATF_TEST_CASE_BODY(kyuafile_path__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["kyuafile"].push_back("/my//path"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ("/my/path", cli::kyuafile_path(mock_cmdline).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__default); +ATF_TEST_CASE_BODY(result_types__default) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back( + cli::results_filter_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_expected_failure); + exp_types.push_back(model::test_result_broken); + exp_types.push_back(model::test_result_failed); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__empty); +ATF_TEST_CASE_BODY(result_types__empty) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back(""); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_passed); + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_expected_failure); + exp_types.push_back(model::test_result_broken); + exp_types.push_back(model::test_result_failed); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__explicit__all); +ATF_TEST_CASE_BODY(result_types__explicit__all) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back("passed,skipped,xfail,broken,failed"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_passed); + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_expected_failure); + exp_types.push_back(model::test_result_broken); + exp_types.push_back(model::test_result_failed); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__explicit__some); +ATF_TEST_CASE_BODY(result_types__explicit__some) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back("skipped,broken"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_broken); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__explicit__invalid); +ATF_TEST_CASE_BODY(result_types__explicit__invalid) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back("skipped,foo,broken"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Unknown result type 'foo'", + cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_create__default__new); +ATF_TEST_CASE_BODY(results_file_create__default__new) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_create_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + + ATF_REQUIRE_EQ(cli::results_file_create_option.default_value(), + cli::results_file_create(mock_cmdline)); + ATF_REQUIRE(!fs::exists(home / ".kyua")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_create__default__historical); +ATF_TEST_CASE_BODY(results_file_create__default__historical) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_create_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + fs::mkdir_p(fs::path("homedir/.kyua"), 0755); + atf::utils::create_file("homedir/.kyua/store.db", "fake store"); + + ATF_REQUIRE_EQ(fs::path("homedir/.kyua/store.db").to_absolute(), + fs::path(cli::results_file_create(mock_cmdline))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_create__explicit); +ATF_TEST_CASE_BODY(results_file_create__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back("/my//path/f.db"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ("/my//path/f.db", + cli::results_file_create(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_open__default__latest); +ATF_TEST_CASE_BODY(results_file_open__default__latest) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_open_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + + ATF_REQUIRE_EQ(cli::results_file_open_option.default_value(), + cli::results_file_open(mock_cmdline)); + ATF_REQUIRE(!fs::exists(home / ".kyua")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_open__default__historical); +ATF_TEST_CASE_BODY(results_file_open__default__historical) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_open_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + fs::mkdir_p(fs::path("homedir/.kyua"), 0755); + atf::utils::create_file("homedir/.kyua/store.db", "fake store"); + + ATF_REQUIRE_EQ(fs::path("homedir/.kyua/store.db").to_absolute(), + fs::path(cli::results_file_open(mock_cmdline))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_open__explicit); +ATF_TEST_CASE_BODY(results_file_open__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back("/my//path/f.db"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ("/my//path/f.db", cli::results_file_open(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__none); +ATF_TEST_CASE_BODY(parse_filters__none) +{ + const cmdline::args_vector args; + const std::set< engine::test_filter > filters = cli::parse_filters(args); + ATF_REQUIRE(filters.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__ok); +ATF_TEST_CASE_BODY(parse_filters__ok) +{ + cmdline::args_vector args; + args.push_back("foo"); + args.push_back("bar/baz"); + args.push_back("other:abc"); + args.push_back("other:bcd"); + const std::set< engine::test_filter > filters = cli::parse_filters(args); + + std::set< engine::test_filter > exp_filters; + exp_filters.insert(mkfilter("foo", "")); + exp_filters.insert(mkfilter("bar/baz", "")); + exp_filters.insert(mkfilter("other", "abc")); + exp_filters.insert(mkfilter("other", "bcd")); + + ATF_REQUIRE(exp_filters == filters); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__duplicate); +ATF_TEST_CASE_BODY(parse_filters__duplicate) +{ + cmdline::args_vector args; + args.push_back("foo/bar//baz"); + args.push_back("hello/world:yes"); + args.push_back("foo//bar/baz"); + ATF_REQUIRE_THROW_RE(cmdline::error, "Duplicate.*'foo/bar/baz'", + cli::parse_filters(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__nondisjoint); +ATF_TEST_CASE_BODY(parse_filters__nondisjoint) +{ + cmdline::args_vector args; + args.push_back("foo/bar"); + args.push_back("hello/world:yes"); + args.push_back("foo/bar:baz"); + ATF_REQUIRE_THROW_RE(cmdline::error, "'foo/bar'.*'foo/bar:baz'.*disjoint", + cli::parse_filters(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_unused_filters__none); +ATF_TEST_CASE_BODY(report_unused_filters__none) +{ + std::set< engine::test_filter > unused; + + cmdline::ui_mock ui; + ATF_REQUIRE(!cli::report_unused_filters(unused, &ui)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_unused_filters__some); +ATF_TEST_CASE_BODY(report_unused_filters__some) +{ + std::set< engine::test_filter > unused; + unused.insert(mkfilter("a/b", "")); + unused.insert(mkfilter("hey/d", "yes")); + + cmdline::ui_mock ui; + cmdline::init("progname"); + ATF_REQUIRE(cli::report_unused_filters(unused, &ui)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE_EQ(2, ui.err_log().size()); + ATF_REQUIRE( atf::utils::grep_collection("No.*matched.*'a/b'", + ui.err_log())); + ATF_REQUIRE( atf::utils::grep_collection("No.*matched.*'hey/d:yes'", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_delta); +ATF_TEST_CASE_BODY(format_delta) +{ + ATF_REQUIRE_EQ("0.000s", cli::format_delta(datetime::delta())); + ATF_REQUIRE_EQ("0.012s", cli::format_delta(datetime::delta(0, 12300))); + ATF_REQUIRE_EQ("0.999s", cli::format_delta(datetime::delta(0, 999000))); + ATF_REQUIRE_EQ("51.321s", cli::format_delta(datetime::delta(51, 321000))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_result__no_reason); +ATF_TEST_CASE_BODY(format_result__no_reason) +{ + ATF_REQUIRE_EQ("passed", cli::format_result( + model::test_result(model::test_result_passed))); + ATF_REQUIRE_EQ("failed", cli::format_result( + model::test_result(model::test_result_failed))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_result__with_reason); +ATF_TEST_CASE_BODY(format_result__with_reason) +{ + ATF_REQUIRE_EQ("broken: Something", cli::format_result( + model::test_result(model::test_result_broken, "Something"))); + ATF_REQUIRE_EQ("expected_failure: A B C", cli::format_result( + model::test_result(model::test_result_expected_failure, "A B C"))); + ATF_REQUIRE_EQ("failed: More text", cli::format_result( + model::test_result(model::test_result_failed, "More text"))); + ATF_REQUIRE_EQ("skipped: Bye", cli::format_result( + model::test_result(model::test_result_skipped, "Bye"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_test_case_id__test_case); +ATF_TEST_CASE_BODY(format_test_case_id__test_case) +{ + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("foo/bar/baz"), fs::path("unused-root"), + "unused-suite-name") + .add_test_case("abc") + .build(); + ATF_REQUIRE_EQ("foo/bar/baz:abc", + cli::format_test_case_id(test_program, "abc")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_test_case_id__test_filter); +ATF_TEST_CASE_BODY(format_test_case_id__test_filter) +{ + const engine::test_filter filter(fs::path("foo/bar"), "baz"); + ATF_REQUIRE_EQ("foo/bar:baz", cli::format_test_case_id(filter)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(write_version_header); +ATF_TEST_CASE_BODY(write_version_header) +{ + cmdline::ui_mock ui; + cli::write_version_header(&ui); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_MATCH("^kyua .*[0-9]+\\.[0-9]+$", ui.out_log()[0]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, build_root_path__default); + ATF_ADD_TEST_CASE(tcs, build_root_path__explicit); + + ATF_ADD_TEST_CASE(tcs, kyuafile_path__default); + ATF_ADD_TEST_CASE(tcs, kyuafile_path__explicit); + + ATF_ADD_TEST_CASE(tcs, result_types__default); + ATF_ADD_TEST_CASE(tcs, result_types__empty); + ATF_ADD_TEST_CASE(tcs, result_types__explicit__all); + ATF_ADD_TEST_CASE(tcs, result_types__explicit__some); + ATF_ADD_TEST_CASE(tcs, result_types__explicit__invalid); + + ATF_ADD_TEST_CASE(tcs, results_file_create__default__new); + ATF_ADD_TEST_CASE(tcs, results_file_create__default__historical); + ATF_ADD_TEST_CASE(tcs, results_file_create__explicit); + + ATF_ADD_TEST_CASE(tcs, results_file_open__default__latest); + ATF_ADD_TEST_CASE(tcs, results_file_open__default__historical); + ATF_ADD_TEST_CASE(tcs, results_file_open__explicit); + + ATF_ADD_TEST_CASE(tcs, parse_filters__none); + ATF_ADD_TEST_CASE(tcs, parse_filters__ok); + ATF_ADD_TEST_CASE(tcs, parse_filters__duplicate); + ATF_ADD_TEST_CASE(tcs, parse_filters__nondisjoint); + + ATF_ADD_TEST_CASE(tcs, report_unused_filters__none); + ATF_ADD_TEST_CASE(tcs, report_unused_filters__some); + + ATF_ADD_TEST_CASE(tcs, format_delta); + + ATF_ADD_TEST_CASE(tcs, format_result__no_reason); + ATF_ADD_TEST_CASE(tcs, format_result__with_reason); + + ATF_ADD_TEST_CASE(tcs, format_test_case_id__test_case); + ATF_ADD_TEST_CASE(tcs, format_test_case_id__test_filter); + + ATF_ADD_TEST_CASE(tcs, write_version_header); +} diff --git a/cli/config.cpp b/cli/config.cpp new file mode 100644 index 000000000000..0049103706bf --- /dev/null +++ b/cli/config.cpp @@ -0,0 +1,223 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/config.hpp" + +#include "cli/common.hpp" +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/env.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; + +using utils::optional; + + +namespace { + + +/// Basename of the configuration file. +static const char* config_basename = "kyua.conf"; + + +/// Magic string to disable loading of configuration files. +static const char* none_config = "none"; + + +/// Textual description of the default configuration files. +/// +/// This is just an auxiliary string required to define the option below, which +/// requires a pointer to a static C string. +/// +/// \todo If the user overrides the KYUA_CONFDIR environment variable, we don't +/// reflect this fact here. We don't want to query the variable during program +/// initialization due to the side-effects it may have. Therefore, fixing this +/// is tricky as it may require a whole rethink of this module. +static const std::string config_lookup_names = + (fs::path("~/.kyua") / config_basename).str() + " or " + + (fs::path(KYUA_CONFDIR) / config_basename).str(); + + +/// Loads the configuration file for this session, if any. +/// +/// This is a helper function that does not apply user-specified overrides. See +/// the documentation for cli::load_config() for more details. +/// +/// \param cmdline The parsed command line. +/// +/// \return The loaded configuration file, or the configuration defaults if the +/// loading is disabled. +/// +/// \throw engine::error If the parsing of the configuration file fails. +/// TODO(jmmv): I'm not sure if this is the raised exception. And even if +/// it is, we should make it more accurate. +config::tree +load_config_file(const cmdline::parsed_cmdline& cmdline) +{ + // TODO(jmmv): We should really be able to use cmdline.has_option here to + // detect whether the option was provided or not instead of checking against + // the default value. + const fs::path filename = cmdline.get_option< cmdline::path_option >( + cli::config_option.long_name()); + if (filename.str() == none_config) { + LD("Configuration loading disabled; using defaults"); + return engine::default_config(); + } else if (filename.str() != cli::config_option.default_value()) + return engine::load_config(filename); + + const optional< fs::path > home = utils::get_home(); + if (home) { + const fs::path path = home.get() / ".kyua" / config_basename; + try { + if (fs::exists(path)) + return engine::load_config(path); + } catch (const fs::error& e) { + // Fall through. If we fail to load the user-specific configuration + // file because it cannot be openend, we try to load the system-wide + // one. + LW(F("Failed to load user-specific configuration file '%s': %s") % + path % e.what()); + } + } + + const fs::path confdir(utils::getenv_with_default( + "KYUA_CONFDIR", KYUA_CONFDIR)); + + const fs::path path = confdir / config_basename; + if (fs::exists(path)) { + return engine::load_config(path); + } else { + return engine::default_config(); + } +} + + +/// Loads the configuration file for this session, if any. +/// +/// This is a helper function for cli::load_config() that attempts to load the +/// configuration unconditionally. +/// +/// \param cmdline The parsed command line. +/// +/// \return The loaded configuration file data. +/// +/// \throw engine::error If the parsing of the configuration file fails. +static config::tree +load_required_config(const cmdline::parsed_cmdline& cmdline) +{ + config::tree user_config = load_config_file(cmdline); + + if (cmdline.has_option(cli::variable_option.long_name())) { + typedef std::pair< std::string, std::string > override_pair; + + const std::vector< override_pair >& overrides = + cmdline.get_multi_option< cmdline::property_option >( + cli::variable_option.long_name()); + + for (std::vector< override_pair >::const_iterator + iter = overrides.begin(); iter != overrides.end(); iter++) { + try { + user_config.set_string((*iter).first, (*iter).second); + } catch (const config::error& e) { + // TODO(jmmv): Raising this type from here is obviously the + // wrong thing to do. + throw engine::error(e.what()); + } + } + } + + return user_config; +} + + +} // anonymous namespace + + +/// Standard definition of the option to specify a configuration file. +/// +/// You must use load_config() to load a configuration file while honoring the +/// value of this flag. +const cmdline::path_option cli::config_option( + 'c', "config", + (std::string("Path to the configuration file; '") + none_config + + "' to disable loading").c_str(), + "file", config_lookup_names.c_str()); + + +/// Standard definition of the option to specify a configuration variable. +const cmdline::property_option cli::variable_option( + 'v', "variable", + "Overrides a particular configuration variable", + "K=V"); + + +/// Loads the configuration file for this session, if any. +/// +/// The algorithm implemented here is as follows: +/// 1) If ~/.kyua/kyua.conf exists, load it. +/// 2) Otherwise, if sysconfdir/kyua.conf exists, load it. +/// 3) Otherwise, use the built-in settings. +/// 4) Lastly, apply any user-provided overrides. +/// +/// \param cmdline The parsed command line. +/// \param required Whether the loading of the configuration file must succeed. +/// Some commands should run regardless, and therefore we need to set this +/// to false for those commands. +/// +/// \return The loaded configuration file data. If required was set to false, +/// this might be the default configuration data if the requested file could not +/// be properly loaded. +/// +/// \throw engine::error If the parsing of the configuration file fails. +config::tree +cli::load_config(const cmdline::parsed_cmdline& cmdline, + const bool required) +{ + try { + return load_required_config(cmdline); + } catch (const engine::error& e) { + if (required) { + throw; + } else { + LW(F("Ignoring failure to load configuration because the requested " + "command should not fail: %s") % e.what()); + return engine::default_config(); + } + } +} diff --git a/cli/config.hpp b/cli/config.hpp new file mode 100644 index 000000000000..d948208ee5d0 --- /dev/null +++ b/cli/config.hpp @@ -0,0 +1,55 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/config.hpp +/// Utility functions to load configuration files. +/// +/// \todo All this should probably just be merged into the main module +/// as nothing else should have access to this. + +#if !defined(CLI_CONFIG_HPP) +#define CLI_CONFIG_HPP + +#include "utils/cmdline/options_fwd.hpp" +#include "utils/cmdline/parser_fwd.hpp" +#include "utils/config/tree_fwd.hpp" + +namespace cli { + + +extern const utils::cmdline::path_option config_option; +extern const utils::cmdline::property_option variable_option; + + +utils::config::tree load_config(const utils::cmdline::parsed_cmdline&, + const bool); + + +} // namespace cli + +#endif // !defined(CLI_CONFIG_HPP) diff --git a/cli/config_test.cpp b/cli/config_test.cpp new file mode 100644 index 000000000000..7a20c2941d8c --- /dev/null +++ b/cli/config_test.cpp @@ -0,0 +1,351 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/config.hpp" + +#include + +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Creates a configuration file for testing purposes. +/// +/// To ensure that the loaded file is the one created by this function, use +/// validate_mock_config(). +/// +/// \param name The name of the configuration file to create. +/// \param cookie The magic value to set in the configuration file, or NULL if a +/// broken configuration file is desired. +static void +create_mock_config(const char* name, const char* cookie) +{ + if (cookie != NULL) { + atf::utils::create_file( + name, + F("syntax(2)\n" + "test_suites.suite.magic_value = '%s'\n") % cookie); + } else { + atf::utils::create_file(name, "syntax(200)\n"); + } +} + + +/// Creates an invalid system configuration. +/// +/// \param cookie The magic value to set in the configuration file, or NULL if a +/// broken configuration file is desired. +static void +mock_system_config(const char* cookie) +{ + fs::mkdir(fs::path("system-dir"), 0755); + utils::setenv("KYUA_CONFDIR", (fs::current_path() / "system-dir").str()); + create_mock_config("system-dir/kyua.conf", cookie); +} + + +/// Creates an invalid user configuration. +/// +/// \param cookie The magic value to set in the configuration file, or NULL if a +/// broken configuration file is desired. +static void +mock_user_config(const char* cookie) +{ + fs::mkdir(fs::path("user-dir"), 0755); + fs::mkdir(fs::path("user-dir/.kyua"), 0755); + utils::setenv("HOME", (fs::current_path() / "user-dir").str()); + create_mock_config("user-dir/.kyua/kyua.conf", cookie); +} + + +/// Ensures that a loaded configuration was created with create_mock_config(). +/// +/// \param user_config The configuration to validate. +/// \param cookie The magic value to expect in the configuration file. +static void +validate_mock_config(const config::tree& user_config, const char* cookie) +{ + const config::properties_map& properties = user_config.all_properties( + "test_suites.suite", true); + const config::properties_map::const_iterator iter = + properties.find("magic_value"); + ATF_REQUIRE(iter != properties.end()); + ATF_REQUIRE_EQ(cookie, (*iter).second); +} + + +/// Ensures that two configuration trees are equal. +/// +/// \param exp_tree The expected configuration tree. +/// \param actual_tree The configuration tree being validated against exp_tree. +static void +require_eq(const config::tree& exp_tree, const config::tree& actual_tree) +{ + ATF_REQUIRE(exp_tree.all_properties() == actual_tree.all_properties()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__none); +ATF_TEST_CASE_BODY(load_config__none) +{ + utils::setenv("KYUA_CONFDIR", "/the/system/does/not/exist"); + utils::setenv("HOME", "/the/user/does/not/exist"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + require_eq(engine::default_config(), + cli::load_config(mock_cmdline, true)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__explicit__ok); +ATF_TEST_CASE_BODY(load_config__explicit__ok) +{ + mock_system_config(NULL); + mock_user_config(NULL); + + create_mock_config("test-file", "hello"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("test-file"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "hello"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__explicit__disable); +ATF_TEST_CASE_BODY(load_config__explicit__disable) +{ + mock_system_config(NULL); + mock_user_config(NULL); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("none"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + require_eq(engine::default_config(), + cli::load_config(mock_cmdline, true)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__explicit__fail); +ATF_TEST_CASE_BODY(load_config__explicit__fail) +{ + mock_system_config("ok1"); + mock_user_config("ok2"); + + create_mock_config("test-file", NULL); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("test-file"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "200", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__user__ok); +ATF_TEST_CASE_BODY(load_config__user__ok) +{ + mock_system_config(NULL); + mock_user_config("I am the user config"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "I am the user config"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__user__fail); +ATF_TEST_CASE_BODY(load_config__user__fail) +{ + mock_system_config("valid"); + mock_user_config(NULL); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "200", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__user__bad_home); +ATF_TEST_CASE_BODY(load_config__user__bad_home) +{ + mock_system_config("Fallback system config"); + utils::setenv("HOME", ""); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "Fallback system config"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__system__ok); +ATF_TEST_CASE_BODY(load_config__system__ok) +{ + mock_system_config("I am the system config"); + utils::setenv("HOME", "/the/user/does/not/exist"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "I am the system config"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__system__fail); +ATF_TEST_CASE_BODY(load_config__system__fail) +{ + mock_system_config(NULL); + utils::setenv("HOME", "/the/user/does/not/exist"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "200", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__overrides__no); +ATF_TEST_CASE_BODY(load_config__overrides__no) +{ + utils::setenv("KYUA_CONFDIR", fs::current_path().str()); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + options["variable"].push_back("architecture=1"); + options["variable"].push_back("platform=2"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + ATF_REQUIRE_EQ("1", + user_config.lookup< config::string_node >("architecture")); + ATF_REQUIRE_EQ("2", + user_config.lookup< config::string_node >("platform")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__overrides__yes); +ATF_TEST_CASE_BODY(load_config__overrides__yes) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "architecture = 'do not see me'\n" + "platform = 'see me'\n"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("config"); + options["variable"].push_back("architecture=overriden"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + ATF_REQUIRE_EQ("overriden", + user_config.lookup< config::string_node >("architecture")); + ATF_REQUIRE_EQ("see me", + user_config.lookup< config::string_node >("platform")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__overrides__fail); +ATF_TEST_CASE_BODY(load_config__overrides__fail) +{ + utils::setenv("KYUA_CONFDIR", fs::current_path().str()); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + options["variable"].push_back(".a=d"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "Empty component in key.*'\\.a'", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, load_config__none); + ATF_ADD_TEST_CASE(tcs, load_config__explicit__ok); + ATF_ADD_TEST_CASE(tcs, load_config__explicit__disable); + ATF_ADD_TEST_CASE(tcs, load_config__explicit__fail); + ATF_ADD_TEST_CASE(tcs, load_config__user__ok); + ATF_ADD_TEST_CASE(tcs, load_config__user__fail); + ATF_ADD_TEST_CASE(tcs, load_config__user__bad_home); + ATF_ADD_TEST_CASE(tcs, load_config__system__ok); + ATF_ADD_TEST_CASE(tcs, load_config__system__fail); + ATF_ADD_TEST_CASE(tcs, load_config__overrides__no); + ATF_ADD_TEST_CASE(tcs, load_config__overrides__yes); + ATF_ADD_TEST_CASE(tcs, load_config__overrides__fail); +} diff --git a/cli/main.cpp b/cli/main.cpp new file mode 100644 index 000000000000..531c252b0a75 --- /dev/null +++ b/cli/main.cpp @@ -0,0 +1,356 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/main.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include +#include +} + +#include +#include +#include +#include + +#include "cli/cmd_about.hpp" +#include "cli/cmd_config.hpp" +#include "cli/cmd_db_exec.hpp" +#include "cli/cmd_db_migrate.hpp" +#include "cli/cmd_debug.hpp" +#include "cli/cmd_help.hpp" +#include "cli/cmd_list.hpp" +#include "cli/cmd_report.hpp" +#include "cli/cmd_report_html.hpp" +#include "cli/cmd_report_junit.hpp" +#include "cli/cmd_test.hpp" +#include "cli/common.ipp" +#include "cli/config.hpp" +#include "engine/atf.hpp" +#include "engine/plain.hpp" +#include "engine/scheduler.hpp" +#include "engine/tap.hpp" +#include "store/exceptions.hpp" +#include "utils/cmdline/commands_map.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace signals = utils::signals; +namespace scheduler = engine::scheduler; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Registers all valid scheduler interfaces. +/// +/// This is part of Kyua's setup but it is a bit strange to find it here. I am +/// not sure what a better location would be though, so for now this is good +/// enough. +static void +register_scheduler_interfaces(void) +{ + scheduler::register_interface( + "atf", std::shared_ptr< scheduler::interface >( + new engine::atf_interface())); + scheduler::register_interface( + "plain", std::shared_ptr< scheduler::interface >( + new engine::plain_interface())); + scheduler::register_interface( + "tap", std::shared_ptr< scheduler::interface >( + new engine::tap_interface())); +} + + +/// Executes the given subcommand with proper usage_error reporting. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param command The subcommand to execute. +/// \param args The part of the command line passed to the subcommand. The +/// first item of this collection must match the command name. +/// \param user_config The runtime configuration to pass to the subcommand. +/// +/// \return The exit code of the command. Typically 0 on success, some other +/// integer otherwise. +/// +/// \throw cmdline::usage_error If the user input to the subcommand is invalid. +/// This error does not encode the command name within it, so this function +/// extends the message in the error to specify which subcommand was +/// affected. +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +static int +run_subcommand(cmdline::ui* ui, cli::cli_command* command, + const cmdline::args_vector& args, + const config::tree& user_config) +{ + try { + PRE(command->name() == args[0]); + return command->main(ui, args, user_config); + } catch (const cmdline::usage_error& e) { + throw std::pair< std::string, cmdline::usage_error >( + command->name(), e); + } +} + + +/// Exception-safe version of main. +/// +/// This function provides the real meat of the entry point of the program. It +/// is allowed to throw some known exceptions which are parsed by the caller. +/// Doing so keeps this function simpler and allow tests to actually validate +/// that the errors reported are accurate. +/// +/// \return The exit code of the program. Should be EXIT_SUCCESS on success and +/// EXIT_FAILURE on failure. The caller extends this to additional integers for +/// errors reported through exceptions. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// \param mock_command An extra command provided for testing purposes; should +/// just be NULL other than for tests. +/// +/// \throw cmdline::usage_error If the user ran the program with invalid +/// arguments. +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +static int +safe_main(cmdline::ui* ui, int argc, const char* const argv[], + cli::cli_command_ptr mock_command) +{ + cmdline::options_vector options; + options.push_back(&cli::config_option); + options.push_back(&cli::variable_option); + const cmdline::string_option loglevel_option( + "loglevel", "Level of the messages to log", "level", "info"); + options.push_back(&loglevel_option); + const cmdline::path_option logfile_option( + "logfile", "Path to the log file", "file", + cli::detail::default_log_name().c_str()); + options.push_back(&logfile_option); + + cmdline::commands_map< cli::cli_command > commands; + + commands.insert(new cli::cmd_about()); + commands.insert(new cli::cmd_config()); + commands.insert(new cli::cmd_db_exec()); + commands.insert(new cli::cmd_db_migrate()); + commands.insert(new cli::cmd_help(&options, &commands)); + + commands.insert(new cli::cmd_debug(), "Workspace"); + commands.insert(new cli::cmd_list(), "Workspace"); + commands.insert(new cli::cmd_test(), "Workspace"); + + commands.insert(new cli::cmd_report(), "Reporting"); + commands.insert(new cli::cmd_report_html(), "Reporting"); + commands.insert(new cli::cmd_report_junit(), "Reporting"); + + if (mock_command.get() != NULL) + commands.insert(mock_command); + + const cmdline::parsed_cmdline cmdline = cmdline::parse(argc, argv, options); + + const fs::path logfile(cmdline.get_option< cmdline::path_option >( + "logfile")); + fs::mkdir_p(logfile.branch_path(), 0755); + LD(F("Log file is %s") % logfile); + utils::install_crash_handlers(logfile.str()); + try { + logging::set_persistency(cmdline.get_option< cmdline::string_option >( + "loglevel"), logfile); + } catch (const std::range_error& e) { + throw cmdline::usage_error(e.what()); + } + + if (cmdline.arguments().empty()) + throw cmdline::usage_error("No command provided"); + const std::string cmdname = cmdline.arguments()[0]; + + const config::tree user_config = cli::load_config(cmdline, + cmdname != "help"); + + cli::cli_command* command = commands.find(cmdname); + if (command == NULL) + throw cmdline::usage_error(F("Unknown command '%s'") % cmdname); + register_scheduler_interfaces(); + return run_subcommand(ui, command, cmdline.arguments(), user_config); +} + + +} // anonymous namespace + + +/// Gets the name of the default log file. +/// +/// \return The path to the log file. +fs::path +cli::detail::default_log_name(void) +{ + // Update doc/troubleshooting.texi if you change this algorithm. + const optional< std::string > home(utils::getenv("HOME")); + if (home) { + return logging::generate_log_name(fs::path(home.get()) / ".kyua" / + "logs", cmdline::progname()); + } else { + const optional< std::string > tmpdir(utils::getenv("TMPDIR")); + if (tmpdir) { + return logging::generate_log_name(fs::path(tmpdir.get()), + cmdline::progname()); + } else { + return logging::generate_log_name(fs::path("/tmp"), + cmdline::progname()); + } + } +} + + +/// Testable entry point, with catch-all exception handlers. +/// +/// This entry point does not perform any initialization of global state; it is +/// provided to allow unit-testing of the utility's entry point. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// \param mock_command An extra command provided for testing purposes; should +/// just be NULL other than for tests. +/// +/// \return 0 on success, some other integer on error. +/// +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +int +cli::main(cmdline::ui* ui, const int argc, const char* const* const argv, + cli_command_ptr mock_command) +{ + try { + const int exit_code = safe_main(ui, argc, argv, mock_command); + + // Codes above 1 are reserved to report conditions captured as + // exceptions below. + INV(exit_code == EXIT_SUCCESS || exit_code == EXIT_FAILURE); + + return exit_code; + } catch (const signals::interrupted_error& e) { + cmdline::print_error(ui, F("%s.") % e.what()); + // Re-deliver the interruption signal to self so that we terminate with + // the right status. At this point we should NOT have any custom signal + // handlers in place. + ::kill(getpid(), e.signo()); + LD("Interrupt signal re-delivery did not terminate program"); + // If we reach this, something went wrong because we did not exit as + // intended. Return an internal error instead. (Would be nicer to + // abort in principle, but it wouldn't be a nice experience if it ever + // happened.) + return 2; + } catch (const std::pair< std::string, cmdline::usage_error >& e) { + const std::string message = F("Usage error for command %s: %s.") % + e.first % e.second.what(); + LE(message); + ui->err(message); + ui->err(F("Type '%s help %s' for usage information.") % + cmdline::progname() % e.first); + return 3; + } catch (const cmdline::usage_error& e) { + const std::string message = F("Usage error: %s.") % e.what(); + LE(message); + ui->err(message); + ui->err(F("Type '%s help' for usage information.") % + cmdline::progname()); + return 3; + } catch (const store::old_schema_error& e) { + const std::string message = F("The database has schema version %s, " + "which is too old; please use db-migrate " + "to upgrade it.") % e.old_version(); + cmdline::print_error(ui, message); + return 2; + } catch (const std::runtime_error& e) { + cmdline::print_error(ui, F("%s.") % e.what()); + return 2; + } +} + + +/// Delegate for ::main(). +/// +/// This function is supposed to be called directly from the top-level ::main() +/// function. It takes care of initializing internal libraries and then calls +/// main(ui, argc, argv). +/// +/// \pre This function can only be called once. +/// +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +int +cli::main(const int argc, const char* const* const argv) +{ + logging::set_inmemory(); + + LI(F("%s %s") % PACKAGE % VERSION); + + std::string plain_args; + for (const char* const* arg = argv; *arg != NULL; arg++) + plain_args += F(" %s") % *arg; + LI(F("Command line:%s") % plain_args); + + cmdline::init(argv[0]); + cmdline::ui ui; + + const int exit_code = main(&ui, argc, argv); + LI(F("Clean exit with code %s") % exit_code); + return exit_code; +} diff --git a/cli/main.hpp b/cli/main.hpp new file mode 100644 index 000000000000..00e53c5a4ab2 --- /dev/null +++ b/cli/main.hpp @@ -0,0 +1,61 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file cli/main.hpp +/// Entry point for the program. +/// +/// These entry points are separate from the top-level ::main() function to +/// allow unit-testing of the main code. + +#if !defined(CLI_MAIN_HPP) +#define CLI_MAIN_HPP + +#include "cli/common.hpp" +#include "utils/cmdline/ui_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace cli { + + +namespace detail { + + +utils::fs::path default_log_name(void); + + +} // namespace detail + + +int main(utils::cmdline::ui*, const int, const char* const* const, + cli_command_ptr = cli_command_ptr()); +int main(const int, const char* const* const); + + +} // namespace cli + +#endif // !defined(CLI_MAIN_HPP) diff --git a/cli/main_test.cpp b/cli/main_test.cpp new file mode 100644 index 000000000000..70d167ff6963 --- /dev/null +++ b/cli/main_test.cpp @@ -0,0 +1,489 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/main.hpp" + +extern "C" { +#include +} + +#include + +#include + +#include "utils/cmdline/base_command.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/logging/operations.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/test_utils.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace process = utils::process; + + +namespace { + + +/// Fake command implementation that crashes during its execution. +class cmd_mock_crash : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// All command parameters are set to irrelevant values. + cmd_mock_crash(void) : + cli::cli_command("mock_error", "", 0, 0, "Mock command that crashes") + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function always aborts. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + utils::abort_without_coredump(); + } +}; + + +/// Fake command implementation that throws an exception during its execution. +class cmd_mock_error : public cli::cli_command { + /// Whether the command raises an exception captured by the parent or not. + /// + /// If this is true, the command will raise a std::runtime_error exception + /// or a subclass of it. The main program is in charge of capturing these + /// and reporting them appropriately. If false, this raises another + /// exception that does not inherit from std::runtime_error. + bool _unhandled; + +public: + /// Constructs a new mock command. + /// + /// \param unhandled If true, make run raise an exception not catched by the + /// main program. + cmd_mock_error(const bool unhandled) : + cli::cli_command("mock_error", "", 0, 0, + "Mock command that raises an error"), + _unhandled(unhandled) + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function always aborts. + /// + /// \throw std::logic_error If _unhandled is true. + /// \throw std::runtime_error If _unhandled is false. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + if (_unhandled) + throw std::logic_error("This is unhandled"); + else + throw std::runtime_error("Runtime error"); + } +}; + + +/// Fake command implementation that prints messages during its execution. +class cmd_mock_write : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// All command parameters are set to irrelevant values. + cmd_mock_write(void) : cli::cli_command( + "mock_write", "", 0, 0, "Mock command that prints output") + { + } + + /// Runs the mock command. + /// + /// \param ui Object to interact with the I/O of the program. + /// + /// \return Nothing because this function always aborts. + int + run(cmdline::ui* ui, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + ui->out("stdout message from subcommand"); + ui->err("stderr message from subcommand"); + return EXIT_FAILURE; + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__default_log_name__home); +ATF_TEST_CASE_BODY(detail__default_log_name__home) +{ + datetime::set_mock_now(2011, 2, 21, 21, 10, 30, 0); + cmdline::init("progname1"); + + utils::setenv("HOME", "/home//fake"); + utils::setenv("TMPDIR", "/do/not/use/this"); + ATF_REQUIRE_EQ( + fs::path("/home/fake/.kyua/logs/progname1.20110221-211030.log"), + cli::detail::default_log_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__default_log_name__tmpdir); +ATF_TEST_CASE_BODY(detail__default_log_name__tmpdir) +{ + datetime::set_mock_now(2011, 2, 21, 21, 10, 50, 987); + cmdline::init("progname2"); + + utils::unsetenv("HOME"); + utils::setenv("TMPDIR", "/a/b//c"); + ATF_REQUIRE_EQ(fs::path("/a/b/c/progname2.20110221-211050.log"), + cli::detail::default_log_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__default_log_name__hardcoded); +ATF_TEST_CASE_BODY(detail__default_log_name__hardcoded) +{ + datetime::set_mock_now(2011, 2, 21, 21, 15, 00, 123456); + cmdline::init("progname3"); + + utils::unsetenv("HOME"); + utils::unsetenv("TMPDIR"); + ATF_REQUIRE_EQ(fs::path("/tmp/progname3.20110221-211500.log"), + cli::detail::default_log_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__no_args); +ATF_TEST_CASE_BODY(main__no_args) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection("Usage error: No command provided", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Type.*progname help", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__unknown_command); +ATF_TEST_CASE_BODY(main__unknown_command) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "foo", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection("Usage error: Unknown command.*foo", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Type.*progname help", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__logfile__default); +ATF_TEST_CASE_BODY(main__logfile__default) +{ + logging::set_inmemory(); + datetime::set_mock_now(2011, 2, 21, 21, 30, 00, 0); + cmdline::init("progname"); + + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE(!fs::exists(fs::path( + ".kyua/logs/progname.20110221-213000.log"))); + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(fs::exists(fs::path( + ".kyua/logs/progname.20110221-213000.log"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__logfile__override); +ATF_TEST_CASE_BODY(main__logfile__override) +{ + logging::set_inmemory(); + datetime::set_mock_now(2011, 2, 21, 21, 30, 00, 321); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "--logfile=test.log", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE(!fs::exists(fs::path("test.log"))); + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(!fs::exists(fs::path( + ".kyua/logs/progname.20110221-213000.log"))); + ATF_REQUIRE(fs::exists(fs::path("test.log"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__default); +ATF_TEST_CASE_BODY(main__loglevel__default) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "--logfile=test.log", NULL}; + + LD("Mock debug message"); + LE("Mock error message"); + LI("Mock info message"); + LW("Mock warning message"); + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(!atf::utils::grep_file("Mock debug message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock error message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock info message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock warning message", "test.log")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__higher); +ATF_TEST_CASE_BODY(main__loglevel__higher) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "--logfile=test.log", + "--loglevel=debug", NULL}; + + LD("Mock debug message"); + LE("Mock error message"); + LI("Mock info message"); + LW("Mock warning message"); + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(atf::utils::grep_file("Mock debug message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock error message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock info message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock warning message", "test.log")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__lower); +ATF_TEST_CASE_BODY(main__loglevel__lower) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "--logfile=test.log", + "--loglevel=warning", NULL}; + + LD("Mock debug message"); + LE("Mock error message"); + LI("Mock info message"); + LW("Mock warning message"); + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(!atf::utils::grep_file("Mock debug message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock error message", "test.log")); + ATF_REQUIRE(!atf::utils::grep_file("Mock info message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock warning message", "test.log")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__error); +ATF_TEST_CASE_BODY(main__loglevel__error) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "--logfile=test.log", + "--loglevel=i-am-invalid", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(atf::utils::grep_collection("Usage error.*i-am-invalid", + ui.err_log())); + ATF_REQUIRE(!fs::exists(fs::path("test.log"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__ok); +ATF_TEST_CASE_BODY(main__subcommand__ok) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_write", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, + cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_write()))); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("stdout message from subcommand", ui.out_log()[0]); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("stderr message from subcommand", ui.err_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__invalid_args); +ATF_TEST_CASE_BODY(main__subcommand__invalid_args) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "mock_write", "bar", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, + cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_write()))); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection( + "Usage error for command mock_write: Too many arguments.", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Type.*progname help", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__runtime_error); +ATF_TEST_CASE_BODY(main__subcommand__runtime_error) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_error", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(2, cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_error(false)))); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection("progname: E: Runtime error.", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__unhandled_exception); +ATF_TEST_CASE_BODY(main__subcommand__unhandled_exception) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_error", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_THROW(std::logic_error, cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_error(true)))); +} + + +static void +do_subcommand_crash(void) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_error", NULL}; + + cmdline::ui_mock ui; + cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_crash())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__crash); +ATF_TEST_CASE_BODY(main__subcommand__crash) +{ + const process::status status = process::child::fork_files( + do_subcommand_crash, fs::path("stdout.txt"), + fs::path("stderr.txt"))->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(atf::utils::grep_file("Fatal signal", "stderr.txt")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, detail__default_log_name__home); + ATF_ADD_TEST_CASE(tcs, detail__default_log_name__tmpdir); + ATF_ADD_TEST_CASE(tcs, detail__default_log_name__hardcoded); + + ATF_ADD_TEST_CASE(tcs, main__no_args); + ATF_ADD_TEST_CASE(tcs, main__unknown_command); + ATF_ADD_TEST_CASE(tcs, main__logfile__default); + ATF_ADD_TEST_CASE(tcs, main__logfile__override); + ATF_ADD_TEST_CASE(tcs, main__loglevel__default); + ATF_ADD_TEST_CASE(tcs, main__loglevel__higher); + ATF_ADD_TEST_CASE(tcs, main__loglevel__lower); + ATF_ADD_TEST_CASE(tcs, main__loglevel__error); + ATF_ADD_TEST_CASE(tcs, main__subcommand__ok); + ATF_ADD_TEST_CASE(tcs, main__subcommand__invalid_args); + ATF_ADD_TEST_CASE(tcs, main__subcommand__runtime_error); + ATF_ADD_TEST_CASE(tcs, main__subcommand__unhandled_exception); + ATF_ADD_TEST_CASE(tcs, main__subcommand__crash); +} diff --git a/configure.ac b/configure.ac new file mode 100644 index 000000000000..a0df977c5226 --- /dev/null +++ b/configure.ac @@ -0,0 +1,173 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +AC_INIT([Kyua], [0.14], [kyua-discuss@googlegroups.com], [kyua], + [https://github.com/jmmv/kyua/]) +AC_PREREQ([2.65]) + + +AC_COPYRIGHT([Copyright 2010 The Kyua Authors.]) +AC_CONFIG_AUX_DIR([admin]) +AC_CONFIG_FILES([Doxyfile Makefile utils/defs.hpp]) +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_MACRO_DIR([m4]) +AC_CONFIG_SRCDIR([main.cpp]) +AC_CONFIG_TESTDIR([bootstrap]) + + +AM_INIT_AUTOMAKE([1.9 foreign subdir-objects -Wall]) + + +AC_LANG([C++]) +AC_PROG_CXX +AX_CXX_COMPILE_STDCXX([11], [noext], [mandatory]) +m4_ifdef([AM_PROG_AR], [AM_PROG_AR]) +KYUA_DEVELOPER_MODE([C++]) +KYUA_ATTRIBUTE_NORETURN +KYUA_ATTRIBUTE_PURE +KYUA_ATTRIBUTE_UNUSED +KYUA_FS_MODULE +KYUA_GETOPT +KYUA_LAST_SIGNO +KYUA_MEMORY +AC_CHECK_FUNCS([putenv setenv unsetenv]) +AC_CHECK_HEADERS([termios.h]) + + +AC_PROG_RANLIB + + +m4_ifndef([PKG_CHECK_MODULES], + [m4_fatal([Cannot find pkg.m4; see the INSTALL document for help])]) + +m4_ifndef([ATF_CHECK_CXX], + [m4_fatal([Cannot find atf-c++.m4; see the INSTALL document for help])]) +ATF_CHECK_CXX([>= 0.17]) +m4_ifndef([ATF_CHECK_SH], + [m4_fatal([Cannot find atf-sh.m4; see the INSTALL document for help])]) +ATF_CHECK_SH([>= 0.15]) +m4_ifndef([ATF_ARG_WITH], + [m4_fatal([Cannot find atf-common.m4; see the INSTALL document for help])]) +ATF_ARG_WITH + +PKG_CHECK_MODULES([LUTOK], [lutok >= 0.4], + [], + AC_MSG_ERROR([lutok (0.4 or newer) is required])) +PKG_CHECK_MODULES([SQLITE3], [sqlite3 >= 3.6.22], + [], + AC_MSG_ERROR([sqlite3 (3.6.22 or newer) is required])) +KYUA_DOXYGEN +AC_PATH_PROG([GDB], [gdb]) +test -n "${GDB}" || GDB=gdb +AC_PATH_PROG([GIT], [git]) + + +KYUA_UNAME_ARCHITECTURE +KYUA_UNAME_PLATFORM + + +AC_ARG_VAR([KYUA_CONFSUBDIR], + [Subdirectory of sysconfdir under which to look for files]) +if test x"${KYUA_CONFSUBDIR-unset}" = x"unset"; then + KYUA_CONFSUBDIR=kyua +else + case ${KYUA_CONFSUBDIR} in + /*) + AC_MSG_ERROR([KYUA_CONFSUBDIR must hold a relative path]) + ;; + *) + ;; + esac +fi +if test x"${KYUA_CONFSUBDIR}" = x""; then + AC_SUBST(kyua_confdir, \${sysconfdir}) +else + AC_SUBST(kyua_confdir, \${sysconfdir}/${KYUA_CONFSUBDIR}) +fi + + +dnl Allow the caller of 'make check', 'make installcheck' and 'make distcheck' +dnl on the Kyua source tree to override the configuration file passed to our +dnl own test runs. This is for the development of Kyua only and the value of +dnl this setting has no effect on the built product in any way. If we go +dnl through great extents in validating the value of this setting, it is to +dnl minimize the chance of false test run negatives later on. +AC_ARG_VAR([KYUA_CONFIG_FILE_FOR_CHECK], + [kyua.conf file to use at 'make (|dist|install)check' time]) +case "${KYUA_CONFIG_FILE_FOR_CHECK-none}" in +none) + KYUA_CONFIG_FILE_FOR_CHECK=none + ;; +/*) + if test -f "${KYUA_CONFIG_FILE_FOR_CHECK}"; then + : # All good! + else + AC_MSG_ERROR([KYUA_CONFIG_FILE_FOR_CHECK file does not exist]) + fi + ;; +*) + AC_MSG_ERROR([KYUA_CONFIG_FILE_FOR_CHECK must hold an absolute path]) + ;; +esac + + +AC_ARG_VAR([KYUA_TMPDIR], + [Path to the directory in which to place work directories]) +case "${KYUA_TMPDIR:-unset}" in + unset) + KYUA_TMPDIR=/tmp + ;; + /*) + ;; + *) + AC_MSG_ERROR([KYUA_TMPDIR must be an absolute path]) + ;; +esac + + +AC_SUBST(examplesdir, \${pkgdatadir}/examples) +AC_SUBST(luadir, \${pkgdatadir}/lua) +AC_SUBST(miscdir, \${pkgdatadir}/misc) +AC_SUBST(pkgtestsdir, \${testsdir}/kyua) +AC_SUBST(storedir, \${pkgdatadir}/store) +AC_SUBST(testsdir, \${exec_prefix}/tests) + + +dnl BSD make(1) doesn't deal with targets specified as './foo' well: they +dnl need to be specified as 'foo'. The following hack is to workaround this +dnl issue. +if test "${srcdir}" = .; then + target_srcdir= +else + target_srcdir="${srcdir}/" +fi +AM_CONDITIONAL(TARGET_SRCDIR_EMPTY, [test -z "${target_srcdir}"]) +AC_SUBST([target_srcdir]) + + +AC_OUTPUT diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 000000000000..ecaaf27b9262 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1,14 @@ +kyua-about.1 +kyua-config.1 +kyua-db-exec.1 +kyua-db-migrate.1 +kyua-debug.1 +kyua-help.1 +kyua-list.1 +kyua-report-html.1 +kyua-report-junit.1 +kyua-report.1 +kyua-test.1 +kyua.1 +kyua.conf.5 +kyuafile.5 diff --git a/doc/Kyuafile b/doc/Kyuafile new file mode 100644 index 000000000000..c538f5b2a531 --- /dev/null +++ b/doc/Kyuafile @@ -0,0 +1,5 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="manbuild_test"} diff --git a/doc/Makefile.am.inc b/doc/Makefile.am.inc new file mode 100644 index 000000000000..638191218bcc --- /dev/null +++ b/doc/Makefile.am.inc @@ -0,0 +1,152 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +BUILD_MANPAGE = \ + $(MKDIR_P) doc; \ + $(SHELL) $(srcdir)/doc/manbuild.sh \ + -v "CONFDIR=$(kyua_confdir)" \ + -v "DOCDIR=$(docdir)" \ + -v "EGDIR=$(examplesdir)" \ + -v "MISCDIR=$(miscdir)" \ + -v "PACKAGE=$(PACKAGE_TARNAME)" \ + -v "STOREDIR=$(storedir)" \ + -v "TESTSDIR=$(testsdir)" \ + -v "VERSION=$(PACKAGE_VERSION)" \ + "$(srcdir)/doc/$${name}.in" "doc/$${name}" + +DIST_MAN_DEPS = doc/manbuild.sh \ + doc/build-root.mdoc \ + doc/results-file-flag-read.mdoc \ + doc/results-file-flag-write.mdoc \ + doc/results-files.mdoc \ + doc/results-files-report-example.mdoc \ + doc/test-filters.mdoc \ + doc/test-isolation.mdoc +MAN_DEPS = $(DIST_MAN_DEPS) Makefile +EXTRA_DIST += $(DIST_MAN_DEPS) + +man_MANS = doc/kyua-about.1 +CLEANFILES += doc/kyua-about.1 +EXTRA_DIST += doc/kyua-about.1.in +doc/kyua-about.1: $(srcdir)/doc/kyua-about.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-about.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-config.1 +CLEANFILES += doc/kyua-config.1 +EXTRA_DIST += doc/kyua-config.1.in +doc/kyua-config.1: $(srcdir)/doc/kyua-config.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-config.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-db-exec.1 +CLEANFILES += doc/kyua-db-exec.1 +EXTRA_DIST += doc/kyua-db-exec.1.in +doc/kyua-db-exec.1: $(srcdir)/doc/kyua-db-exec.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-db-exec.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-db-migrate.1 +CLEANFILES += doc/kyua-db-migrate.1 +EXTRA_DIST += doc/kyua-db-migrate.1.in +doc/kyua-db-migrate.1: $(srcdir)/doc/kyua-db-migrate.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-db-migrate.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-debug.1 +CLEANFILES += doc/kyua-debug.1 +EXTRA_DIST += doc/kyua-debug.1.in +doc/kyua-debug.1: $(srcdir)/doc/kyua-debug.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-debug.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-help.1 +CLEANFILES += doc/kyua-help.1 +EXTRA_DIST += doc/kyua-help.1.in +doc/kyua-help.1: $(srcdir)/doc/kyua-help.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-help.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-list.1 +CLEANFILES += doc/kyua-list.1 +EXTRA_DIST += doc/kyua-list.1.in +doc/kyua-list.1: $(srcdir)/doc/kyua-list.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-list.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-report-html.1 +CLEANFILES += doc/kyua-report-html.1 +EXTRA_DIST += doc/kyua-report-html.1.in +doc/kyua-report-html.1: $(srcdir)/doc/kyua-report-html.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-report-html.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-report-junit.1 +CLEANFILES += doc/kyua-report-junit.1 +EXTRA_DIST += doc/kyua-report-junit.1.in +doc/kyua-report-junit.1: $(srcdir)/doc/kyua-report-junit.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-report-junit.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-report.1 +CLEANFILES += doc/kyua-report.1 +EXTRA_DIST += doc/kyua-report.1.in +doc/kyua-report.1: $(srcdir)/doc/kyua-report.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-report.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua-test.1 +CLEANFILES += doc/kyua-test.1 +EXTRA_DIST += doc/kyua-test.1.in +doc/kyua-test.1: $(srcdir)/doc/kyua-test.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua-test.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua.1 +CLEANFILES += doc/kyua.1 +EXTRA_DIST += doc/kyua.1.in +doc/kyua.1: $(srcdir)/doc/kyua.1.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua.1; $(BUILD_MANPAGE) + +man_MANS += doc/kyua.conf.5 +CLEANFILES += doc/kyua.conf.5 +EXTRA_DIST += doc/kyua.conf.5.in +doc/kyua.conf.5: $(srcdir)/doc/kyua.conf.5.in $(MAN_DEPS) + $(AM_V_GEN)name=kyua.conf.5; $(BUILD_MANPAGE) + +man_MANS += doc/kyuafile.5 +CLEANFILES += doc/kyuafile.5 +EXTRA_DIST += doc/kyuafile.5.in +doc/kyuafile.5: $(srcdir)/doc/kyuafile.5.in $(MAN_DEPS) + $(AM_V_GEN)name=kyuafile.5; $(BUILD_MANPAGE) + +if WITH_ATF +EXTRA_DIST += doc/Kyuafile + +noinst_SCRIPTS += doc/manbuild_test +CLEANFILES += doc/manbuild_test +EXTRA_DIST += doc/manbuild_test.sh +doc/manbuild_test: $(srcdir)/doc/manbuild_test.sh Makefile + $(AM_V_GEN)$(MKDIR_P) doc; \ + echo "#! $(ATF_SH)" >doc/manbuild_test.tmp; \ + echo "# AUTOMATICALLY GENERATED FROM Makefile" \ + >>doc/manbuild_test.tmp; \ + sed -e 's,__MANBUILD__,$(abs_srcdir)/doc/manbuild.sh,g' \ + <$(srcdir)/doc/manbuild_test.sh >>doc/manbuild_test.tmp; \ + mv doc/manbuild_test.tmp doc/manbuild_test; \ + chmod +x doc/manbuild_test +endif diff --git a/doc/build-root.mdoc b/doc/build-root.mdoc new file mode 100644 index 000000000000..2fb008246f41 --- /dev/null +++ b/doc/build-root.mdoc @@ -0,0 +1,104 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Em Build directories +(or object directories, target directories, product directories, etc.) is +the concept that allows a developer to keep the source tree clean from +build products by asking the build system to place such build products +under a separate subtree. +.Pp +Most build systems today support build directories. +For example, the GNU Automake/Autoconf build system exposes such concept when +invoked as follows: +.Bd -literal -offset indent +$ cd my-project-1.0 +$ mkdir build +$ cd build +$ ../configure +$ make +.Ed +.Pp +Under such invocation, all the results of the build are left in the +.Pa my-project-1.0/build/ +subdirectory while maintaining the contents of +.Pa my-project-1.0/ +intact. +.Pp +Because build directories are an integral part of most build systems, and +because they are a tool that developers use frequently, +.Nm +supports build directories too. +This manifests in the form of +.Nm +being able to run tests from build directories while reading the (often +immutable) test suite definition from the source tree. +.Pp +One important property of build directories is that they follow (or need to +follow) the exact same layout as the source tree. +For example, consider the following directory listings: +.Bd -literal -offset indent +src/Kyuafile +src/bin/ls/ +src/bin/ls/Kyuafile +src/bin/ls/ls.c +src/bin/ls/ls_test.c +src/sbin/su/ +src/sbin/su/Kyuafile +src/sbin/su/su.c +src/sbin/su/su_test.c + +obj/bin/ls/ +obj/bin/ls/ls* +obj/bin/ls/ls_test* +obj/sbin/su/ +obj/sbin/su/su* +obj/sbin/su/su_test* +.Ed +.Pp +Note how the directory layout within +.Pa src/ +matches that of +.Pa obj/ . +The +.Pa src/ +directory contains only source files and the definition of the test suite +(the Kyuafiles), while the +.Pa obj/ +directory contains only the binaries generated during a build. +.Pp +All commands that deal with the workspace support the +.Fl -build-root Ar path +option. +When this option is provided, the directory specified by the +option is considered to be the root of the build directory. +For example, considering our previous fake tree layout, we could invoke +.Nm +as any of the following: +.Bd -literal -offset indent +$ kyua __COMMAND__ --kyuafile=src/Kyuafile --build-root=obj +$ cd src && kyua __COMMAND__ --build-root=../obj +.Ed diff --git a/doc/kyua-about.1.in b/doc/kyua-about.1.in new file mode 100644 index 000000000000..1ea134810e65 --- /dev/null +++ b/doc/kyua-about.1.in @@ -0,0 +1,95 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd May 20, 2015 +.Dt KYUA-ABOUT 1 +.Os +.Sh NAME +.Nm "kyua about" +.Nd Shows detailed authors, license, and version information +.Sh SYNOPSIS +.Nm +.Op Ar authors | license | version +.Sh DESCRIPTION +The +.Sq about +command provides generic information about the +.Xr kyua 1 +tool. +In the default synopsis form (no arguments), the information printed +includes: +.Bl -enum +.It +The name of the package, which is +.Sq __PACKAGE__ . +.It +The version number, which is +.Sq __VERSION__ . +.It +License information. +.It +Authors information. +.It +A link to the project web site. +.El +.Pp +You can customize the information printed by this command by specifying +the desired topic as the single argument to the command. +This can be one of: +.Bl -tag -width authorsXX +.It Ar authors +Displays the list of authors and contributors only. +.It Ar license +Displays the license information and the list of copyrights. +.It Ar version +Displays the package name and the version number in a format that is +compatible with the output of GNU tools that support a +.Fl -version +flag. +Use this whenever you have to query the version number of the package. +.El +.Sh FILES +The following files are read by the +.Nm +command: +.Bl -tag -width XX +.It Pa __DOCDIR__/AUTHORS +List of authors (aka copyright holders). +.It Pa __DOCDIR__/CONTRIBUTORS +List of contributors (aka individuals that have contributed to the project). +.It Pa __DOCDIR__/LICENSE +License information. +.El +.Sh EXIT STATUS +The +.Nm +command always returns 0. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 diff --git a/doc/kyua-config.1.in b/doc/kyua-config.1.in new file mode 100644 index 000000000000..9c13ce06505e --- /dev/null +++ b/doc/kyua-config.1.in @@ -0,0 +1,59 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd September 9, 2012 +.Dt KYUA-CONFIG 1 +.Os +.Sh NAME +.Nm "kyua config" +.Nd Inspects the values of the loaded configuration +.Sh SYNOPSIS +.Nm +.Op Ar variable1 .. variableN +.Sh DESCRIPTION +The +.Nm +command provides a way to list all defined configuration variables and +their current values. +.Pp +This command is intended to help you in resolving the values of the +configuration variables without having to scan over configuration files. +.Pp +In the default synopsis form (no arguments), the command prints all +configuration variables. +If any arguments are provided, the command will only print the +requested variables. +.Sh EXIT STATUS +The +.Nm +command returns 0 on success or 1 if any of the specified configuration +variables does not exist. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 diff --git a/doc/kyua-db-exec.1.in b/doc/kyua-db-exec.1.in new file mode 100644 index 000000000000..04f34c7b54e7 --- /dev/null +++ b/doc/kyua-db-exec.1.in @@ -0,0 +1,80 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd October 13, 2014 +.Dt KYUA-DB-EXEC 1 +.Os +.Sh NAME +.Nm "kyua db-exec" +.Nd Executes a SQL statement in a results file +.Sh SYNOPSIS +.Nm +.Op Fl -no-headers +.Op Fl -results-file Ar file +.Ar statement +.Sh DESCRIPTION +The +.Nm +command provides a way to execute an arbitrary SQL statement within the +database. +This command is mostly intended to aid in debugging, but can also be used to +extract information from the database when the current interfaces do not +provide the desired functionality. +.Pp +The input database must exist. +It makes no sense to use +.Nm +on a nonexistent or empty database. +.Pp +The +.Nm +command takes one or more arguments, all of which are concatenated to form +a single SQL statement. +Once the statement is executed, +.Nm +prints the resulting table on the screen, if any. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -no-headers +Avoids printing the headers of the table in the output of the command. +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.El +.Ss Results files +__include__ results-files.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 on success or 1 if the SQL statement is invalid or fails +to run. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-test 1 diff --git a/doc/kyua-db-migrate.1.in b/doc/kyua-db-migrate.1.in new file mode 100644 index 000000000000..67e46de46fec --- /dev/null +++ b/doc/kyua-db-migrate.1.in @@ -0,0 +1,63 @@ +.\" Copyright 2013 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd October 13, 2014 +.Dt KYUA-DB-MIGRATE 1 +.Os +.Sh NAME +.Nm "kyua db-migrate" +.Nd Upgrades the schema of an existing results file +.Sh SYNOPSIS +.Nm +.Op Fl -results-file Ar file +.Sh DESCRIPTION +The +.Nm +command migrates the schema of an existing database to the latest +version implemented in +.Xr kyua 1 . +.Pp +This operation is not reversible. +However, a backup of the database is created in the same directory where the +database lives. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.El +.Ss Results files +__include__ results-files.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 on success or 1 if the migration fails. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 diff --git a/doc/kyua-debug.1.in b/doc/kyua-debug.1.in new file mode 100644 index 000000000000..9e962a465421 --- /dev/null +++ b/doc/kyua-debug.1.in @@ -0,0 +1,145 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd October 13, 2014 +.Dt KYUA-DEBUG 1 +.Os +.Sh NAME +.Nm "kyua debug" +.Nd Executes a single test case with facilities for debugging +.Sh SYNOPSIS +.Nm +.Op Fl -build-root Ar path +.Op Fl -kyuafile Ar file +.Op Fl -stdout Ar path +.Op Fl -stderr Ar path +.Ar test_case +.Sh DESCRIPTION +The +.Nm +command provides a mechanism to execute a single test case bypassing some +of the Kyua infrastructure and allowing the user to poke into the execution +behavior of the test. +.Pp +The test case to run is selected by providing a test filter, described below in +.Sx Test filters , +that matches a single test case. +The test case is executed and its result is printed as the last line of the +output of the tool. +.Pp +The test executed by +.Nm +is run under a controlled environment as described in +.Sx Test isolation . +.Pp +At the moment, the +.Nm +command allows the following aspects of a test case execution to be +tweaked: +.Bl -bullet +.It +Redirection of the test case's stdout and stderr to the console (the +default) or to arbitrary files. +See the +.Fl -stdout +and +.Fl -stderr +options below. +.El +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -build-root Ar path +Specifies the build root in which to find the test programs referenced +by the Kyuafile, if different from the Kyuafile's directory. +See +.Sx Build directories +below for more information. +.It Fl -kyuafile Ar file , Fl k Ar file +Specifies the Kyuafile to process. +Defaults to +.Pa Kyuafile +file in the current directory. +.It Fl -stderr Ar path +Specifies the file to which to send the standard error of the test +program's body. +The default is +.Pa /dev/stderr , +which is a special character device that redirects the output to +standard error on the console. +.It Fl -stdout Ar path +Specifies the file to which to send the standard output of the test +program's body. +The default is +.Pa /dev/stdout , +which is a special character device that redirects the output to +standard output on the console. +.El +.Pp +For example, consider the following Kyua session: +.Bd -literal -offset indent +$ kyua test +kernel/fs:mkdir -> passed +kernel/fs:rmdir -> failed: Invalid argument + +1/2 passed (1 failed) +.Ed +.Pp +At this point, we do not have a lot of information regarding the +failure of the +.Sq kernel/fs:rmdir +test. +We can run this test through the +.Nm +command to inspect its output a bit closer, hoping that the test case is +kind enough to log its progress: +.Bd -literal -offset indent +$ kyua debug kernel/fs:rmdir +Trying rmdir('foo') +Trying rmdir(NULL) +kernel/fs:rmdir -> failed: Invalid argument +.Ed +.Pp +Luckily, the offending test case was printing status lines as it +progressed, so we could see the last attempted call and we can know match +the failure message to the problem. +.Ss Build directories +__include__ build-root.mdoc COMMAND=debug +.Ss Test filters +__include__ test-filters.mdoc +.Ss Test isolation +__include__ test-isolation.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 if the test case passes or 1 if the test case fails. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyuafile 5 diff --git a/doc/kyua-help.1.in b/doc/kyua-help.1.in new file mode 100644 index 000000000000..2c4f2bc3859e --- /dev/null +++ b/doc/kyua-help.1.in @@ -0,0 +1,64 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd September 9, 2012 +.Dt KYUA-HELP 1 +.Os +.Sh NAME +.Nm "kyua help" +.Nd Shows usage information +.Sh SYNOPSIS +.Nm +.Op Ar command +.Sh DESCRIPTION +The +.Nm +command provides interactive help on all supported commands and options. +If, for some reason, you happen to spot a discrepancy in the output of this +command and this document, the command is the authoritative source of +information. +.Pp +If no arguments are provided, the command prints the list of common options +and the list of supported subcommands. +.Pp +If the +.Ar command +argument is provided to, this single argument is the name of a valid +subcommand. +In that case, +.Nm +prints a textual description of the command, the list of common options and +the list of subcommand-specific options. +.Sh EXIT STATUS +The +.Nm +command always returns 0. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 diff --git a/doc/kyua-list.1.in b/doc/kyua-list.1.in new file mode 100644 index 000000000000..5774354d9236 --- /dev/null +++ b/doc/kyua-list.1.in @@ -0,0 +1,90 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd October 13, 2014 +.Dt KYUA-LIST 1 +.Os +.Sh NAME +.Nm "kyua list" +.Nd Lists test cases and their metadata +.Sh SYNOPSIS +.Nm +.Op Fl -build-root Ar path +.Op Fl -kyuafile Ar file +.Op Fl -verbose +.Ar test_case1 Op Ar .. test_caseN +.Sh DESCRIPTION +The +.Nm +command scans all the test programs and test cases in a test suite (as +defined by a +.Xr kyuafile 5 ) +and prints a list of all their names, optionally accompanied by any metadata +properties they have. +.Pp +The optional arguments to +.Nm +are used to select which test programs or test cases to run. +These are filters and are described below in +.Sx Test filters . +.Pp +This command must be run within a test suite or a test suite must be +provided with the +.Fl -kyuafile +flag. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -build-root Ar path +Specifies the build root in which to find the test programs referenced +by the Kyuafile, if different from the Kyuafile's directory. +See +.Sx Build directories +below for more information. +.It Fl -kyuafile Ar path , Fl k Ar path +Specifies the Kyuafile to process. +Defaults to a +.Pa Kyuafile +file in the current directory. +.It Fl -verbose , Fl v +Prints metadata properties for every test case. +.El +.Ss Build directories +__include__ build-root.mdoc COMMAND=list +.Ss Test filters +__include__ test-filters.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 on success or 1 if any of the given test case filters +does not match any test case. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyuafile 5 diff --git a/doc/kyua-report-html.1.in b/doc/kyua-report-html.1.in new file mode 100644 index 000000000000..1f9f55b69a3f --- /dev/null +++ b/doc/kyua-report-html.1.in @@ -0,0 +1,103 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd October 13, 2014 +.Dt KYUA-REPORT-HTML 1 +.Os +.Sh NAME +.Nm "kyua report-html" +.Nd Generates an HTML report with the results of a test suite run +.Sh SYNOPSIS +.Nm +.Op Fl -force +.Op Fl -output Ar path +.Op Fl -results-file Ar file +.Op Fl -results-filter Ar types +.Sh DESCRIPTION +The +.Nm +command provides a simple mechanism to generate HTML reports of the +execution of a test suite. +The command processes a results file and then populates a directory with +multiple HTML and supporting files to describe the results recorded in that +results file. +.Pp +The HTML output is static and self-contained, so it can easily be served by +any simple web server. +The command expects the target directory to not exist, because it would +overwrite any contents if not careful. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -force +Forces the deletion of the output directory if it exists. +Use care, as this effectively means a +.Sq rm -rf . +.It Fl -output Ar directory +Specifies the target directory into which to generate the HTML files. +The directory must not exist unless the +.Fl -force +option is provided. +The default is +.Pa ./html . +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.It Fl -results-filter Ar types +Comma-separated list of the test result types to include in the report. +The ordering of the values is respected so that you can determine how you +want the list of tests to be shown. +.Pp +The valid values are: +.Sq broken , +.Sq failed , +.Sq passed , +.Sq skipped +and +.Sq xfail . +If the parameter supplied to the option is empty, filtering is suppressed +and all result types are shown in the report. +.Pp +The default value for this flag includes all the test results except the +passed tests. +Showing the passed tests by default clutters the report with too much +information, so only abnormal conditions are included. +.El +.Ss Results files +__include__ results-files.mdoc +.Sh EXIT STATUS +The +.Nm +command always returns 0. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh EXAMPLES +__include__ results-files-report-example.mdoc REPORT_COMMAND=report-html +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-report 1 , +.Xr kyua-report-junit 1 diff --git a/doc/kyua-report-junit.1.in b/doc/kyua-report-junit.1.in new file mode 100644 index 000000000000..f1ad3a2e7f29 --- /dev/null +++ b/doc/kyua-report-junit.1.in @@ -0,0 +1,87 @@ +.\" Copyright 2014 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd October 13, 2014 +.Dt KYUA-REPORT-JUNIT 1 +.Os +.Sh NAME +.Nm "kyua report-junit" +.Nd Generates a JUnit report with the results of a test suite run +.Sh SYNOPSIS +.Nm +.Op Fl -output Ar path +.Op Fl -results-file Ar file +.Sh DESCRIPTION +The +.Nm +command provides a simple mechanism to generate JUnit reports of the +execution of a test suite. +The command processes a results file and then generates a single XML file +that complies with the JUnit XSchema. +.Pp +The JUnit output is static and self-contained, so it can easily be plugged +into any continuous integration system, like Jenkins. +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -output Ar directory +Specifies the file into which to store the JUnit report. +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.El +.Ss Caveats +Because of limitations in the JUnit XML schema, not all the data collected by +Kyua can be properly represented in JUnit reports. +However, because test data are extremely useful for debugging purposes, the +.Nm +command shovels these data into the JUnit output. +In particular: +.Bl -bullet +.It +The test case metadata values are prepended to the test case's standard error +output. +.It +Test cases that report expected failures as their results are recorded as +passed. +The fact that they failed as expected is recorded in the test case's standard +error output along with the corresponding reason. +.El +.Ss Results files +__include__ results-files.mdoc +.Sh EXIT STATUS +The +.Nm +command always returns 0. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh EXAMPLES +__include__ results-files-report-example.mdoc REPORT_COMMAND=report-junit +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-report 1 , +.Xr kyua-report-html 1 diff --git a/doc/kyua-report.1.in b/doc/kyua-report.1.in new file mode 100644 index 000000000000..8e2485f9c4ac --- /dev/null +++ b/doc/kyua-report.1.in @@ -0,0 +1,118 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd October 13, 2014 +.Dt KYUA-REPORT 1 +.Os +.Sh NAME +.Nm "kyua report" +.Nd Generates reports with the results of a test suite run +.Sh SYNOPSIS +.Nm +.Op Fl -output Ar path +.Op Fl -results-file Ar file +.Op Fl -results-filter Ar types +.Op Fl -verbose +.Op Ar test_filter1 .. test_filterN +.Sh DESCRIPTION +The +.Nm +command parses a results file and generates a user-friendly, plaintext +report for user consumption on the terminal. +By default, these reports only display a summary of the execution of the full +test suite to highlight where problems may lie. +.Pp +The output of +.Nm +can be customized to display full details on all executed test cases. +Additionally, the optional arguments to +.Nm +are used to select which test programs or test cases to display. +These are filters and are described below in +.Sx Test filters . +.Pp +Reports generated by +.Nm +are +.Em not intended to be machine-parseable . +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -output Ar path +Specifies the path to which the report should be written to. +The special values +.Pa /dev/stdout +and +.Pa /dev/stderr +can be used to specify the standard output and the standard error, +respectively. +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-read.mdoc +.It Fl -results-filter Ar types +Comma-separated list of the test result types to include in the report. +The ordering of the values is respected so that you can determine how you +want the list of tests to be shown. +.Pp +The valid values are: +.Sq broken , +.Sq failed , +.Sq passed , +.Sq skipped +and +.Sq xfail . +If the parameter supplied to the option is empty, filtering is suppressed +and all result types are shown in the report. +.Pp +The default value for this flag includes all the test results except the +passed tests. +Showing the passed tests by default clutters the report with too much +information, so only abnormal conditions are included. +.It Fl -verbose +Prints a detailed report of the execution. +In addition to all the information printed by default, verbose reports +include the runtime context of the test suite run, the metadata of each +test case, and the verbatim output of the test cases. +.El +.Ss Results files +__include__ results-files.mdoc +.Ss Test filters +__include__ test-filters.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 if no filters were specified or if all filters match one +or more test cases. +If any filter fails to match any test case, the command returns 1. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh EXAMPLES +__include__ results-files-report-example.mdoc REPORT_COMMAND=report +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-report-html 1 , +.Xr kyua-report-junit 1 diff --git a/doc/kyua-test.1.in b/doc/kyua-test.1.in new file mode 100644 index 000000000000..8cd5f34ae6af --- /dev/null +++ b/doc/kyua-test.1.in @@ -0,0 +1,102 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd October 13, 2014 +.Dt KYUA-TEST 1 +.Os +.Sh NAME +.Nm "kyua test" +.Nd Runs tests +.Sh SYNOPSIS +.Nm +.Op Fl -build-root Ar path +.Op Fl -kyuafile Ar file +.Op Fl -results-file Ar file +.Op Ar test_filter1 .. test_filterN +.Sh DESCRIPTION +The +.Nm +command loads a test suite definition from a +.Xr kyuafile 5 , +runs the tests defined in it, and records the results into a new results +file. +By default, all tests in the test suite are executed but the optional +arguments to +.Nm +can be used to select which test programs or test cases to run. +These are filters and are described below in +.Sx Test filters . +.Pp +Every test executed by +.Nm +is run under a controlled environment as described in +.Sx Test isolation . +.Pp +The following subcommand options are recognized: +.Bl -tag -width XX +.It Fl -build-root Ar path +Specifies the build root in which to find the test programs referenced by +the Kyuafile, if different from the Kyuafile's directory. +See +.Sx Build directories +below for more information. +.It Fl -kyuafile Ar path , Fl k Ar path +Specifies the Kyuafile to process. +Defaults to a +.Pa Kyuafile +file in the current directory. +.It Fl -results-file Ar path , Fl s Ar path +__include__ results-file-flag-write.mdoc +.El +.Pp +You can later inspect the results of the test run in more detail by using +.Xr kyua-report 1 +or you can execute a single test case with debugging functionality by using +.Xr kyua-debug 1 . +.Ss Build directories +__include__ build-root.mdoc COMMAND=test +.Ss Results files +__include__ results-files.mdoc +.Ss Test filters +__include__ test-filters.mdoc +.Ss Test isolation +__include__ test-isolation.mdoc +.Sh EXIT STATUS +The +.Nm +command returns 0 if all executed test cases pass or 1 if any of the +executed test cases fails or if any of the given test case filters does not +match any test case. +.Pp +Additional exit codes may be returned as described in +.Xr kyua 1 . +.Sh EXAMPLES +__include__ results-files-report-example.mdoc REPORT_COMMAND=report +.Sh SEE ALSO +.Xr kyua 1 , +.Xr kyua-report 1 , +.Xr kyuafile 5 diff --git a/doc/kyua.1.in b/doc/kyua.1.in new file mode 100644 index 000000000000..2fca5eb09f9f --- /dev/null +++ b/doc/kyua.1.in @@ -0,0 +1,400 @@ +.\" Copyright 2011 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd May 12, 2015 +.Dt KYUA 1 +.Os +.Sh NAME +.Nm kyua +.Nd Testing framework for infrastructure software +.Sh SYNOPSIS +.Nm +.Op Fl -config Ar file +.Op Fl -logfile Ar file +.Op Fl -loglevel Ar level +.Op Fl -variable Ar name=value +.Ar command +.Op Ar command_options +.Op Ar command_arguments +.Sh DESCRIPTION +.Em If you are here looking for details on how to run the test suite in +.Pa /usr/tests +.Em ( or +.Pa __TESTSDIR__ ) , +.Em please start by reading the +.Xr tests 7 +.Em manual page that should be supplied by your system . +.Pp +Kyua is a testing framework for infrastructure software, originally +designed to equip BSD-based operating systems with a test suite. +This means that Kyua is lightweight and simple, and that Kyua integrates well +with various build systems and continuous integration frameworks. +.Pp +Kyua features an expressive test suite definition language, a safe +runtime engine for test suites and a powerful report generation engine. +.Pp +Kyua is for both developers and users, from the developer applying a +simple fix to a library to the system administrator deploying a new +release on a production machine. +.Pp +Kyua is able to execute test programs written with a plethora of testing +libraries and languages. +The test program library of choice is ATF, which +.Nm Ns 's +design originated from. +However, framework-less test programs and TAP-compliant test programs can also +be executed through +.Nm +.Ss Overview +As can be observed in the synopsis, the interface of +.Nm +implements a common subcommand-based interface. +The arguments to the tool specify, in this order: a set of common options +that all the commands accept, a required +.Ar command +name that specifies what +.Nm +should do, and +a set of possibly-optional +.Ar command_options +and +.Ar command_arguments +that are specific to the chosen command. +.Pp +The following options are recognized by all the commands. +Keep in mind that these must always be specified before the command name. +.Bl -tag -width XX +.It Fl -config Ar path , Fl c Ar path +Specifies the configuration file to process, which must be in the format +described in +.Xr kyua.conf 5 . +The special value +.Sq none +explicitly disables the loading of any configuration file. +.Pp +Defaults to +.Pa ~/.kyua/kyua.conf +if it exists, otherwise to +.Pa __CONFDIR__/kyua.conf +if it exists, +or else to +.Sq none . +.It Fl -logfile Ar path +Specifies the location of the file to which +.Nm +will log run time events useful for postmortem debugging. +.Pp +The default depends on different environment variables as described in +.Sx Logging , +but typically the file will be stored within the user's home directory. +.It Fl -loglevel Ar level +Specifies the maximum logging level to record in the log file. +See +.Sx Logging +for more details. +.Pp +The default is +.Sq info . +.It Fl -variable Ar name=value , Fl v Ar name=value +Sets the +.Ar name +configuration variable to +.Ar value . +The values set through this option have preference over the values set in the +configuration file. +.Pp +The specified variable can either be a builtin variable or a test-suite +specific variable. +See +.Xr kyua.conf 5 +for more details. +.El +.Pp +The following commands are generic and do not have any relation to the execution +of tests or the inspection of their results: +.Bl -tag -width reportXjunitXX -offset indent +.It Ar about +Shows general program information. +See +.Xr kyua-about 1 . +.It Ar config +Inspects the values of the configuration variables. +See +.Xr kyua-config 1 . +.It Ar db-exec +Executes an arbitrary SQL statement on a results file and prints the +resulting table. +See +.Xr kyua-db-exec 1 . +.It Ar help +Shows usage information. +See +.Xr kyua-help 1 . +.El +.Pp +The following commands are used to generate reports based on the data previously +recorded in a results file: +.Bl -tag -width reportXjunitXX -offset indent +.It Ar report +Generates a plaintext report. +Combined with its +.Fl -verbose +flag and the ability to only display specific test cases, this command can also +be used to debug test failures post-facto on the console. +See +.Xr kyua-report 1 . +.It Ar report-html +Generates an HTML report. +See +.Xr kyua-report-html 1 . +.It Ar report-junit +Generates a JUnit report. +See +.Xr kyua-report-junit 1 . +.El +.Pp +The following commands are used to interact with a test suite: +.Bl -tag -width reportXjunitXX -offset indent +.It Ar debug +Executes a single test case in a controlled environment for debugging purposes. +See +.Xr kyua-debug 1 . +.It Ar list +Lists test cases defined in a test suite by a +.Xr kyuafile 5 +and, optionally, displays their metadata. +See +.Xr kyua-list 1 . +.It Ar test +Runs tests defined in a test suite by a +.Xr kyuafile 5 . +See +.Xr kyua-test 1 . +.El +.Ss Logging +.Nm +has a logging facility that collects all kinds of events at run time. +These events are always logged to a file so that the log is available when +it is most needed: right after a non-reproducible problem happens. +The only way to disable logging is by sending the log to +.Pa /dev/null . +.Pp +The location of the log file can be manually specified with the +.Fl -logfile +option, which applies to all commands. +If no file is explicitly specified, the location of the log files is chosen in +this order: +.Bl -enum -offset indent +.It +.Pa ${HOME}/.kyua/logs/ +if +.Va HOME +is defined. +.It +.Pa ${TMPDIR}/ +if +.Va TMPDIR +is defined. +.It +.Pa /tmp/ . +.El +.Pp +And the default naming scheme of the log files is: +.Sq ..log . +.Pp +The messages stored in the log file have a level (or severity) attached to +them. +These are: +.Bl -tag -width warningXX -offset indent +.It error +Fatal error messages. +The program generally terminates after these, either in a clean manner or by +crashing. +.It warning +Non-fatal error messages. +These generally report a condition that must be addressed but the application +can continue to run. +.It info +Informational messages. +These tell the user what the program was doing at a general level of +operation. +.It debug +Detailed informational messages. +These are often useful when debugging problems in the application, as they +contain lots of internal details. +.El +.Pp +The default log level is +.Sq info +unless explicitly overridden with +.Fl -loglevel . +.Pp +The log file is a plain text file containing one line per log record. +The format of each line is as follows: +.Bd -literal -offset indent +timestamp entry_type pid file:line: message +.Ed +.Pp +.Ar entry_type +can be one of: +.Sq E +for an error, +.Sq W +for a warning, +.Sq I +for an informational message and +.Sq D +for a debug message. +.Ss Bug reporting +If you think you have encountered a bug in +.Nm , +please take the time to let the developers know about it. +This will ensure that the bug is addressed and potentially fixed in the next +Kyua release. +.Pp +The first step in reporting a bug is to check if there already is a similar +bug in the database. +You can check what issues are currently in the database by going to: +.Bd -literal -offset indent +https://github.com/jmmv/kyua/issues/ +.Ed +.Pp +If there is no existing issue that describes an issue similar to the +one you are experiencing, you can open a new one by visiting: +.Bd -literal -offset indent +https://github.com/jmmv/kyua/issues/new/ +.Ed +.Pp +When doing so, please include as much detail as possible. +Among other things, explain what operating system and platform you are running +.Nm +on, what were you trying to do, what exact messages you saw on the screen, +how did you expect the program to behave, and any other details that you +may find relevant. +.Pp +Also, please include a copy of the log file corresponding to the problem +you are experiencing. +Unless you have changed the location of the log files, you can most likely +find them in +.Pa ~/.kyua/logs/ . +If the problem is reproducible, it is good idea to regenerate the log file +with an increased log level so as to provide more information. +For example: +.Bd -literal -offset indent +$ kyua --logfile=problem.log --loglevel=debug \\ + [rest of the command line] +.Ed +.Sh ENVIRONMENT +The following variables are recognized and can be freely tuned by the end user: +.Bl -tag -width COLUMNSXX +.It Va COLUMNS +The width of the screen, in number of characters. +.Nm +uses this to wrap long lines. +If not present, the width of the screen is determined from the terminal +stdout is connected to, and, if the guessing fails, this defaults to infinity. +.It Va HOME +Path to the user's home directory. +.Nm +uses this location to determine paths to configuration files and default log +files. +.It Va TMPDIR +Path to the system-wide temporary directory. +.Nm +uses this location to place the work directory of test cases, among other +things. +.Pp +The default value of this variable depends on the operating system. +In general, it is +.Pa /tmp . +.El +.Pp +The following variables are also recognized, but you should not need to set them +during normal operation. +They are only provided to override the value of built-in values, which is useful +when testing +.Nm +itself: +.Bl -tag -width KYUAXCONFDIRXX +.It Va KYUA_CONFDIR +Path to the system-wide configuration files for +.Nm . +.Pp +Defaults to +.Pa __CONFDIR__ . +.It Va KYUA_DOCDIR +Path to the location of installed documentation. +.Pp +Defaults to +.Pa __DOCDIR__ . +.It Va KYUA_MISCDIR +Path to the location of the installed miscellaneous scripts and data +files provided by +.Nm . +.Pp +Defaults to +.Pa __MISCDIR__ . +.It Va KYUA_STOREDIR +Path to the location of the installed store support files; e.g., the +directory containing the SQL database schema. +.Pp +Defaults to +.Pa __STOREDIR__ . +.El +.Sh FILES +.Bl -tag -width XXXX +.It Pa ~/.kyua/store/ +Default location for the results files. +.It Pa ~/.kyua/kyua.conf +User-specific configuration file. +.It Pa ~/.kyua/logs/ +Default location for the collected log files. +.It Pa __CONFDIR__/kyua.conf +System-wide configuration file. +.El +.Sh EXIT STATUS +.Nm +returns 0 on success, 1 on a controlled error condition in the given +subcommand, 2 on a general unexpected error and 3 on a usage error. +.Pp +The documentation of the subcommands in the corresponding manual pages only +details the difference between a successful exit (0) and the detection of a +controlled error (1). +Even though when those manual pages do not describe any other exit statuses, +codes above 1 can be returned. +.Sh SEE ALSO +.Xr kyua.conf 5 , +.Xr kyuafile 5 , +.Xr atf 7 , +.Xr tests 7 +.Sh AUTHORS +For more details on the people that made +.Nm +possible and the license terms, run: +.Bd -literal -offset indent +$ kyua about +.Ed diff --git a/doc/kyua.conf.5.in b/doc/kyua.conf.5.in new file mode 100644 index 000000000000..05a9499b48c4 --- /dev/null +++ b/doc/kyua.conf.5.in @@ -0,0 +1,141 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd February 20, 2015 +.Dt KYUA.CONF 5 +.Os +.Sh NAME +.Nm kyua.conf +.Nd Configuration file for the kyua tool +.Sh SYNOPSIS +.Fn syntax "int version" +.Pp +Variables: +.Va architecture , +.Va platform , +.Va test_suites , +.Va unprivileged_user . +.Sh DESCRIPTION +The configuration of Kyua is a simple collection of key/value pairs called +configuration variables. +There are configuration variables that have a special meaning to the runtime +engine implemented by +.Xr kyua 1 , +and there are variables that only have meaning in the context of particular +test suites. +.Pp +Configuration files are Lua scripts. +In their most basic form, their whole purpose is to assign values to +variables, but the user has the freedom to implement any logic he desires +to compute such values. +.Ss File versioning +Every +.Nm +file starts with a call to +.Fn syntax "int version" . +This call determines the specific schema used by the file so that future +backwards-incompatible modifications to the file can be introduced. +.Pp +Any new +.Nm +file should set +.Fa version +to +.Sq 2 . +.Ss Runtime configuration variables +The following variables are internally recognized by +.Xr kyua 1 : +.Bl -tag -width XX -offset indent +.It Va architecture +Name of the system architecture (aka processor type). +.It Va parallelism +Maximum number of test cases to execute concurrently. +.It Va platform +Name of the system platform (aka machine type). +.It Va unprivileged_user +Name or UID of the unprivileged user. +.Pp +If set, the given user must exist in the system and his privileges will be +used to run test cases that need regular privileges when +.Xr kyua 1 +is executed as root. +.El +.Ss Test-suite configuration variables +Each test suite is able to recognize arbitrary configuration variables, and +their type and meaning is specific to the test suite. +Because the existence and naming of these variables depends on every test +suite, this manual page cannot detail them; please refer to the documentation +of the test suite you are working with for more details on this topic. +.Pp +Test-suite specific configuration variables are defined inside the +.Va test_suites +dictionary. +The general syntax is: +.Bd -literal -offset indent +test_suites.. = +.Ed +.Pp +where +.Va test_suite_name +is the name of the test suite, +.Va variable_name +is the name of the variable to set, and +.Va value +is a value. +The value can be a string, an integer or a boolean. +.Sh FILES +.Bl -tag -width XX +.It __EGDIR__/kyua.conf +Sample configuration file. +.El +.Sh EXAMPLES +The following +.Nm +shows a simple configuration file that overrides a bunch of the built-in +.Xr kyua 1 +configuration variables: +.Bd -literal -offset indent +syntax(2) + +architecture = 'x86_64' +platform = 'amd64' +.Ed +.Pp +The following is a more complex example that introduces the definition of +per-test suite configuration variables: +.Bd -literal -offset indent +syntax(2) + +-- Assign built-in variables. +unprivileged_user = '_tests' + +-- Assign test-suite variables. All of these must be strings. +test_suites.NetBSD.file_systems = 'ffs ext2fs' +test_suites.X11.graphics_driver = 'vesa' +.Ed +.Sh SEE ALSO +.Xr kyua 1 diff --git a/doc/kyuafile.5.in b/doc/kyuafile.5.in new file mode 100644 index 000000000000..06cb2dbc42a8 --- /dev/null +++ b/doc/kyuafile.5.in @@ -0,0 +1,407 @@ +.\" Copyright 2012 The Kyua Authors. +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are +.\" met: +.\" +.\" * Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" * Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" * Neither the name of Google Inc. nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +.Dd July 3, 2015 +.Dt KYUAFILE 5 +.Os +.Sh NAME +.Nm Kyuafile +.Nd Test suite description files +.Sh SYNOPSIS +.Fn atf_test_program "string name" "[string metadata]" +.Fn current_kyuafile +.Fn fs.basename "string path" +.Fn fs.dirname "string path" +.Fn fs.exists "string path" +.Fn fs.files "string path" +.Fn fs.is_absolute "string path" +.Fn fs.join "string path" "string path" +.Fn include "string path" +.Fn plain_test_program "string name" "[string metadata]" +.Fn syntax "int version" +.Fn tap_test_program "string name" "[string metadata]" +.Fn test_suite "string name" +.Sh DESCRIPTION +A test suite is a collection of test programs and is represented by a +hierarchical layout of test binaries on the file system. +Any subtree of the file system can represent a test suite, provided that it +includes one or more +.Nm Ns s , +which are the test suite definition files. +.Pp +A +.Nm +is a Lua script whose purpose is to describe the structure of the test +suite it belongs to. +To do so, the script has access to a collection of special functions provided +by +.Xr kyua 1 +as described in +.Sx Helper functions . +.Ss File versioning +Every +.Nm +file starts with a call to +.Fn syntax "int version" . +This call determines the specific schema used by the file so that future +backwards-incompatible modifications to the file can be introduced. +.Pp +Any new +.Nm +file should set +.Fa version +to +.Sq 2 . +.Ss Test suite definition +If the +.Nm +registers any test programs, +the +.Nm +must define the name of the test suite the test programs belong to by using the +.Fn test_suite +function at the very beginning of the file. +.Pp +The test suite name provided in the +.Fn test_suite +call tells +.Xr kyua 1 +which set of configuration variables from +.Xr kyua.conf 5 +to pass to the test programs at run time. +.Ss Test program registration +A +.Nm +can register test programs by means of a variety of +.Fn *_test_program +functions, all of which take the name of a test program and a set of +optional metadata properties that describe such test program. +.Pp +The test programs to be registered must live in the current directory; in +other words, the various +.Fn *_test_program +calls cannot reference test programs in other directories. +The rationale for this is to force all +.Nm +files to be self-contained, and to simplify their internal representation. +.Pp +.Em ATF test programs +are those that use the +.Xr atf 7 +libraries. +They can be registered with the +.Fn atf_test_program +table constructor. +This function takes the +.Fa name +of the test program and a collection of optional metadata settings for all +the test cases in the test program. +Any metadata properties defined by the test cases themselves override the +metadata values defined here. +.Pp +.Em Plain test programs +are those that return 0 on success and non-0 on failure; in general, most test +programs (even those that use fancy unit-testing libraries) behave this way and +thus also qualify as plain test programs. +They can be registered with the +.Fn plain_test_program +table constructor. +This function takes the +.Fa name +of the test program, an optional +.Fa test_suite +name that overrides the global test suite name, and a collection of optional +metadata settings for the test program. +.Pp +.Em TAP test programs +are those that implement the Test Anything Protocol. +They can be registered with the +.Fn tap_test_program +table constructor. +This function takes the +.Fa name +of the test program and a collection of optional metadata settings for the +test program. +.Pp +The following metadata properties can be passed to any test program definition: +.Bl -tag -width XX -offset indent +.It Va allowed_architectures +Whitespace-separated list of machine architecture names allowed by the test. +If empty or not defined, the test is allowed to run on any machine +architecture. +.It Va allowed_platforms +Whitespace-separated list of machine platform names allowed by the test. +If empty or not defined, the test is allowed to run on any machine +platform. +.It Va custom.NAME +Custom variable defined by the test where +.Sq NAME +denotes the name of the variable. +These variables are useful to tag your tests with information specific to +your project. +The values of such variables are propagated all the way from the tests to the +results files and later to any generated reports. +.Pp +Note that if the name happens to have dashes or any other special characters +in it, you will have to use a special Lua syntax to define the property. +Refer to the +.Sx EXAMPLES +section below for clarification. +.It Va description +Textual description of the test. +.It Va is_exclusive +If true, indicates that this test program cannot be executed along any other +programs at the same time. +Test programs that affect global system state, such as those that modify the +value of a +.Xr sysctl 8 +setting, must set themselves as exclusive to prevent failures due to race +conditions. +Defaults to false. +.It Va required_configs +Whitespace-separated list of configuration variables that the test requires +to be defined before it can run. +.It Va required_disk_space +Amount of available disk space that the test needs to run successfully. +.It Va required_files +Whitespace-separated list of paths that the test requires to exist before +it can run. +.It Va required_memory +Amount of physical memory that the test needs to run successfully. +.It Va required_programs +Whitespace-separated list of basenames or absolute paths pointing to executable +binaries that the test requires to exist before it can run. +.It Va required_user +If empty, the test has no restrictions on the calling user for it to run. +If set to +.Sq unprivileged , +the test needs to not run as root. +If set to +.Sq root , +the test must run as root. +.It Va timeout +Amount of seconds that the test is allowed to execute before being killed. +.El +.Ss Recursion +To reference test programs in another subdirectory, a different +.Nm +must be created in that directory and it must be included into the original +.Nm +by means of the +.Fn include +function. +.Pp +.Fn include +may only be called with a relative path and with at most one directory +component. +This is by design: Kyua uses the file system structure as the layout of the +test suite definition. +Therefore, each subdirectory in a test suite must include its own +.Nm +and each +.Nm +can only descend into the +.Nm Ns s +of immediate subdirectories. +.Pp +If you need to source a +.Nm +located in disjoint parts of your file system namespace, you will have to +create a +.Sq shadow tree +using symbolic links and possibly helper +.Nm Ns s +to plug the various subdirectories together. +See the +.Sx EXAMPLES +section below for details. +.Pp +Note that each file is processed in its own Lua environment: there is no +mechanism to pass state from one file to the other. +The reason for this is that there is no such thing as a +.Dq top-level +.Nm +in a test suite: the user has to be able to run the test suite from any +directory in a given hierarchy, and this execution must not depend on files +that live in parent directories. +.Ss Top-level Kyuafile +Every system has a top directory into which test suites get installed. +The default is +.Pa __TESTSDIR__ . +Within this directory live test suites, each of which is in an independent +subdirectory. +Each subdirectory can be provided separately by independent third-party +packages. +.Pp +Kyua allows running all the installed test suites at once in order to +provide comprehensive cross-component reports. +In order to do this, there is a special file in the top directory that knows +how to inspect the subdirectories in search for other Kyuafiles and include +them. +.Pp +The +.Sx FILES +section includes more details on where this file lives. +.Ss Helper functions +The +.Sq base , +.Sq string , +and +.Sq table +Lua modules are fully available in the context of a +.Nm . +.Pp +The following extra functions are provided by Kyua: +.Bl -tag -width XX -offset indent +.It Ft string Fn current_kyuafile +Returns the absolute path to the current +.Nm . +.It Ft string Fn fs.basename "string path" +Returns the last component of the given path. +.It Ft string Fn fs.dirname "string path" +Returns the given path without its last component or a dot if the path has +a single component. +.It Ft bool Fn fs.exists "string path" +Checks if the given path exists. +If the path is not absolute, it is relative to the directory containing the +.Nm +in which the call to this function occurs. +.It Ft iterator Fn fs.files "string path" +Opens a directory for scanning of its entries. +The returned iterator yields an entry on each call, and the entry is simply +the filename. +If the path is not absolute, it is relative to the directory containing the +.Nm +in which the call to this function occurs. +.It Ft is_absolute Fn fs.is_absolute "string path" +Returns true if the given path is absolute; false otherwise. +.It Ft join Fn fs.join "string path" "string path" +Concatenates the two paths. +The second path cannot be absolute. +.El +.Sh FILES +.Bl -tag -width XX +.It Pa __TESTSDIR__/Kyuafile . +Top-level +.Nm +for the current system. +.It Pa __EGDIR__/Kyuafile.top . +Sample file to serve as a top-level +.Nm . +.El +.Sh EXAMPLES +The following +.Nm +is the simplest you can define. +It provides a test suite definition and registers a couple of different test +programs using different interfaces: +.Bd -literal -offset indent +syntax(2) + +test_suite('first') + +atf_test_program{name='integration_test'} +plain_test_program{name='legacy_test'} +.Ed +.Pp +The following example is a bit more elaborate. +It introduces some metadata properties to the test program definitions and +recurses into a couple of subdirectories: +.Bd -literal -offset indent +syntax(2) + +test_suite('second') + +plain_test_program{name='legacy_test', + allowed_architectures='amd64 i386', + required_files='/bin/ls', + timeout=30} + +tap_test_program{name='privileged_test', + required_user='root'} + +include('module-1/Kyuafile') +include('module-2/Kyuafile') +.Ed +.Pp +The syntax to define custom properties may be not obvious if their names +have any characters that make the property name not be a valid Lua identifier. +Dashes are just one example. +To set such properties, do something like this: +.Bd -literal -offset indent +syntax(2) + +test_suite('FreeBSD') + +plain_test_program{name='the_test', + ['custom.FreeBSD-Bug-Id']='category/12345'} +.Ed +.Ss Connecting disjoint test suites +Now suppose you had various test suites on your file system and you would +like to connect them together so that they could be executed and treated as +a single unit. +The test suites we would like to connect live under +.Pa /usr/tests , +.Pa /usr/local/tests +and +.Pa ~/local/tests . +.Pp +We cannot create a +.Nm +that references these because the +.Fn include +directive does not support absolute paths. +Instead, what we can do is create a shadow tree using symbolic links: +.Bd -literal -offset indent +$ mkdir ~/everything +$ ln -s /usr/tests ~/everything/system-tests +$ ln -s /usr/local/tests ~/everything/local-tests +$ ln -s ~/local/tests ~/everything/home-tests +.Ed +.Pp +And then we create an +.Pa ~/everything/Kyuafile +file to drive the execution of the integrated test suite: +.Bd -literal -offset indent +syntax(2) + +test_suite('test-all-the-things') + +include('system-tests/Kyuafile') +include('local-tests/Kyuafile') +include('home-tests/Kyuafile') +.Ed +.Pp +Or, simply, you could reuse the sample top-level +.Nm +to avoid having to manually craft the list of directories into which to +recurse: +.Bd -literal -offset indent +$ cp __EGDIR__/Kyuafile.top ~/everything/Kyuafile +.Ed +.Sh SEE ALSO +.Xr kyua 1 diff --git a/doc/manbuild.sh b/doc/manbuild.sh new file mode 100755 index 000000000000..e01239909183 --- /dev/null +++ b/doc/manbuild.sh @@ -0,0 +1,171 @@ +#! /bin/sh +# Copyright 2014 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# \file doc/manbuild.sh +# Generates a manual page from a source file. +# +# Input files can have __VAR__-style patterns in them that are replaced +# with the values provided by the caller via the -v VAR=VALUE flag. +# +# Input files can also include other files using the __include__ directive, +# which takes a relative path to the file to include plus an optional +# collection of additional variables to replace in the included file. + + +# Name of the running program for error reporting purposes. +Prog_Name="${0##*/}" + + +# Prints an error message and exits. +# +# Args: +# ...: The error message to print. Multiple arguments are joined with a +# single space separator. +err() { + echo "${Prog_Name}: ${*}" 1>&2 + exit 1 +} + + +# Invokes sed(1) translating input variables to expressions. +# +# Args: +# ...: List of var=value pairs to replace. +# +# Returns: +# True if the operation succeeds; false otherwise. +sed_with_vars() { + local vars="${*}" + + set -- + for pair in ${vars}; do + local var="$(echo "${pair}" | cut -d = -f 1)" + local value="$(echo "${pair}" | cut -d = -f 2-)" + set -- "${@}" -e"s&__${var}__&${value}&g" + done + + if [ "${#}" -gt 0 ]; then + sed "${@}" + else + cat + fi +} + + +# Generates the manual page reading from stdin and dumping to stdout. +# +# Args: +# include_dir: Path to the directory containing the include files. +# ...: List of var=value pairs to replace in the manpage. +# +# Returns: +# True if the generation succeeds; false otherwise. +generate() { + local include_dir="${1}"; shift + + while :; do + local read_ok=yes + local oldifs="${IFS}" + IFS= + read -r line || read_ok=no + IFS="${oldifs}" + [ "${read_ok}" = yes ] || break + + case "${line}" in + __include__*) + local file="$(echo "${line}" | cut -d ' ' -f 2)" + local extra_vars="$(echo "${line}" | cut -d ' ' -f 3-)" + # If we fail to output the included file, just leave the line as + # is. validate_file() will later error out. + [ -f "${include_dir}/${file}" ] || echo "${line}" + generate <"${include_dir}/${file}" "${include_dir}" \ + "${@}" ${extra_vars} || echo "${line}" + ;; + + *) + echo "${line}" + ;; + esac + done | sed_with_vars "${@}" +} + + +# Validates that the manual page has been properly generated. +# +# In particular, this checks if any directives or common replacement patterns +# have been left in place. +# +# Returns: +# True if the manual page is valid; false otherwise. +validate_file() { + local filename="${1}" + + if grep '__[A-Za-z0-9]*__' "${filename}" >/dev/null; then + return 1 + else + return 0 + fi +} + + +# Program entry point. +main() { + local vars= + + while getopts :v: arg; do + case "${arg}" in + v) + vars="${vars} ${OPTARG}" + ;; + + \?) + err "Unknown option -${OPTARG}" + ;; + esac + done + shift $((${OPTIND} - 1)) + + [ ${#} -eq 2 ] || err "Must provide input and output names as arguments" + local input="${1}"; shift + local output="${1}"; shift + + trap "rm -f '${output}.tmp'" EXIT HUP INT TERM + generate "$(dirname "${input}")" ${vars} \ + <"${input}" >"${output}.tmp" \ + || err "Failed to generate ${output}" + if validate_file "${output}.tmp"; then + : + else + err "Failed to generate ${output}; some patterns were left unreplaced" + fi + mv "${output}.tmp" "${output}" +} + + +main "${@}" diff --git a/doc/manbuild_test.sh b/doc/manbuild_test.sh new file mode 100755 index 000000000000..87234324e829 --- /dev/null +++ b/doc/manbuild_test.sh @@ -0,0 +1,235 @@ +# Copyright 2014 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# Absolute path to the uninstalled script. +MANBUILD="__MANBUILD__" + + +atf_test_case empty +empty_body() { + touch input + atf_check "${MANBUILD}" input output + atf_check cat output +} + + +atf_test_case no_replacements +no_replacements_body() { + cat >input <input <expout <input <expout <input <expout <doc/input <doc/subdir/chunk <doc/chunk <expout <input <chunk <expout <input <input < +#include + +#include "engine/filters.hpp" +#include "engine/kyuafile.hpp" +#include "engine/scanner.hpp" +#include "engine/scheduler.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/auto_cleaners.hpp" +#include "utils/optional.ipp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::optional; + + +/// Executes the operation. +/// +/// \param kyuafile_path The path to the Kyuafile to be loaded. +/// \param build_root If not none, path to the built test programs. +/// \param filter The test case filter to locate the test to debug. +/// \param user_config The end-user configuration properties. +/// \param stdout_path The name of the file into which to store the test case +/// stdout. +/// \param stderr_path The name of the file into which to store the test case +/// stderr. +/// +/// \returns A structure with all results computed by this driver. +drivers::debug_test::result +drivers::debug_test::drive(const fs::path& kyuafile_path, + const optional< fs::path > build_root, + const engine::test_filter& filter, + const config::tree& user_config, + const fs::path& stdout_path, + const fs::path& stderr_path) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + kyuafile_path, build_root, user_config, handle); + std::set< engine::test_filter > filters; + filters.insert(filter); + + engine::scanner scanner(kyuafile.test_programs(), filters); + optional< engine::scan_result > match; + while (!match && !scanner.done()) { + match = scanner.yield(); + } + if (!match) { + throw std::runtime_error(F("Unknown test case '%s'") % filter.str()); + } else if (!scanner.done()) { + throw std::runtime_error(F("The filter '%s' matches more than one test " + "case") % filter.str()); + } + INV(match && scanner.done()); + const model::test_program_ptr test_program = match.get().first; + const std::string& test_case_name = match.get().second; + + scheduler::result_handle_ptr result_handle = handle.debug_test( + test_program, test_case_name, user_config, + stdout_path, stderr_path); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + const model::test_result test_result = test_result_handle->test_result(); + result_handle->cleanup(); + + handle.check_interrupt(); + handle.cleanup(); + + return result(engine::test_filter( + test_program->relative_path(), test_case_name), test_result); +} diff --git a/drivers/debug_test.hpp b/drivers/debug_test.hpp new file mode 100644 index 000000000000..cbaa2f6acea0 --- /dev/null +++ b/drivers/debug_test.hpp @@ -0,0 +1,79 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file drivers/debug_test.hpp +/// Driver to run a single test in a controlled manner. +/// +/// This driver module implements the logic to execute a particular test +/// with hooks into the runtime procedure. This is to permit debugging the +/// behavior of the test. + +#if !defined(DRIVERS_DEBUG_TEST_HPP) +#define DRIVERS_DEBUG_TEST_HPP + +#include "engine/filters.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace drivers { +namespace debug_test { + + +/// Tuple containing the results of this driver. +class result { +public: + /// A filter matching the executed test case only. + engine::test_filter test_case; + + /// The result of the test case. + model::test_result test_result; + + /// Initializer for the tuple's fields. + /// + /// \param test_case_ The matched test case. + /// \param test_result_ The result of the test case. + result(const engine::test_filter& test_case_, + const model::test_result& test_result_) : + test_case(test_case_), + test_result(test_result_) + { + } +}; + + +result drive(const utils::fs::path&, const utils::optional< utils::fs::path >, + const engine::test_filter&, const utils::config::tree&, + const utils::fs::path&, const utils::fs::path&); + + +} // namespace debug_test +} // namespace drivers + +#endif // !defined(DRIVERS_DEBUG_TEST_HPP) diff --git a/drivers/list_tests.cpp b/drivers/list_tests.cpp new file mode 100644 index 000000000000..b56706d30b93 --- /dev/null +++ b/drivers/list_tests.cpp @@ -0,0 +1,84 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "drivers/list_tests.hpp" + +#include "engine/exceptions.hpp" +#include "engine/filters.hpp" +#include "engine/kyuafile.hpp" +#include "engine/scanner.hpp" +#include "engine/scheduler.hpp" +#include "model/test_program.hpp" +#include "utils/optional.ipp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::optional; + + +/// Pure abstract destructor. +drivers::list_tests::base_hooks::~base_hooks(void) +{ +} + + +/// Executes the operation. +/// +/// \param kyuafile_path The path to the Kyuafile to be loaded. +/// \param build_root If not none, path to the built test programs. +/// \param filters The test case filters as provided by the user. +/// \param user_config The end-user configuration properties. +/// \param hooks The hooks for this execution. +/// +/// \returns A structure with all results computed by this driver. +drivers::list_tests::result +drivers::list_tests::drive(const fs::path& kyuafile_path, + const optional< fs::path > build_root, + const std::set< engine::test_filter >& filters, + const config::tree& user_config, + base_hooks& hooks) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + kyuafile_path, build_root, user_config, handle); + + engine::scanner scanner(kyuafile.test_programs(), filters); + + while (!scanner.done()) { + const optional< engine::scan_result > result = scanner.yield(); + INV(result); + hooks.got_test_case(*result.get().first, result.get().second); + } + + handle.cleanup(); + + return result(scanner.unused_filters()); +} diff --git a/drivers/list_tests.hpp b/drivers/list_tests.hpp new file mode 100644 index 000000000000..6b1257e41b22 --- /dev/null +++ b/drivers/list_tests.hpp @@ -0,0 +1,92 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file drivers/list_tests.hpp +/// Driver to obtain a list of test cases out of a test suite. +/// +/// This driver module implements the logic to extract a list of test cases out +/// of a particular test suite. + +#if !defined(DRIVERS_LIST_TESTS_HPP) +#define DRIVERS_LIST_TESTS_HPP + +#include +#include + +#include "engine/filters_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace drivers { +namespace list_tests { + + +/// Abstract definition of the hooks for this driver. +class base_hooks { +public: + virtual ~base_hooks(void) = 0; + + /// Called when a test case is identified in a test suite. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the located test case. + virtual void got_test_case(const model::test_program& test_program, + const std::string& test_case_name) = 0; +}; + + +/// Tuple containing the results of this driver. +class result { +public: + /// Filters that did not match any available test case. + /// + /// The presence of any filters here probably indicates a usage error. If a + /// test filter does not match any test case, it is probably a typo. + std::set< engine::test_filter > unused_filters; + + /// Initializer for the tuple's fields. + /// + /// \param unused_filters_ The filters that did not match any test case. + result(const std::set< engine::test_filter >& unused_filters_) : + unused_filters(unused_filters_) + { + } +}; + + +result drive(const utils::fs::path&, const utils::optional< utils::fs::path >, + const std::set< engine::test_filter >&, + const utils::config::tree&, base_hooks&); + + +} // namespace list_tests +} // namespace drivers + +#endif // !defined(DRIVERS_LIST_TESTS_HPP) diff --git a/drivers/list_tests_helpers.cpp b/drivers/list_tests_helpers.cpp new file mode 100644 index 000000000000..2b40b1e11db1 --- /dev/null +++ b/drivers/list_tests_helpers.cpp @@ -0,0 +1,98 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include + +#include + +#include "utils/test_utils.ipp" + + +ATF_TEST_CASE(config_in_head); +ATF_TEST_CASE_HEAD(config_in_head) +{ + if (has_config_var("the-variable")) { + set_md_var("descr", "the-variable is " + + get_config_var("the-variable")); + } +} +ATF_TEST_CASE_BODY(config_in_head) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE(crash_list); +ATF_TEST_CASE_HEAD(crash_list) +{ + utils::abort_without_coredump(); +} +ATF_TEST_CASE_BODY(crash_list) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(no_properties); +ATF_TEST_CASE_BODY(no_properties) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE(some_properties); +ATF_TEST_CASE_HEAD(some_properties) +{ + set_md_var("descr", "This is a description"); + set_md_var("require.progs", "non-existent /bin/ls"); +} +ATF_TEST_CASE_BODY(some_properties) +{ + utils::abort_without_coredump(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + std::string enabled; + + const char* tests = std::getenv("TESTS"); + if (tests == NULL) + enabled = "config_in_head crash_list no_properties some_properties"; + else + enabled = tests; + + if (enabled.find("config_in_head") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, config_in_head); + if (enabled.find("crash_list") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, crash_list); + if (enabled.find("no_properties") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, no_properties); + if (enabled.find("some_properties") != std::string::npos) + ATF_ADD_TEST_CASE(tcs, some_properties); +} diff --git a/drivers/list_tests_test.cpp b/drivers/list_tests_test.cpp new file mode 100644 index 000000000000..752b251052ad --- /dev/null +++ b/drivers/list_tests_test.cpp @@ -0,0 +1,287 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "drivers/list_tests.hpp" + +extern "C" { +#include + +#include +} + +#include +#include +#include + +#include + +#include "cli/cmd_list.hpp" +#include "cli/common.ipp" +#include "engine/atf.hpp" +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "engine/filters.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/test_utils.ipp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Gets the path to the helpers for this test program. +/// +/// \param test_case A pointer to the currently running test case. +/// +/// \return The path to the helpers binary. +static fs::path +helpers(const atf::tests::tc* test_case) +{ + return fs::path(test_case->get_config_var("srcdir")) / + "list_tests_helpers"; +} + + +/// Hooks to capture the incremental listing of test cases. +class capture_hooks : public drivers::list_tests::base_hooks { +public: + /// Set of the listed test cases in a program:test_case form. + std::set< std::string > test_cases; + + /// Set of the listed test cases in a program:test_case form. + std::map< std::string, model::metadata > metadatas; + + /// Called when a test case is identified in a test suite. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the located test case. + virtual void + got_test_case(const model::test_program& test_program, + const std::string& test_case_name) + { + const std::string ident = F("%s:%s") % + test_program.relative_path() % test_case_name; + test_cases.insert(ident); + + metadatas.insert(std::map< std::string, model::metadata >::value_type( + ident, test_program.find(test_case_name).get_metadata())); + } +}; + + +/// Creates a mock test suite. +/// +/// \param tc Pointer to the caller test case; needed to obtain the srcdir +/// variable of the caller. +/// \param source_root Basename of the directory that will contain the +/// Kyuafiles. +/// \param build_root Basename of the directory that will contain the test +/// programs. May or may not be the same as source_root. +static void +create_helpers(const atf::tests::tc* tc, const fs::path& source_root, + const fs::path& build_root) +{ + ATF_REQUIRE(::mkdir(source_root.c_str(), 0755) != -1); + ATF_REQUIRE(::mkdir((source_root / "dir").c_str(), 0755) != -1); + if (source_root != build_root) { + ATF_REQUIRE(::mkdir(build_root.c_str(), 0755) != -1); + ATF_REQUIRE(::mkdir((build_root / "dir").c_str(), 0755) != -1); + } + ATF_REQUIRE(::symlink(helpers(tc).c_str(), + (build_root / "dir/program").c_str()) != -1); + + atf::utils::create_file( + (source_root / "Kyuafile").str(), + "syntax(2)\n" + "include('dir/Kyuafile')\n"); + + atf::utils::create_file( + (source_root / "dir/Kyuafile").str(), + "syntax(2)\n" + "atf_test_program{name='program', test_suite='suite-name'}\n"); +} + + +/// Runs the mock test suite. +/// +/// \param source_root Path to the directory that contains the Kyuafiles. +/// \param build_root If not none, path to the directory that contains the test +/// programs. +/// \param hooks The hooks to use during the listing. +/// \param filter_program If not null, the filter on the test program name. +/// \param filter_test_case If not null, the filter on the test case name. +/// \param the_variable If not null, the value to pass to the test program as +/// its "the-variable" configuration property. +/// +/// \return The result data of the driver. +static drivers::list_tests::result +run_helpers(const fs::path& source_root, + const optional< fs::path > build_root, + drivers::list_tests::base_hooks& hooks, + const char* filter_program = NULL, + const char* filter_test_case = NULL, + const char* the_variable = NULL) +{ + std::set< engine::test_filter > filters; + if (filter_program != NULL && filter_test_case != NULL) + filters.insert(engine::test_filter(fs::path(filter_program), + filter_test_case)); + + config::tree user_config = engine::empty_config(); + if (the_variable != NULL) { + user_config.set_string("test_suites.suite-name.the-variable", + the_variable); + } + + return drivers::list_tests::drive(source_root / "Kyuafile", build_root, + filters, user_config, hooks); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(one_test_case); +ATF_TEST_CASE_BODY(one_test_case) +{ + utils::setenv("TESTS", "some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("root"), fs::path("root")); + run_helpers(fs::path("root"), none, hooks); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:some_properties"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(many_test_cases); +ATF_TEST_CASE_BODY(many_test_cases) +{ + utils::setenv("TESTS", "no_properties some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("root"), fs::path("root")); + run_helpers(fs::path("root"), none, hooks); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:no_properties"); + exp_test_cases.insert("dir/program:some_properties"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filter_match); +ATF_TEST_CASE_BODY(filter_match) +{ + utils::setenv("TESTS", "no_properties some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("root"), fs::path("root")); + run_helpers(fs::path("root"), none, hooks, "dir/program", + "some_properties"); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:some_properties"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(build_root); +ATF_TEST_CASE_BODY(build_root) +{ + utils::setenv("TESTS", "no_properties some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("source"), fs::path("build")); + run_helpers(fs::path("source"), utils::make_optional(fs::path("build")), + hooks); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:no_properties"); + exp_test_cases.insert("dir/program:some_properties"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config_in_head); +ATF_TEST_CASE_BODY(config_in_head) +{ + utils::setenv("TESTS", "config_in_head"); + capture_hooks hooks; + create_helpers(this, fs::path("source"), fs::path("build")); + run_helpers(fs::path("source"), utils::make_optional(fs::path("build")), + hooks, NULL, NULL, "magic value"); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:config_in_head"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); + + const model::metadata& metadata = hooks.metadatas.find( + "dir/program:config_in_head")->second; + ATF_REQUIRE_EQ("the-variable is magic value", metadata.description()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(crash); +ATF_TEST_CASE_BODY(crash) +{ + utils::setenv("TESTS", "crash_list some_properties"); + capture_hooks hooks; + create_helpers(this, fs::path("root"), fs::path("root")); + run_helpers(fs::path("root"), none, hooks, "dir/program"); + + std::set< std::string > exp_test_cases; + exp_test_cases.insert("dir/program:__test_cases_list__"); + ATF_REQUIRE(exp_test_cases == hooks.test_cases); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "atf", std::shared_ptr< scheduler::interface >( + new engine::atf_interface())); + + ATF_ADD_TEST_CASE(tcs, one_test_case); + ATF_ADD_TEST_CASE(tcs, many_test_cases); + ATF_ADD_TEST_CASE(tcs, filter_match); + ATF_ADD_TEST_CASE(tcs, build_root); + ATF_ADD_TEST_CASE(tcs, config_in_head); + ATF_ADD_TEST_CASE(tcs, crash); +} diff --git a/drivers/report_junit.cpp b/drivers/report_junit.cpp new file mode 100644 index 000000000000..4c14d535675f --- /dev/null +++ b/drivers/report_junit.cpp @@ -0,0 +1,258 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "drivers/report_junit.hpp" + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "model/types.hpp" +#include "store/read_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/text/operations.hpp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace text = utils::text; + + +/// Converts a test program name into a class-like name. +/// +/// \param test_program Test program from which to extract the name. +/// +/// \return A class-like representation of the test program's identifier. +std::string +drivers::junit_classname(const model::test_program& test_program) +{ + std::string classname = test_program.relative_path().str(); + std::replace(classname.begin(), classname.end(), '/', '.'); + return classname; +} + + +/// Converts a test case's duration to a second-based representation. +/// +/// \param delta The duration to convert. +/// +/// \return A second-based with millisecond-precision representation of the +/// input duration. +std::string +drivers::junit_duration(const datetime::delta& delta) +{ + return F("%.3s") % (delta.seconds + (delta.useconds / 1000000.0)); +} + + +/// String to prepend to the formatted test case metadata. +const char* const drivers::junit_metadata_header = + "Test case metadata\n" + "------------------\n" + "\n"; + + +/// String to prepend to the formatted test case timing details. +const char* const drivers::junit_timing_header = + "\n" + "Timing information\n" + "------------------\n" + "\n"; + + +/// String to append to the formatted test case metadata. +const char* const drivers::junit_stderr_header = + "\n" + "Original stderr\n" + "---------------\n" + "\n"; + + +/// Formats a test's metadata for recording in stderr. +/// +/// \param metadata The metadata to format. +/// +/// \return A string with the metadata contents that can be prepended to the +/// original test's stderr. +std::string +drivers::junit_metadata(const model::metadata& metadata) +{ + const model::properties_map props = metadata.to_properties(); + if (props.empty()) + return ""; + + std::ostringstream output; + output << junit_metadata_header; + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + if ((*iter).second.empty()) { + output << F("%s is empty\n") % (*iter).first; + } else { + output << F("%s = %s\n") % (*iter).first % (*iter).second; + } + } + return output.str(); +} + + +/// Formats a test's timing information for recording in stderr. +/// +/// \param start_time The start time of the test. +/// \param end_time The end time of the test. +/// +/// \return A string with the timing information that can be prepended to the +/// original test's stderr. +std::string +drivers::junit_timing(const datetime::timestamp& start_time, + const datetime::timestamp& end_time) +{ + std::ostringstream output; + output << junit_timing_header; + output << F("Start time: %s\n") % start_time.to_iso8601_in_utc(); + output << F("End time: %s\n") % end_time.to_iso8601_in_utc(); + output << F("Duration: %ss\n") % junit_duration(end_time - start_time); + return output.str(); +} + + +/// Constructor for the hooks. +/// +/// \param [out] output_ Stream to which to write the report. +drivers::report_junit_hooks::report_junit_hooks(std::ostream& output_) : + _output(output_) +{ +} + + +/// Callback executed when the context is loaded. +/// +/// \param context The context loaded from the database. +void +drivers::report_junit_hooks::got_context(const model::context& context) +{ + _output << "\n"; + _output << "\n"; + + _output << "\n"; + _output << F("\n") + % text::escape_xml(context.cwd().str()); + for (model::properties_map::const_iterator iter = + context.env().begin(); iter != context.env().end(); ++iter) { + _output << F("\n") + % text::escape_xml((*iter).first) + % text::escape_xml((*iter).second); + } + _output << "\n"; +} + + +/// Callback executed when a test results is found. +/// +/// \param iter Container for the test result's data. +void +drivers::report_junit_hooks::got_result(store::results_iterator& iter) +{ + const model::test_result result = iter.result(); + + _output << F("\n") + % text::escape_xml(junit_classname(*iter.test_program())) + % text::escape_xml(iter.test_case_name()) + % junit_duration(iter.end_time() - iter.start_time()); + + std::string stderr_contents; + + switch (result.type()) { + case model::test_result_failed: + _output << F("\n") + % text::escape_xml(result.reason()); + break; + + case model::test_result_expected_failure: + stderr_contents += ("Expected failure result details\n" + "-------------------------------\n" + "\n" + + result.reason() + "\n" + "\n"); + break; + + case model::test_result_passed: + // Passed results have no status nodes. + break; + + case model::test_result_skipped: + _output << "\n"; + stderr_contents += ("Skipped result details\n" + "----------------------\n" + "\n" + + result.reason() + "\n" + "\n"); + break; + + default: + _output << F("\n") + % text::escape_xml(result.reason()); + } + + const std::string stdout_contents = iter.stdout_contents(); + if (!stdout_contents.empty()) { + _output << F("%s\n") + % text::escape_xml(stdout_contents); + } + + { + const model::test_case& test_case = iter.test_program()->find( + iter.test_case_name()); + stderr_contents += junit_metadata(test_case.get_metadata()); + } + stderr_contents += junit_timing(iter.start_time(), iter.end_time()); + { + stderr_contents += junit_stderr_header; + const std::string real_stderr_contents = iter.stderr_contents(); + if (real_stderr_contents.empty()) { + stderr_contents += "\n"; + } else { + stderr_contents += real_stderr_contents; + } + } + _output << "" << text::escape_xml(stderr_contents) + << "\n"; + + _output << "\n"; +} + + +/// Finalizes the report. +void +drivers::report_junit_hooks::end(const drivers::scan_results::result& /* r */) +{ + _output << "\n"; +} diff --git a/drivers/report_junit.hpp b/drivers/report_junit.hpp new file mode 100644 index 000000000000..adb0aa12757e --- /dev/null +++ b/drivers/report_junit.hpp @@ -0,0 +1,75 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file drivers/report_junit.hpp +/// Generates a JUnit report out of a test suite execution. + +#if !defined(ENGINE_REPORT_JUNIT_HPP) +#define ENGINE_REPORT_JUNIT_HPP + +#include +#include + +#include "drivers/scan_results.hpp" +#include "model/metadata_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/datetime_fwd.hpp" + +namespace drivers { + + +extern const char* const junit_metadata_header; +extern const char* const junit_timing_header; +extern const char* const junit_stderr_header; + + +std::string junit_classname(const model::test_program&); +std::string junit_duration(const utils::datetime::delta&); +std::string junit_metadata(const model::metadata&); +std::string junit_timing(const utils::datetime::timestamp&, + const utils::datetime::timestamp&); + + +/// Hooks for the scan_results driver to generate a JUnit report. +class report_junit_hooks : public drivers::scan_results::base_hooks { + /// Stream to which to write the report. + std::ostream& _output; + +public: + report_junit_hooks(std::ostream&); + + void got_context(const model::context&); + void got_result(store::results_iterator&); + + void end(const drivers::scan_results::result&); +}; + + +} // namespace drivers + +#endif // !defined(ENGINE_REPORT_JUNIT_HPP) diff --git a/drivers/report_junit_test.cpp b/drivers/report_junit_test.cpp new file mode 100644 index 000000000000..462dca72f9be --- /dev/null +++ b/drivers/report_junit_test.cpp @@ -0,0 +1,415 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "drivers/report_junit.hpp" + +#include +#include + +#include + +#include "drivers/scan_results.hpp" +#include "engine/filters.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace units = utils::units; + +using utils::none; + + +namespace { + + +/// Formatted metadata for a test case with defaults. +static const char* const default_metadata = + "allowed_architectures is empty\n" + "allowed_platforms is empty\n" + "description is empty\n" + "has_cleanup = false\n" + "is_exclusive = false\n" + "required_configs is empty\n" + "required_disk_space = 0\n" + "required_files is empty\n" + "required_memory = 0\n" + "required_programs is empty\n" + "required_user is empty\n" + "timeout = 300\n"; + + +/// Formatted metadata for a test case constructed with the "with_metadata" flag +/// set to true in add_tests. +static const char* const overriden_metadata = + "allowed_architectures is empty\n" + "allowed_platforms is empty\n" + "description = Textual description\n" + "has_cleanup = false\n" + "is_exclusive = false\n" + "required_configs is empty\n" + "required_disk_space = 0\n" + "required_files is empty\n" + "required_memory = 0\n" + "required_programs is empty\n" + "required_user is empty\n" + "timeout = 5678\n"; + + +/// Populates the context of the given database. +/// +/// \param tx Transaction to use for the writes to the database. +/// \param env_vars Number of environment variables to add to the context. +static void +add_context(store::write_transaction& tx, const std::size_t env_vars) +{ + std::map< std::string, std::string > env; + for (std::size_t i = 0; i < env_vars; i++) + env[F("VAR%s") % i] = F("Value %s") % i; + const model::context context(fs::path("/root"), env); + (void)tx.put_context(context); +} + + +/// Adds a new test program with various test cases to the given database. +/// +/// \param tx Transaction to use for the writes to the database. +/// \param prog Test program name. +/// \param results Collection of results for the added test cases. The size of +/// this vector indicates the number of tests in the test program. +/// \param with_metadata Whether to add metadata overrides to the test cases. +/// \param with_output Whether to add stdout/stderr messages to the test cases. +static void +add_tests(store::write_transaction& tx, + const char* prog, + const std::vector< model::test_result >& results, + const bool with_metadata, const bool with_output) +{ + model::test_program_builder test_program_builder( + "plain", fs::path(prog), fs::path("/root"), "suite"); + + for (std::size_t j = 0; j < results.size(); j++) { + model::metadata_builder builder; + if (with_metadata) { + builder.set_description("Textual description"); + builder.set_timeout(datetime::delta(5678, 0)); + } + test_program_builder.add_test_case(F("t%s") % j, builder.build()); + } + + const model::test_program test_program = test_program_builder.build(); + const int64_t tp_id = tx.put_test_program(test_program); + + for (std::size_t j = 0; j < results.size(); j++) { + const int64_t tc_id = tx.put_test_case(test_program, F("t%s") % j, + tp_id); + const datetime::timestamp start = + datetime::timestamp::from_microseconds(0); + const datetime::timestamp end = + datetime::timestamp::from_microseconds(j * 1000000 + 500000); + tx.put_result(results[j], tc_id, start, end); + + if (with_output) { + atf::utils::create_file("fake-out", F("stdout file %s") % j); + tx.put_test_case_file("__STDOUT__", fs::path("fake-out"), tc_id); + atf::utils::create_file("fake-err", F("stderr file %s") % j); + tx.put_test_case_file("__STDERR__", fs::path("fake-err"), tc_id); + } + } +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_classname); +ATF_TEST_CASE_BODY(junit_classname) +{ + const model::test_program test_program = model::test_program_builder( + "plain", fs::path("dir1/dir2/program"), fs::path("/root"), "suite") + .build(); + + ATF_REQUIRE_EQ("dir1.dir2.program", drivers::junit_classname(test_program)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_duration); +ATF_TEST_CASE_BODY(junit_duration) +{ + ATF_REQUIRE_EQ("0.457", + drivers::junit_duration(datetime::delta(0, 456700))); + ATF_REQUIRE_EQ("3.120", + drivers::junit_duration(datetime::delta(3, 120000))); + ATF_REQUIRE_EQ("5.000", drivers::junit_duration(datetime::delta(5, 0))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_metadata__defaults); +ATF_TEST_CASE_BODY(junit_metadata__defaults) +{ + const model::metadata metadata = model::metadata_builder().build(); + + const std::string expected = std::string() + + drivers::junit_metadata_header + + default_metadata; + + ATF_REQUIRE_EQ(expected, drivers::junit_metadata(metadata)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_metadata__overrides); +ATF_TEST_CASE_BODY(junit_metadata__overrides) +{ + const model::metadata metadata = model::metadata_builder() + .add_allowed_architecture("arch1") + .add_allowed_platform("platform1") + .set_description("This is a test") + .set_has_cleanup(true) + .set_is_exclusive(true) + .add_required_config("config1") + .set_required_disk_space(units::bytes(456)) + .add_required_file(fs::path("file1")) + .set_required_memory(units::bytes(123)) + .add_required_program(fs::path("prog1")) + .set_required_user("root") + .set_timeout(datetime::delta(10, 0)) + .build(); + + const std::string expected = std::string() + + drivers::junit_metadata_header + + "allowed_architectures = arch1\n" + + "allowed_platforms = platform1\n" + + "description = This is a test\n" + + "has_cleanup = true\n" + + "is_exclusive = true\n" + + "required_configs = config1\n" + + "required_disk_space = 456\n" + + "required_files = file1\n" + + "required_memory = 123\n" + + "required_programs = prog1\n" + + "required_user = root\n" + + "timeout = 10\n"; + + ATF_REQUIRE_EQ(expected, drivers::junit_metadata(metadata)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(junit_timing); +ATF_TEST_CASE_BODY(junit_timing) +{ + const std::string expected = std::string() + + drivers::junit_timing_header + + "Start time: 2015-06-12T01:02:35.123456Z\n" + "End time: 2016-07-13T18:47:10.000001Z\n" + "Duration: 34364674.877s\n"; + + const datetime::timestamp start_time = + datetime::timestamp::from_values(2015, 6, 12, 1, 2, 35, 123456); + const datetime::timestamp end_time = + datetime::timestamp::from_values(2016, 7, 13, 18, 47, 10, 1); + + ATF_REQUIRE_EQ(expected, drivers::junit_timing(start_time, end_time)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_junit_hooks__minimal); +ATF_TEST_CASE_BODY(report_junit_hooks__minimal) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + add_context(tx, 0); + tx.commit(); + backend.close(); + + std::ostringstream output; + + drivers::report_junit_hooks hooks(output); + drivers::scan_results::drive(fs::path("test.db"), + std::set< engine::test_filter >(), + hooks); + + const char* expected = + "\n" + "\n" + "\n" + "\n" + "\n" + "\n"; + ATF_REQUIRE_EQ(expected, output.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_junit_hooks__some_tests); +ATF_TEST_CASE_BODY(report_junit_hooks__some_tests) +{ + std::vector< model::test_result > results1; + results1.push_back(model::test_result( + model::test_result_broken, "Broken")); + results1.push_back(model::test_result( + model::test_result_expected_failure, "XFail")); + results1.push_back(model::test_result( + model::test_result_failed, "Failed")); + std::vector< model::test_result > results2; + results2.push_back(model::test_result( + model::test_result_passed)); + results2.push_back(model::test_result( + model::test_result_skipped, "Skipped")); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + add_context(tx, 2); + add_tests(tx, "dir/prog-1", results1, false, false); + add_tests(tx, "dir/sub/prog-2", results2, true, true); + tx.commit(); + backend.close(); + + std::ostringstream output; + + drivers::report_junit_hooks hooks(output); + drivers::scan_results::drive(fs::path("test.db"), + std::set< engine::test_filter >(), + hooks); + + const std::string expected = std::string() + + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + + "\n" + "\n" + "" + + drivers::junit_metadata_header + + default_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:00.500000Z\n" + "Duration: 0.500s\n" + + drivers::junit_stderr_header + + "<EMPTY>\n" + "\n" + "\n" + + "\n" + "" + "Expected failure result details\n" + "-------------------------------\n" + "\n" + "XFail\n" + "\n" + + drivers::junit_metadata_header + + default_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:01.500000Z\n" + "Duration: 1.500s\n" + + drivers::junit_stderr_header + + "<EMPTY>\n" + "\n" + "\n" + + "\n" + "\n" + "" + + drivers::junit_metadata_header + + default_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:02.500000Z\n" + "Duration: 2.500s\n" + + drivers::junit_stderr_header + + "<EMPTY>\n" + "\n" + "\n" + + "\n" + "stdout file 0\n" + "" + + drivers::junit_metadata_header + + overriden_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:00.500000Z\n" + "Duration: 0.500s\n" + + drivers::junit_stderr_header + + "stderr file 0\n" + "\n" + + "\n" + "\n" + "stdout file 1\n" + "" + "Skipped result details\n" + "----------------------\n" + "\n" + "Skipped\n" + "\n" + + drivers::junit_metadata_header + + overriden_metadata + + drivers::junit_timing_header + + "Start time: 1970-01-01T00:00:00.000000Z\n" + "End time: 1970-01-01T00:00:01.500000Z\n" + "Duration: 1.500s\n" + + drivers::junit_stderr_header + + "stderr file 1\n" + "\n" + + "\n"; + ATF_REQUIRE_EQ(expected, output.str()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, junit_classname); + + ATF_ADD_TEST_CASE(tcs, junit_duration); + + ATF_ADD_TEST_CASE(tcs, junit_metadata__defaults); + ATF_ADD_TEST_CASE(tcs, junit_metadata__overrides); + + ATF_ADD_TEST_CASE(tcs, junit_timing); + + ATF_ADD_TEST_CASE(tcs, report_junit_hooks__minimal); + ATF_ADD_TEST_CASE(tcs, report_junit_hooks__some_tests); +} diff --git a/drivers/run_tests.cpp b/drivers/run_tests.cpp new file mode 100644 index 000000000000..d92940005242 --- /dev/null +++ b/drivers/run_tests.cpp @@ -0,0 +1,344 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "drivers/run_tests.hpp" + +#include + +#include "engine/config.hpp" +#include "engine/filters.hpp" +#include "engine/kyuafile.hpp" +#include "engine/scanner.hpp" +#include "engine/scheduler.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace scheduler = engine::scheduler; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Map of test program identifiers (relative paths) to their identifiers in the +/// database. We need to keep this in memory because test programs can be +/// returned by the scanner in any order, and we only want to put each test +/// program once. +typedef std::map< fs::path, int64_t > path_to_id_map; + + +/// Map of in-flight PIDs to their corresponding test case IDs. +typedef std::map< int, int64_t > pid_to_id_map; + + +/// Pair of PID to a test case ID. +typedef pid_to_id_map::value_type pid_and_id_pair; + + +/// Puts a test program in the store and returns its identifier. +/// +/// This function is idempotent: we maintain a side cache of already-put test +/// programs so that we can return their identifiers without having to put them +/// again. +/// TODO(jmmv): It's possible that the store module should offer this +/// functionality and not have to do this ourselves here. +/// +/// \param test_program The test program being put. +/// \param [in,out] tx Writable transaction on the store. +/// \param [in,out] ids_cache Cache of already-put test programs. +/// +/// \return A test program identifier. +static int64_t +find_test_program_id(const model::test_program_ptr test_program, + store::write_transaction& tx, + path_to_id_map& ids_cache) +{ + const fs::path& key = test_program->relative_path(); + std::map< fs::path, int64_t >::const_iterator iter = ids_cache.find(key); + if (iter == ids_cache.end()) { + const int64_t id = tx.put_test_program(*test_program); + ids_cache.insert(std::make_pair(key, id)); + return id; + } else { + return (*iter).second; + } +} + + +/// Stores the result of an execution in the database. +/// +/// \param test_case_id Identifier of the test case in the database. +/// \param result The result of the execution. +/// \param [in,out] tx Writable transaction where to store the result data. +static void +put_test_result(const int64_t test_case_id, + const scheduler::test_result_handle& result, + store::write_transaction& tx) +{ + tx.put_result(result.test_result(), test_case_id, + result.start_time(), result.end_time()); + tx.put_test_case_file("__STDOUT__", result.stdout_file(), test_case_id); + tx.put_test_case_file("__STDERR__", result.stderr_file(), test_case_id); + +} + + +/// Cleans up a test case and folds any errors into the test result. +/// +/// \param handle The result handle for the test. +/// +/// \return The test result if the cleanup succeeds; a broken test result +/// otherwise. +model::test_result +safe_cleanup(scheduler::test_result_handle handle) throw() +{ + try { + handle.cleanup(); + return handle.test_result(); + } catch (const std::exception& e) { + return model::test_result( + model::test_result_broken, + F("Failed to clean up test case's work directory %s: %s") % + handle.work_directory() % e.what()); + } +} + + +/// Starts a test asynchronously. +/// +/// \param handle Scheduler handle. +/// \param match Test program and test case to start. +/// \param [in,out] tx Writable transaction to obtain test IDs. +/// \param [in,out] ids_cache Cache of already-put test cases. +/// \param user_config The end-user configuration properties. +/// \param hooks The hooks for this execution. +/// +/// \returns The PID for the started test and the test case's identifier in the +/// store. +pid_and_id_pair +start_test(scheduler::scheduler_handle& handle, + const engine::scan_result& match, + store::write_transaction& tx, + path_to_id_map& ids_cache, + const config::tree& user_config, + drivers::run_tests::base_hooks& hooks) +{ + const model::test_program_ptr test_program = match.first; + const std::string& test_case_name = match.second; + + hooks.got_test_case(*test_program, test_case_name); + + const int64_t test_program_id = find_test_program_id( + test_program, tx, ids_cache); + const int64_t test_case_id = tx.put_test_case( + *test_program, test_case_name, test_program_id); + + const scheduler::exec_handle exec_handle = handle.spawn_test( + test_program, test_case_name, user_config); + return std::make_pair(exec_handle, test_case_id); +} + + +/// Processes the completion of a test. +/// +/// \param [in,out] result_handle The completion handle of the test subprocess. +/// \param test_case_id Identifier of the test case as returned by start_test(). +/// \param [in,out] tx Writable transaction to put the test results. +/// \param hooks The hooks for this execution. +/// +/// \post result_handle is cleaned up. The caller cannot clean it up again. +void +finish_test(scheduler::result_handle_ptr result_handle, + const int64_t test_case_id, + store::write_transaction& tx, + drivers::run_tests::base_hooks& hooks) +{ + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + put_test_result(test_case_id, *test_result_handle, tx); + + const model::test_result test_result = safe_cleanup(*test_result_handle); + hooks.got_result( + *test_result_handle->test_program(), + test_result_handle->test_case_name(), + test_result_handle->test_result(), + result_handle->end_time() - result_handle->start_time()); +} + + +/// Extracts the keys of a pid_to_id_map and returns them as a string. +/// +/// \param map The PID to test ID map from which to get the PIDs. +/// +/// \return A user-facing string with the collection of PIDs. +static std::string +format_pids(const pid_to_id_map& map) +{ + std::set< pid_to_id_map::key_type > pids; + for (pid_to_id_map::const_iterator iter = map.begin(); iter != map.end(); + ++iter) { + pids.insert(iter->first); + } + return text::join(pids, ","); +} + + +} // anonymous namespace + + +/// Pure abstract destructor. +drivers::run_tests::base_hooks::~base_hooks(void) +{ +} + + +/// Executes the operation. +/// +/// \param kyuafile_path The path to the Kyuafile to be loaded. +/// \param build_root If not none, path to the built test programs. +/// \param store_path The path to the store to be used. +/// \param filters The test case filters as provided by the user. +/// \param user_config The end-user configuration properties. +/// \param hooks The hooks for this execution. +/// +/// \returns A structure with all results computed by this driver. +drivers::run_tests::result +drivers::run_tests::drive(const fs::path& kyuafile_path, + const optional< fs::path > build_root, + const fs::path& store_path, + const std::set< engine::test_filter >& filters, + const config::tree& user_config, + base_hooks& hooks) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + kyuafile_path, build_root, user_config, handle); + store::write_backend db = store::write_backend::open_rw(store_path); + store::write_transaction tx = db.start_write(); + + { + const model::context context = scheduler::current_context(); + (void)tx.put_context(context); + } + + engine::scanner scanner(kyuafile.test_programs(), filters); + + path_to_id_map ids_cache; + pid_to_id_map in_flight; + std::vector< engine::scan_result > exclusive_tests; + + const std::size_t slots = user_config.lookup< config::positive_int_node >( + "parallelism"); + INV(slots >= 1); + do { + INV(in_flight.size() <= slots); + + // Spawn as many jobs as needed to fill our execution slots. We do this + // first with the assumption that the spawning is faster than any single + // job, so we want to keep as many jobs in the background as possible. + while (in_flight.size() < slots) { + optional< engine::scan_result > match = scanner.yield(); + if (!match) + break; + const model::test_program_ptr test_program = match.get().first; + const std::string& test_case_name = match.get().second; + + const model::test_case& test_case = test_program->find( + test_case_name); + if (test_case.get_metadata().is_exclusive()) { + // Exclusive tests get processed later, separately. + exclusive_tests.push_back(match.get()); + continue; + } + + const pid_and_id_pair pid_id = start_test( + handle, match.get(), tx, ids_cache, user_config, hooks); + INV_MSG(in_flight.find(pid_id.first) == in_flight.end(), + F("Spawned test has PID of still-tracked process %s") % + pid_id.first); + in_flight.insert(pid_id); + } + + // If there are any used slots, consume any at random and return the + // result. We consume slots one at a time to give preference to the + // spawning of new tests as detailed above. + if (!in_flight.empty()) { + scheduler::result_handle_ptr result_handle = handle.wait_any(); + + const pid_to_id_map::iterator iter = in_flight.find( + result_handle->original_pid()); + INV_MSG(iter != in_flight.end(), + F("Lost track of in-flight PID %s; tracking %s") % + result_handle->original_pid() % format_pids(in_flight)); + const int64_t test_case_id = (*iter).second; + in_flight.erase(iter); + + finish_test(result_handle, test_case_id, tx, hooks); + } + } while (!in_flight.empty() || !scanner.done()); + + // Run any exclusive tests that we spotted earlier sequentially. + for (std::vector< engine::scan_result >::const_iterator + iter = exclusive_tests.begin(); iter != exclusive_tests.end(); + ++iter) { + const pid_and_id_pair data = start_test( + handle, *iter, tx, ids_cache, user_config, hooks); + scheduler::result_handle_ptr result_handle = handle.wait_any(); + finish_test(result_handle, data.second, tx, hooks); + } + + tx.commit(); + + handle.cleanup(); + + return result(scanner.unused_filters()); +} diff --git a/drivers/run_tests.hpp b/drivers/run_tests.hpp new file mode 100644 index 000000000000..7f09953d4e03 --- /dev/null +++ b/drivers/run_tests.hpp @@ -0,0 +1,106 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file drivers/run_tests.hpp +/// Driver to run a collection of tests. +/// +/// This driver module implements the logic to execute a collection of tests. +/// The presentation layer is able to monitor progress by hooking into +/// particular points of the driver. + +#if !defined(DRIVERS_RUN_TESTS_HPP) +#define DRIVERS_RUN_TESTS_HPP + +#include +#include + +#include "engine/filters.hpp" +#include "model/test_program.hpp" +#include "model/test_result_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace drivers { +namespace run_tests { + + +/// Abstract definition of the hooks for this driver. +class base_hooks { +public: + virtual ~base_hooks(void) = 0; + + /// Called when the processing of a test case begins. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the test case being executed. + virtual void got_test_case(const model::test_program& test_program, + const std::string& test_case_name) = 0; + + /// Called when a result of a test case becomes available. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the executed test case. + /// \param result The result of the execution of the test case. + /// \param duration The time it took to run the test. + virtual void got_result(const model::test_program& test_program, + const std::string& test_case_name, + const model::test_result& result, + const utils::datetime::delta& duration) = 0; +}; + + +/// Tuple containing the results of this driver. +class result { +public: + /// Filters that did not match any available test case. + /// + /// The presence of any filters here probably indicates a usage error. If a + /// test filter does not match any test case, it is probably a typo. + std::set< engine::test_filter > unused_filters; + + /// Initializer for the tuple's fields. + /// + /// \param unused_filters_ The filters that did not match any test case. + result(const std::set< engine::test_filter >& unused_filters_) : + unused_filters(unused_filters_) + { + } +}; + + +result drive(const utils::fs::path&, const utils::optional< utils::fs::path >, + const utils::fs::path&, const std::set< engine::test_filter >&, + const utils::config::tree&, base_hooks&); + + +} // namespace run_tests +} // namespace drivers + +#endif // !defined(DRIVERS_RUN_TESTS_HPP) diff --git a/drivers/scan_results.cpp b/drivers/scan_results.cpp new file mode 100644 index 000000000000..e013cd1fb314 --- /dev/null +++ b/drivers/scan_results.cpp @@ -0,0 +1,107 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "drivers/scan_results.hpp" + +#include "engine/filters.hpp" +#include "model/context.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "store/read_backend.hpp" +#include "store/read_transaction.hpp" +#include "utils/defs.hpp" + +namespace fs = utils::fs; + + +/// Pure abstract destructor. +drivers::scan_results::base_hooks::~base_hooks(void) +{ +} + + +/// Callback executed before any operation is performed. +void +drivers::scan_results::base_hooks::begin(void) +{ +} + + +/// Callback executed after all operations are performed. +void +drivers::scan_results::base_hooks::end(const result& /* r */) +{ +} + + +/// Executes the operation. +/// +/// \param store_path The path to the database store. +/// \param raw_filters The test case filters as provided by the user. +/// \param hooks The hooks for this execution. +/// +/// \returns A structure with all results computed by this driver. +drivers::scan_results::result +drivers::scan_results::drive(const fs::path& store_path, + const std::set< engine::test_filter >& raw_filters, + base_hooks& hooks) +{ + engine::filters_state filters(raw_filters); + + store::read_backend db = store::read_backend::open_ro(store_path); + store::read_transaction tx = db.start_read(); + + hooks.begin(); + + const model::context context = tx.get_context(); + hooks.got_context(context); + + store::results_iterator iter = tx.get_results(); + while (iter) { + // TODO(jmmv): We should be filtering at the test case level for + // efficiency, but that means we would need to execute more than one + // query on the database and our current interfaces don't support that. + // + // Reuse engine::filters_state for the time being because it is simpler + // and we get tracking of unmatched filters "for free". + const model::test_program_ptr test_program = iter.test_program(); + if (filters.match_test_program(test_program->relative_path())) { + const model::test_case& test_case = test_program->find( + iter.test_case_name()); + if (filters.match_test_case(test_program->relative_path(), + test_case.name())) { + hooks.got_result(iter); + } + } + ++iter; + } + + result r(filters.unused()); + hooks.end(r); + return r; +} diff --git a/drivers/scan_results.hpp b/drivers/scan_results.hpp new file mode 100644 index 000000000000..ddf099ae3565 --- /dev/null +++ b/drivers/scan_results.hpp @@ -0,0 +1,105 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file drivers/scan_results.hpp +/// Driver to scan the contents of a results file. +/// +/// This driver module implements the logic to scan the contents of a results +/// file and to notify the presentation layer as soon as data becomes +/// available. This is to prevent reading all the data from the file at once, +/// which could take too much memory. + +#if !defined(DRIVERS_SCAN_RESULTS_HPP) +#define DRIVERS_SCAN_RESULTS_HPP + +extern "C" { +#include +} + +#include + +#include "engine/filters.hpp" +#include "model/context_fwd.hpp" +#include "store/read_transaction_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace drivers { +namespace scan_results { + + +/// Tuple containing the results of this driver. +class result { +public: + /// Filters that did not match any available test case. + /// + /// The presence of any filters here probably indicates a usage error. If a + /// test filter does not match any test case, it is probably a typo. + std::set< engine::test_filter > unused_filters; + + /// Initializer for the tuple's fields. + /// + /// \param unused_filters_ The filters that did not match any test case. + result(const std::set< engine::test_filter >& unused_filters_) : + unused_filters(unused_filters_) + { + } +}; + + +/// Abstract definition of the hooks for this driver. +class base_hooks { +public: + virtual ~base_hooks(void) = 0; + + virtual void begin(void); + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + virtual void got_context(const model::context& context) = 0; + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. Some of the data are + /// lazily fetched, hence why we receive the object instead of the + /// individual elements. + virtual void got_result(store::results_iterator& iter) = 0; + + virtual void end(const result& r); +}; + + +result drive(const utils::fs::path&, const std::set< engine::test_filter >&, + base_hooks&); + + +} // namespace scan_results +} // namespace drivers + +#endif // !defined(DRIVERS_SCAN_RESULTS_HPP) diff --git a/drivers/scan_results_test.cpp b/drivers/scan_results_test.cpp new file mode 100644 index 000000000000..5519cb670d85 --- /dev/null +++ b/drivers/scan_results_test.cpp @@ -0,0 +1,258 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "drivers/scan_results.hpp" + +#include + +#include + +#include "engine/filters.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "store/read_transaction.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Records the callback values for futher investigation. +class capture_hooks : public drivers::scan_results::base_hooks { +public: + /// Whether begin() was called or not. + bool _begin_called; + + /// The captured driver result, if any. + optional< drivers::scan_results::result > _end_result; + + /// The captured context, if any. + optional< model::context > _context; + + /// The captured results, flattened as "program:test_case:result". + std::set< std::string > _results; + + /// Constructor. + capture_hooks(void) : + _begin_called(false) + { + } + + /// Callback executed before any operation is performed. + void + begin(void) + { + _begin_called = true; + } + + /// Callback executed after all operations are performed. + /// + /// \param r A structure with all results computed by this driver. Note + /// that this is also returned by the drive operation. + void + end(const drivers::scan_results::result& r) + { + PRE(!_end_result); + _end_result = r; + } + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + void got_context(const model::context& context) + { + PRE(!_context); + _context = context; + } + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. + void got_result(store::results_iterator& iter) + { + const char* type; + switch (iter.result().type()) { + case model::test_result_passed: type = "passed"; break; + case model::test_result_skipped: type = "skipped"; break; + default: + UNREACHABLE_MSG("Formatting unimplemented"); + } + const datetime::delta duration = iter.end_time() - iter.start_time(); + _results.insert(F("%s:%s:%s:%s:%s:%s") % + iter.test_program()->absolute_path() % + iter.test_case_name() % type % iter.result().reason() % + duration.seconds % duration.useconds); + } +}; + + +/// Populates a results file. +/// +/// It is not OK to call this function multiple times on the same file. +/// +/// \param db_name The database to update. +/// \param count The number of "elements" to insert into the results file. +/// Determines the number of test programs and the number of test cases +/// each has. Can be used to determine from the caller which particular +/// results file has been loaded. +static void +populate_results_file(const char* db_name, const int count) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path(db_name)); + + store::write_transaction tx = backend.start_write(); + + std::map< std::string, std::string > env; + for (int i = 0; i < count; i++) + env[F("VAR%s") % i] = F("Value %s") % i; + const model::context context(fs::path("/root"), env); + tx.put_context(context); + + for (int i = 0; i < count; i++) { + model::test_program_builder test_program_builder( + "fake", fs::path(F("dir/prog_%s") % i), fs::path("/root"), + F("suite_%s") % i); + for (int j = 0; j < count; j++) { + test_program_builder.add_test_case(F("case_%s") % j); + } + const model::test_program test_program = test_program_builder.build(); + const int64_t tp_id = tx.put_test_program(test_program); + + for (int j = 0; j < count; j++) { + const model::test_result result(model::test_result_skipped, + F("Count %s") % j); + const int64_t tc_id = tx.put_test_case(test_program, + F("case_%s") % j, tp_id); + const datetime::timestamp start = + datetime::timestamp::from_microseconds(1000010); + const datetime::timestamp end = + datetime::timestamp::from_microseconds(5000020 + i + j); + tx.put_result(result, tc_id, start, end); + } + } + + tx.commit(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(ok__all); +ATF_TEST_CASE_BODY(ok__all) +{ + populate_results_file("test.db", 2); + + capture_hooks hooks; + const drivers::scan_results::result result = drivers::scan_results::drive( + fs::path("test.db"), std::set< engine::test_filter >(), hooks); + ATF_REQUIRE(result.unused_filters.empty()); + ATF_REQUIRE(hooks._begin_called); + ATF_REQUIRE(hooks._end_result); + + std::map< std::string, std::string > env; + env["VAR0"] = "Value 0"; + env["VAR1"] = "Value 1"; + const model::context context(fs::path("/root"), env); + ATF_REQUIRE(context == hooks._context.get()); + + std::set< std::string > results; + results.insert("/root/dir/prog_0:case_0:skipped:Count 0:4:10"); + results.insert("/root/dir/prog_0:case_1:skipped:Count 1:4:11"); + results.insert("/root/dir/prog_1:case_0:skipped:Count 0:4:11"); + results.insert("/root/dir/prog_1:case_1:skipped:Count 1:4:12"); + ATF_REQUIRE_EQ(results, hooks._results); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ok__filters); +ATF_TEST_CASE_BODY(ok__filters) +{ + populate_results_file("test.db", 3); + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("dir/prog_1"), "")); + filters.insert(engine::test_filter(fs::path("dir/prog_1"), "no")); + filters.insert(engine::test_filter(fs::path("dir/prog_2"), "case_1")); + filters.insert(engine::test_filter(fs::path("dir/prog_3"), "")); + + capture_hooks hooks; + const drivers::scan_results::result result = drivers::scan_results::drive( + fs::path("test.db"), filters, hooks); + ATF_REQUIRE(hooks._begin_called); + ATF_REQUIRE(hooks._end_result); + + std::set< engine::test_filter > unused_filters; + unused_filters.insert(engine::test_filter(fs::path("dir/prog_1"), "no")); + unused_filters.insert(engine::test_filter(fs::path("dir/prog_3"), "")); + ATF_REQUIRE_EQ(unused_filters, result.unused_filters); + + std::set< std::string > results; + results.insert("/root/dir/prog_1:case_0:skipped:Count 0:4:11"); + results.insert("/root/dir/prog_1:case_1:skipped:Count 1:4:12"); + results.insert("/root/dir/prog_1:case_2:skipped:Count 2:4:13"); + results.insert("/root/dir/prog_2:case_1:skipped:Count 1:4:13"); + ATF_REQUIRE_EQ(results, hooks._results); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_db); +ATF_TEST_CASE_BODY(missing_db) +{ + capture_hooks hooks; + ATF_REQUIRE_THROW( + store::error, + drivers::scan_results::drive(fs::path("test.db"), + std::set< engine::test_filter >(), + hooks)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ok__all); + ATF_ADD_TEST_CASE(tcs, ok__filters); + ATF_ADD_TEST_CASE(tcs, missing_db); +} diff --git a/engine/Kyuafile b/engine/Kyuafile new file mode 100644 index 000000000000..1baa63bc9118 --- /dev/null +++ b/engine/Kyuafile @@ -0,0 +1,17 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="atf_test"} +atf_test_program{name="atf_list_test"} +atf_test_program{name="atf_result_test"} +atf_test_program{name="config_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="filters_test"} +atf_test_program{name="kyuafile_test"} +atf_test_program{name="plain_test"} +atf_test_program{name="requirements_test"} +atf_test_program{name="scanner_test"} +atf_test_program{name="tap_test"} +atf_test_program{name="tap_parser_test"} +atf_test_program{name="scheduler_test"} diff --git a/engine/Makefile.am.inc b/engine/Makefile.am.inc new file mode 100644 index 000000000000..baa7fe0bb8a0 --- /dev/null +++ b/engine/Makefile.am.inc @@ -0,0 +1,155 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +ENGINE_CFLAGS = $(STORE_CFLAGS) $(MODEL_CFLAGS) $(UTILS_CFLAGS) +ENGINE_LIBS = libengine.a $(STORE_LIBS) $(MODEL_LIBS) $(UTILS_LIBS) + +noinst_LIBRARIES += libengine.a +libengine_a_CPPFLAGS = $(STORE_CFLAGS) $(UTILS_CFLAGS) +libengine_a_SOURCES = engine/atf.cpp +libengine_a_SOURCES += engine/atf.hpp +libengine_a_SOURCES += engine/atf_list.cpp +libengine_a_SOURCES += engine/atf_list.hpp +libengine_a_SOURCES += engine/atf_result.cpp +libengine_a_SOURCES += engine/atf_result.hpp +libengine_a_SOURCES += engine/atf_result_fwd.hpp +libengine_a_SOURCES += engine/config.cpp +libengine_a_SOURCES += engine/config.hpp +libengine_a_SOURCES += engine/config_fwd.hpp +libengine_a_SOURCES += engine/exceptions.cpp +libengine_a_SOURCES += engine/exceptions.hpp +libengine_a_SOURCES += engine/filters.cpp +libengine_a_SOURCES += engine/filters.hpp +libengine_a_SOURCES += engine/filters_fwd.hpp +libengine_a_SOURCES += engine/kyuafile.cpp +libengine_a_SOURCES += engine/kyuafile.hpp +libengine_a_SOURCES += engine/kyuafile_fwd.hpp +libengine_a_SOURCES += engine/plain.cpp +libengine_a_SOURCES += engine/plain.hpp +libengine_a_SOURCES += engine/requirements.cpp +libengine_a_SOURCES += engine/requirements.hpp +libengine_a_SOURCES += engine/scanner.cpp +libengine_a_SOURCES += engine/scanner.hpp +libengine_a_SOURCES += engine/scanner_fwd.hpp +libengine_a_SOURCES += engine/tap.cpp +libengine_a_SOURCES += engine/tap.hpp +libengine_a_SOURCES += engine/tap_parser.cpp +libengine_a_SOURCES += engine/tap_parser.hpp +libengine_a_SOURCES += engine/tap_parser_fwd.hpp +libengine_a_SOURCES += engine/scheduler.cpp +libengine_a_SOURCES += engine/scheduler.hpp +libengine_a_SOURCES += engine/scheduler_fwd.hpp + +if WITH_ATF +tests_enginedir = $(pkgtestsdir)/engine + +tests_engine_DATA = engine/Kyuafile +EXTRA_DIST += $(tests_engine_DATA) + +tests_engine_PROGRAMS = engine/atf_helpers +engine_atf_helpers_SOURCES = engine/atf_helpers.cpp +engine_atf_helpers_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +engine_atf_helpers_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/atf_test +engine_atf_test_SOURCES = engine/atf_test.cpp +engine_atf_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_atf_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/atf_list_test +engine_atf_list_test_SOURCES = engine/atf_list_test.cpp +engine_atf_list_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_atf_list_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/atf_result_test +engine_atf_result_test_SOURCES = engine/atf_result_test.cpp +engine_atf_result_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_atf_result_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/config_test +engine_config_test_SOURCES = engine/config_test.cpp +engine_config_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_config_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/exceptions_test +engine_exceptions_test_SOURCES = engine/exceptions_test.cpp +engine_exceptions_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_exceptions_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/filters_test +engine_filters_test_SOURCES = engine/filters_test.cpp +engine_filters_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_filters_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/kyuafile_test +engine_kyuafile_test_SOURCES = engine/kyuafile_test.cpp +engine_kyuafile_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_kyuafile_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/plain_helpers +engine_plain_helpers_SOURCES = engine/plain_helpers.cpp +engine_plain_helpers_CXXFLAGS = $(UTILS_CFLAGS) +engine_plain_helpers_LDADD = $(UTILS_LIBS) + +tests_engine_PROGRAMS += engine/plain_test +engine_plain_test_SOURCES = engine/plain_test.cpp +engine_plain_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_plain_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/requirements_test +engine_requirements_test_SOURCES = engine/requirements_test.cpp +engine_requirements_test_CXXFLAGS = $(ENGINE_CFLAGS) $(UTILS_TEST_CFLAGS) \ + $(ATF_CXX_CFLAGS) +engine_requirements_test_LDADD = $(ENGINE_LIBS) $(UTILS_TEST_LIBS) \ + $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/scanner_test +engine_scanner_test_SOURCES = engine/scanner_test.cpp +engine_scanner_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_scanner_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/tap_helpers +engine_tap_helpers_SOURCES = engine/tap_helpers.cpp +engine_tap_helpers_CXXFLAGS = $(UTILS_CFLAGS) +engine_tap_helpers_LDADD = $(UTILS_LIBS) + +tests_engine_PROGRAMS += engine/tap_test +engine_tap_test_SOURCES = engine/tap_test.cpp +engine_tap_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_tap_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/tap_parser_test +engine_tap_parser_test_SOURCES = engine/tap_parser_test.cpp +engine_tap_parser_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_tap_parser_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_engine_PROGRAMS += engine/scheduler_test +engine_scheduler_test_SOURCES = engine/scheduler_test.cpp +engine_scheduler_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +engine_scheduler_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/engine/atf.cpp b/engine/atf.cpp new file mode 100644 index 000000000000..eb63be20b0e7 --- /dev/null +++ b/engine/atf.cpp @@ -0,0 +1,242 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/atf.hpp" + +extern "C" { +#include +} + +#include +#include +#include + +#include "engine/atf_list.hpp" +#include "engine/atf_result.hpp" +#include "engine/exceptions.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/process/exceptions.hpp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/stream.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::optional; + + +namespace { + + +/// Basename of the file containing the result written by the ATF test case. +static const char* result_name = "result.atf"; + + +/// Magic numbers returned by exec_list when exec(2) fails. +enum list_exit_code { + exit_eacces = 90, + exit_enoent, + exit_enoexec, +}; + + +} // anonymous namespace + + +/// Executes a test program's list operation. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param vars User-provided variables to pass to the test program. +void +engine::atf_interface::exec_list(const model::test_program& test_program, + const config::properties_map& vars) const +{ + utils::setenv("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value"); + + process::args_vector args; + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + args.push_back(F("-v%s=%s") % (*iter).first % (*iter).second); + } + + args.push_back("-l"); + try { + process::exec_unsafe(test_program.absolute_path(), args); + } catch (const process::system_error& e) { + if (e.original_errno() == EACCES) + ::_exit(exit_eacces); + else if (e.original_errno() == ENOENT) + ::_exit(exit_enoent); + else if (e.original_errno() == ENOEXEC) + ::_exit(exit_enoexec); + throw; + } +} + + +/// Computes the test cases list of a test program. +/// +/// \param status The termination status of the subprocess used to execute +/// the exec_test() method or none if the test timed out. +/// \param stdout_path Path to the file containing the stdout of the test. +/// \param stderr_path Path to the file containing the stderr of the test. +/// +/// \return A list of test cases. +/// +/// \throw error If there is a problem parsing the test case list. +model::test_cases_map +engine::atf_interface::parse_list(const optional< process::status >& status, + const fs::path& stdout_path, + const fs::path& stderr_path) const +{ + const std::string stderr_contents = utils::read_file(stderr_path); + if (!stderr_contents.empty()) + LW("Test case list wrote to stderr: " + stderr_contents); + + if (!status) + throw engine::error("Test case list timed out"); + if (status.get().exited()) { + const int exitstatus = status.get().exitstatus(); + if (exitstatus == EXIT_SUCCESS) { + // Nothing to do; fall through. + } else if (exitstatus == exit_eacces) { + throw engine::error("Permission denied to run test program"); + } else if (exitstatus == exit_enoent) { + throw engine::error("Cannot find test program"); + } else if (exitstatus == exit_enoexec) { + throw engine::error("Invalid test program format"); + } else { + throw engine::error("Test program did not exit cleanly"); + } + } else { + throw engine::error("Test program received signal"); + } + + std::ifstream input(stdout_path.c_str()); + if (!input) + throw engine::load_error(stdout_path, "Cannot open file for read"); + const model::test_cases_map test_cases = parse_atf_list(input); + + if (!stderr_contents.empty()) + throw engine::error("Test case list wrote to stderr"); + + return test_cases; +} + + +/// Executes a test case of the test program. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param test_case_name Name of the test case to invoke. +/// \param vars User-provided variables to pass to the test program. +/// \param control_directory Directory where the interface may place control +/// files. +void +engine::atf_interface::exec_test(const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& control_directory) const +{ + utils::setenv("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value"); + + process::args_vector args; + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + args.push_back(F("-v%s=%s") % (*iter).first % (*iter).second); + } + + args.push_back(F("-r%s") % (control_directory / result_name)); + args.push_back(test_case_name); + process::exec(test_program.absolute_path(), args); +} + + +/// Executes a test cleanup routine of the test program. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param test_case_name Name of the test case to invoke. +/// \param vars User-provided variables to pass to the test program. +void +engine::atf_interface::exec_cleanup( + const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& /* control_directory */) const +{ + utils::setenv("__RUNNING_INSIDE_ATF_RUN", "internal-yes-value"); + + process::args_vector args; + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + args.push_back(F("-v%s=%s") % (*iter).first % (*iter).second); + } + + args.push_back(F("%s:cleanup") % test_case_name); + process::exec(test_program.absolute_path(), args); +} + + +/// Computes the result of a test case based on its termination status. +/// +/// \param status The termination status of the subprocess used to execute +/// the exec_test() method or none if the test timed out. +/// \param control_directory Directory where the interface may have placed +/// control files. +/// +/// \return A test result. +model::test_result +engine::atf_interface::compute_result( + const optional< process::status >& status, + const fs::path& control_directory, + const fs::path& /* stdout_path */, + const fs::path& /* stderr_path */) const +{ + return calculate_atf_result(status, control_directory / result_name); +} diff --git a/engine/atf.hpp b/engine/atf.hpp new file mode 100644 index 000000000000..34ddc2413235 --- /dev/null +++ b/engine/atf.hpp @@ -0,0 +1,72 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/atf.hpp +/// Execution engine for test programs that implement the atf interface. + +#if !defined(ENGINE_ATF_HPP) +#define ENGINE_ATF_HPP + +#include "engine/scheduler.hpp" + +namespace engine { + + +/// Implementation of the scheduler interface for atf test programs. +class atf_interface : public engine::scheduler::interface { +public: + void exec_list(const model::test_program&, + const utils::config::properties_map&) const UTILS_NORETURN; + + model::test_cases_map parse_list( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&) const; + + void exec_test(const model::test_program&, const std::string&, + const utils::config::properties_map&, + const utils::fs::path&) const + UTILS_NORETURN; + + void exec_cleanup(const model::test_program&, const std::string&, + const utils::config::properties_map&, + const utils::fs::path&) const + UTILS_NORETURN; + + model::test_result compute_result( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&, + const utils::fs::path&) const; +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_ATF_HPP) diff --git a/engine/atf_helpers.cpp b/engine/atf_helpers.cpp new file mode 100644 index 000000000000..c45654f10e58 --- /dev/null +++ b/engine/atf_helpers.cpp @@ -0,0 +1,414 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +extern "C" { +#include + +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/test_utils.ipp" +#include "utils/text/operations.ipp" + +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace text = utils::text; + +using utils::optional; + + +namespace { + + +/// Creates an empty file in the given directory. +/// +/// \param test_case The test case currently running. +/// \param directory The name of the configuration variable that holds the path +/// to the directory in which to create the cookie file. +/// \param name The name of the cookie file to create. +static void +create_cookie(const atf::tests::tc* test_case, const char* directory, + const char* name) +{ + if (!test_case->has_config_var(directory)) + test_case->fail(std::string(name) + " not provided"); + + const fs::path control_dir(test_case->get_config_var(directory)); + std::ofstream file((control_dir / name).c_str()); + if (!file) + test_case->fail("Failed to create the control cookie"); + file.close(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITH_CLEANUP(check_cleanup_workdir); +ATF_TEST_CASE_HEAD(check_cleanup_workdir) +{ + set_md_var("require.config", "control_dir"); +} +ATF_TEST_CASE_BODY(check_cleanup_workdir) +{ + std::ofstream cookie("workdir_cookie"); + cookie << "1234\n"; + cookie.close(); + skip("cookie created"); +} +ATF_TEST_CASE_CLEANUP(check_cleanup_workdir) +{ + const fs::path control_dir(get_config_var("control_dir")); + + std::ifstream cookie("workdir_cookie"); + if (!cookie) { + std::ofstream((control_dir / "missing_cookie").c_str()).close(); + std::exit(EXIT_FAILURE); + } + + std::string value; + cookie >> value; + if (value != "1234") { + std::ofstream((control_dir / "invalid_cookie").c_str()).close(); + std::exit(EXIT_FAILURE); + } + + std::ofstream((control_dir / "cookie_ok").c_str()).close(); + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_configuration_variables); +ATF_TEST_CASE_BODY(check_configuration_variables) +{ + ATF_REQUIRE(has_config_var("first")); + ATF_REQUIRE_EQ("some value", get_config_var("first")); + + ATF_REQUIRE(has_config_var("second")); + ATF_REQUIRE_EQ("some other value", get_config_var("second")); +} + + +ATF_TEST_CASE(check_list_config); +ATF_TEST_CASE_HEAD(check_list_config) +{ + std::string description = "Found:"; + + if (has_config_var("var1")) + description += " var1=" + get_config_var("var1"); + if (has_config_var("var2")) + description += " var2=" + get_config_var("var2"); + + set_md_var("descr", description); +} +ATF_TEST_CASE_BODY(check_list_config) +{ +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_unprivileged); +ATF_TEST_CASE_BODY(check_unprivileged) +{ + if (::getuid() == 0) + fail("Running as root, but I shouldn't be"); + + std::ofstream file("cookie"); + if (!file) + fail("Failed to create the cookie; work directory probably owned by " + "root"); + file.close(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(crash); +ATF_TEST_CASE_BODY(crash) +{ + std::abort(); +} + + +ATF_TEST_CASE(crash_head); +ATF_TEST_CASE_HEAD(crash_head) +{ + utils::abort_without_coredump(); +} +ATF_TEST_CASE_BODY(crash_head) +{ +} + + +ATF_TEST_CASE_WITH_CLEANUP(crash_cleanup); +ATF_TEST_CASE_HEAD(crash_cleanup) +{ +} +ATF_TEST_CASE_BODY(crash_cleanup) +{ +} +ATF_TEST_CASE_CLEANUP(crash_cleanup) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(create_cookie_in_control_dir); +ATF_TEST_CASE_BODY(create_cookie_in_control_dir) +{ + create_cookie(this, "control_dir", "cookie"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(create_cookie_in_workdir); +ATF_TEST_CASE_BODY(create_cookie_in_workdir) +{ + std::ofstream file("cookie"); + if (!file) + fail("Failed to create the cookie"); + file.close(); +} + + +ATF_TEST_CASE_WITH_CLEANUP(create_cookie_from_cleanup); +ATF_TEST_CASE_HEAD(create_cookie_from_cleanup) +{ +} +ATF_TEST_CASE_BODY(create_cookie_from_cleanup) +{ +} +ATF_TEST_CASE_CLEANUP(create_cookie_from_cleanup) +{ + create_cookie(this, "control_dir", "cookie"); +} + + +ATF_TEST_CASE_WITH_CLEANUP(expect_timeout); +ATF_TEST_CASE_HEAD(expect_timeout) +{ + if (has_config_var("timeout")) + set_md_var("timeout", get_config_var("timeout")); +} +ATF_TEST_CASE_BODY(expect_timeout) +{ + expect_timeout("Times out on purpose"); + ::sleep(10); + create_cookie(this, "control_dir", "cookie"); +} +ATF_TEST_CASE_CLEANUP(expect_timeout) +{ + create_cookie(this, "control_dir", "cookie.cleanup"); +} + + +ATF_TEST_CASE_WITH_CLEANUP(output); +ATF_TEST_CASE_HEAD(output) +{ +} +ATF_TEST_CASE_BODY(output) +{ + std::cout << "Body message to stdout\n"; + std::cerr << "Body message to stderr\n"; +} +ATF_TEST_CASE_CLEANUP(output) +{ + std::cout << "Cleanup message to stdout\n"; + std::cerr << "Cleanup message to stderr\n"; +} + + +ATF_TEST_CASE(output_in_list); +ATF_TEST_CASE_HEAD(output_in_list) +{ + std::cerr << "Should not write anything!\n"; +} +ATF_TEST_CASE_BODY(output_in_list) +{ +} + + +ATF_TEST_CASE(pass); +ATF_TEST_CASE_HEAD(pass) +{ + set_md_var("descr", "Always-passing test case"); +} +ATF_TEST_CASE_BODY(pass) +{ +} + + +ATF_TEST_CASE_WITH_CLEANUP(shared_workdir); +ATF_TEST_CASE_HEAD(shared_workdir) +{ +} +ATF_TEST_CASE_BODY(shared_workdir) +{ + atf::utils::create_file("shared_cookie", ""); +} +ATF_TEST_CASE_CLEANUP(shared_workdir) +{ + if (!atf::utils::file_exists("shared_cookie")) + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE(spawn_blocking_child); +ATF_TEST_CASE_HEAD(spawn_blocking_child) +{ + set_md_var("require.config", "control_dir"); +} +ATF_TEST_CASE_BODY(spawn_blocking_child) +{ + pid_t pid = ::fork(); + if (pid == -1) + fail("Cannot fork subprocess"); + else if (pid == 0) { + for (;;) + ::pause(); + } else { + const fs::path name = fs::path(get_config_var("control_dir")) / "pid"; + std::ofstream pidfile(name.c_str()); + ATF_REQUIRE(pidfile); + pidfile << pid; + pidfile.close(); + } +} + + +ATF_TEST_CASE_WITH_CLEANUP(timeout_body); +ATF_TEST_CASE_HEAD(timeout_body) +{ + if (has_config_var("timeout")) + set_md_var("timeout", get_config_var("timeout")); +} +ATF_TEST_CASE_BODY(timeout_body) +{ + ::sleep(10); + create_cookie(this, "control_dir", "cookie"); +} +ATF_TEST_CASE_CLEANUP(timeout_body) +{ + create_cookie(this, "control_dir", "cookie.cleanup"); +} + + +ATF_TEST_CASE_WITH_CLEANUP(timeout_cleanup); +ATF_TEST_CASE_HEAD(timeout_cleanup) +{ +} +ATF_TEST_CASE_BODY(timeout_cleanup) +{ +} +ATF_TEST_CASE_CLEANUP(timeout_cleanup) +{ + ::sleep(10); + create_cookie(this, "control_dir", "cookie"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(validate_isolation); +ATF_TEST_CASE_BODY(validate_isolation) +{ + ATF_REQUIRE(utils::getenv("HOME").get() != "fake-value"); + ATF_REQUIRE(!utils::getenv("LANG")); +} + + +/// Wrapper around ATF_ADD_TEST_CASE to only add a test when requested. +/// +/// The caller can set the TEST_CASES environment variable to a +/// whitespace-separated list of test case names to enable. If not empty, the +/// list acts as a filter for the tests to add. +/// +/// \param tcs List of test cases into which to register the test. +/// \param filters List of filters to determine whether the test applies or not. +/// \param name Name of the test case being added. +#define ADD_TEST_CASE(tcs, filters, name) \ + do { \ + if (filters.empty() || filters.find(#name) != filters.end()) \ + ATF_ADD_TEST_CASE(tcs, name); \ + } while (false) + + +ATF_INIT_TEST_CASES(tcs) +{ + logging::set_inmemory(); + + // TODO(jmmv): Instead of using "filters", we should make TEST_CASES + // explicitly list all the test cases to enable. This would let us get rid + // of some of the hacks below... + std::set< std::string > filters; + + const optional< std::string > names_raw = utils::getenv("TEST_CASES"); + if (names_raw) { + if (names_raw.get().empty()) + return; // See TODO above. + + const std::vector< std::string > names = text::split( + names_raw.get(), ' '); + std::copy(names.begin(), names.end(), + std::inserter(filters, filters.begin())); + } + + if (filters.find("crash_head") != filters.end()) // See TODO above. + ATF_ADD_TEST_CASE(tcs, crash_head); + if (filters.find("output_in_list") != filters.end()) // See TODO above. + ATF_ADD_TEST_CASE(tcs, output_in_list); + + ADD_TEST_CASE(tcs, filters, check_cleanup_workdir); + ADD_TEST_CASE(tcs, filters, check_configuration_variables); + ADD_TEST_CASE(tcs, filters, check_list_config); + ADD_TEST_CASE(tcs, filters, check_unprivileged); + ADD_TEST_CASE(tcs, filters, crash); + ADD_TEST_CASE(tcs, filters, crash_cleanup); + ADD_TEST_CASE(tcs, filters, create_cookie_in_control_dir); + ADD_TEST_CASE(tcs, filters, create_cookie_in_workdir); + ADD_TEST_CASE(tcs, filters, create_cookie_from_cleanup); + ADD_TEST_CASE(tcs, filters, expect_timeout); + ADD_TEST_CASE(tcs, filters, output); + ADD_TEST_CASE(tcs, filters, pass); + ADD_TEST_CASE(tcs, filters, shared_workdir); + ADD_TEST_CASE(tcs, filters, spawn_blocking_child); + ADD_TEST_CASE(tcs, filters, timeout_body); + ADD_TEST_CASE(tcs, filters, timeout_cleanup); + ADD_TEST_CASE(tcs, filters, validate_isolation); +} diff --git a/engine/atf_list.cpp b/engine/atf_list.cpp new file mode 100644 index 000000000000..a16b889c74f0 --- /dev/null +++ b/engine/atf_list.cpp @@ -0,0 +1,196 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/atf_list.hpp" + +#include +#include +#include + +#include "engine/exceptions.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "utils/config/exceptions.hpp" +#include "utils/format/macros.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Splits a property line of the form "name: word1 [... wordN]". +/// +/// \param line The line to parse. +/// +/// \return A (property_name, property_value) pair. +/// +/// \throw format_error If the value of line is invalid. +static std::pair< std::string, std::string > +split_prop_line(const std::string& line) +{ + const std::string::size_type pos = line.find(": "); + if (pos == std::string::npos) + throw engine::format_error("Invalid property line; expecting line of " + "the form 'name: value'"); + return std::make_pair(line.substr(0, pos), line.substr(pos + 2)); +} + + +/// Parses a set of consecutive property lines. +/// +/// Processing stops when an empty line or the end of file is reached. None of +/// these conditions indicate errors. +/// +/// \param input The stream to read the lines from. +/// +/// \return The parsed property lines. +/// +/// throw format_error If the input stream has an invalid format. +static model::properties_map +parse_properties(std::istream& input) +{ + model::properties_map properties; + + std::string line; + while (std::getline(input, line).good() && !line.empty()) { + const std::pair< std::string, std::string > property = split_prop_line( + line); + if (properties.find(property.first) != properties.end()) + throw engine::format_error("Duplicate value for property " + + property.first); + properties.insert(property); + } + + return properties; +} + + +} // anonymous namespace + + +/// Parses the metadata of an ATF test case. +/// +/// \param props The properties (name/value string pairs) as provided by the +/// ATF test program. +/// +/// \return A parsed metadata object. +/// +/// \throw engine::format_error If the syntax of any of the properties is +/// invalid. +model::metadata +engine::parse_atf_metadata(const model::properties_map& props) +{ + model::metadata_builder mdbuilder; + + try { + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); iter++) { + const std::string& name = (*iter).first; + const std::string& value = (*iter).second; + + if (name == "descr") { + mdbuilder.set_string("description", value); + } else if (name == "has.cleanup") { + mdbuilder.set_string("has_cleanup", value); + } else if (name == "require.arch") { + mdbuilder.set_string("allowed_architectures", value); + } else if (name == "require.config") { + mdbuilder.set_string("required_configs", value); + } else if (name == "require.files") { + mdbuilder.set_string("required_files", value); + } else if (name == "require.machine") { + mdbuilder.set_string("allowed_platforms", value); + } else if (name == "require.memory") { + mdbuilder.set_string("required_memory", value); + } else if (name == "require.progs") { + mdbuilder.set_string("required_programs", value); + } else if (name == "require.user") { + mdbuilder.set_string("required_user", value); + } else if (name == "timeout") { + mdbuilder.set_string("timeout", value); + } else if (name.length() > 2 && name.substr(0, 2) == "X-") { + mdbuilder.add_custom(name.substr(2), value); + } else { + throw engine::format_error(F("Unknown test case metadata " + "property '%s'") % name); + } + } + } catch (const config::error& e) { + throw engine::format_error(e.what()); + } + + return mdbuilder.build(); +} + + +/// Parses the ATF list of test cases from an open stream. +/// +/// \param input The stream to read from. +/// +/// \return The collection of parsed test cases. +/// +/// \throw format_error If there is any problem in the input data. +model::test_cases_map +engine::parse_atf_list(std::istream& input) +{ + std::string line; + + std::getline(input, line); + if (line != "Content-Type: application/X-atf-tp; version=\"1\"" + || !input.good()) + throw format_error(F("Invalid header for test case list; expecting " + "Content-Type for application/X-atf-tp version 1, " + "got '%s'") % line); + + std::getline(input, line); + if (!line.empty() || !input.good()) + throw format_error(F("Invalid header for test case list; expecting " + "a blank line, got '%s'") % line); + + model::test_cases_map_builder test_cases_builder; + while (std::getline(input, line).good()) { + const std::pair< std::string, std::string > ident = split_prop_line( + line); + if (ident.first != "ident" or ident.second.empty()) + throw format_error("Invalid test case definition; must be " + "preceeded by the identifier"); + + const model::properties_map props = parse_properties(input); + test_cases_builder.add(ident.second, parse_atf_metadata(props)); + } + const model::test_cases_map test_cases = test_cases_builder.build(); + if (test_cases.empty()) { + // The scheduler interface also checks for the presence of at least one + // test case. However, because the atf format itself requires one test + // case to be always present, we check for this condition here as well. + throw format_error("No test cases"); + } + return test_cases; +} diff --git a/engine/atf_list.hpp b/engine/atf_list.hpp new file mode 100644 index 000000000000..3d81d03e3bcf --- /dev/null +++ b/engine/atf_list.hpp @@ -0,0 +1,51 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/atf_list.hpp +/// Parser of ATF test case lists. + +#if !defined(ENGINE_ATF_LIST_HPP) +#define ENGINE_ATF_LIST_HPP + +#include + +#include "model/metadata_fwd.hpp" +#include "model/test_case_fwd.hpp" +#include "model/types.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace engine { + + +model::metadata parse_atf_metadata(const model::properties_map&); +model::test_cases_map parse_atf_list(std::istream&); + + +} // namespace engine + +#endif // !defined(ENGINE_ATF_LIST_HPP) diff --git a/engine/atf_list_test.cpp b/engine/atf_list_test.cpp new file mode 100644 index 000000000000..7f19ca8fbec5 --- /dev/null +++ b/engine/atf_list_test.cpp @@ -0,0 +1,278 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/atf_list.hpp" + +#include +#include + +#include + +#include "engine/exceptions.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/types.hpp" +#include "utils/datetime.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_metadata__defaults) +ATF_TEST_CASE_BODY(parse_atf_metadata__defaults) +{ + const model::properties_map properties; + const model::metadata md = engine::parse_atf_metadata(properties); + + const model::metadata exp_md = model::metadata_builder().build(); + ATF_REQUIRE_EQ(exp_md, md); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_metadata__override_all) +ATF_TEST_CASE_BODY(parse_atf_metadata__override_all) +{ + model::properties_map properties; + properties["descr"] = "Some text"; + properties["has.cleanup"] = "true"; + properties["require.arch"] = "i386 x86_64"; + properties["require.config"] = "var1 var2 var3"; + properties["require.files"] = "/file1 /dir/file2"; + properties["require.machine"] = "amd64"; + properties["require.memory"] = "1m"; + properties["require.progs"] = "/bin/ls svn"; + properties["require.user"] = "root"; + properties["timeout"] = "123"; + properties["X-foo"] = "value1"; + properties["X-bar"] = "value2"; + properties["X-baz-www"] = "value3"; + const model::metadata md = engine::parse_atf_metadata(properties); + + const model::metadata exp_md = model::metadata_builder() + .add_allowed_architecture("i386") + .add_allowed_architecture("x86_64") + .add_allowed_platform("amd64") + .add_custom("foo", "value1") + .add_custom("bar", "value2") + .add_custom("baz-www", "value3") + .add_required_config("var1") + .add_required_config("var2") + .add_required_config("var3") + .add_required_file(fs::path("/file1")) + .add_required_file(fs::path("/dir/file2")) + .add_required_program(fs::path("/bin/ls")) + .add_required_program(fs::path("svn")) + .set_description("Some text") + .set_has_cleanup(true) + .set_required_memory(units::bytes::parse("1m")) + .set_required_user("root") + .set_timeout(datetime::delta(123, 0)) + .build(); + ATF_REQUIRE_EQ(exp_md, md); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_metadata__unknown) +ATF_TEST_CASE_BODY(parse_atf_metadata__unknown) +{ + model::properties_map properties; + properties["foobar"] = "Some text"; + + ATF_REQUIRE_THROW_RE(engine::format_error, "Unknown.*property.*'foobar'", + engine::parse_atf_metadata(properties)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__empty); +ATF_TEST_CASE_BODY(parse_atf_list__empty) +{ + const std::string text = ""; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "expecting Content-Type", + engine::parse_atf_list(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__invalid_header); +ATF_TEST_CASE_BODY(parse_atf_list__invalid_header) +{ + { + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "expecting.*blank line", + engine::parse_atf_list(input)); + } + + { + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\nfoo\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "expecting.*blank line", + engine::parse_atf_list(input)); + } + + { + const std::string text = + "Content-Type: application/X-atf-tp; version=\"2\"\n\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "expecting Content-Type", + engine::parse_atf_list(input)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__no_test_cases); +ATF_TEST_CASE_BODY(parse_atf_list__no_test_cases) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "No test cases", + engine::parse_atf_list(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__one_test_case_simple); +ATF_TEST_CASE_BODY(parse_atf_list__one_test_case_simple) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n" + "\n" + "ident: test-case\n"; + std::istringstream input(text); + const model::test_cases_map tests = engine::parse_atf_list(input); + + const model::test_cases_map exp_tests = model::test_cases_map_builder() + .add("test-case").build(); + ATF_REQUIRE_EQ(exp_tests, tests); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__one_test_case_complex); +ATF_TEST_CASE_BODY(parse_atf_list__one_test_case_complex) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n" + "\n" + "ident: first\n" + "descr: This is the description\n" + "timeout: 500\n"; + std::istringstream input(text); + const model::test_cases_map tests = engine::parse_atf_list(input); + + const model::test_cases_map exp_tests = model::test_cases_map_builder() + .add("first", model::metadata_builder() + .set_description("This is the description") + .set_timeout(datetime::delta(500, 0)) + .build()) + .build(); + ATF_REQUIRE_EQ(exp_tests, tests); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__one_test_case_invalid_syntax); +ATF_TEST_CASE_BODY(parse_atf_list__one_test_case_invalid_syntax) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n\n" + "descr: This is the description\n" + "ident: first\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "preceeded.*identifier", + engine::parse_atf_list(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__one_test_case_invalid_properties); +ATF_TEST_CASE_BODY(parse_atf_list__one_test_case_invalid_properties) +{ + // Inject a single invalid property that makes test_case::from_properties() + // raise a particular error message so that we can validate that such + // function was called. We do intensive testing separately, so it is not + // necessary to redo it here. + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n\n" + "ident: first\n" + "require.progs: bin/ls\n"; + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, "Relative path 'bin/ls'", + engine::parse_atf_list(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_atf_list__many_test_cases); +ATF_TEST_CASE_BODY(parse_atf_list__many_test_cases) +{ + const std::string text = + "Content-Type: application/X-atf-tp; version=\"1\"\n" + "\n" + "ident: first\n" + "descr: This is the description\n" + "\n" + "ident: second\n" + "timeout: 500\n" + "descr: Some text\n" + "\n" + "ident: third\n"; + std::istringstream input(text); + const model::test_cases_map tests = engine::parse_atf_list(input); + + const model::test_cases_map exp_tests = model::test_cases_map_builder() + .add("first", model::metadata_builder() + .set_description("This is the description") + .build()) + .add("second", model::metadata_builder() + .set_description("Some text") + .set_timeout(datetime::delta(500, 0)) + .build()) + .add("third") + .build(); + ATF_REQUIRE_EQ(exp_tests, tests); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, parse_atf_metadata__defaults); + ATF_ADD_TEST_CASE(tcs, parse_atf_metadata__override_all); + ATF_ADD_TEST_CASE(tcs, parse_atf_metadata__unknown); + + ATF_ADD_TEST_CASE(tcs, parse_atf_list__empty); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__invalid_header); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__no_test_cases); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__one_test_case_simple); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__one_test_case_complex); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__one_test_case_invalid_syntax); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__one_test_case_invalid_properties); + ATF_ADD_TEST_CASE(tcs, parse_atf_list__many_test_cases); +} diff --git a/engine/atf_result.cpp b/engine/atf_result.cpp new file mode 100644 index 000000000000..f99b28f9e96e --- /dev/null +++ b/engine/atf_result.cpp @@ -0,0 +1,642 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/atf_result.hpp" + +#include +#include +#include + +#include "engine/exceptions.hpp" +#include "model/test_result.hpp" +#include "utils/fs/path.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace fs = utils::fs; +namespace process = utils::process; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Reads a file and flattens its lines. +/// +/// The main purpose of this function is to simplify the parsing of a file +/// containing the result of a test. Therefore, the return value carries +/// several assumptions. +/// +/// \param input The stream to read from. +/// +/// \return A pair (line count, contents) detailing how many lines where read +/// and their contents. If the file contains a single line with no newline +/// character, the line count is 0. If the file includes more than one line, +/// the lines are merged together and separated by the magic string +/// '<<NEWLINE>>'. +static std::pair< size_t, std::string > +read_lines(std::istream& input) +{ + std::pair< size_t, std::string > ret = std::make_pair(0, ""); + + do { + std::string line; + std::getline(input, line); + if (input.eof() && !line.empty()) { + if (ret.first == 0) + ret.second = line; + else { + ret.second += "<>" + line; + ret.first++; + } + } else if (input.good()) { + if (ret.first == 0) + ret.second = line; + else + ret.second += "<>" + line; + ret.first++; + } + } while (input.good()); + + return ret; +} + + +/// Parses a test result that does not accept a reason. +/// +/// \param status The result status name. +/// \param rest The rest of the line after the status name. +/// +/// \return An object representing the test result. +/// +/// \throw format_error If the result is invalid (i.e. rest is invalid). +/// +/// \pre status must be "passed". +static engine::atf_result +parse_without_reason(const std::string& status, const std::string& rest) +{ + if (!rest.empty()) + throw engine::format_error(F("%s cannot have a reason") % status); + PRE(status == "passed"); + return engine::atf_result(engine::atf_result::passed); +} + + +/// Parses a test result that needs a reason. +/// +/// \param status The result status name. +/// \param rest The rest of the line after the status name. +/// +/// \return An object representing the test result. +/// +/// \throw format_error If the result is invalid (i.e. rest is invalid). +/// +/// \pre status must be one of "broken", "expected_death", "expected_failure", +/// "expected_timeout", "failed" or "skipped". +static engine::atf_result +parse_with_reason(const std::string& status, const std::string& rest) +{ + using engine::atf_result; + + if (rest.length() < 3 || rest.substr(0, 2) != ": ") + throw engine::format_error(F("%s must be followed by ': '") % + status); + const std::string reason = rest.substr(2); + INV(!reason.empty()); + + if (status == "broken") + return atf_result(atf_result::broken, reason); + else if (status == "expected_death") + return atf_result(atf_result::expected_death, reason); + else if (status == "expected_failure") + return atf_result(atf_result::expected_failure, reason); + else if (status == "expected_timeout") + return atf_result(atf_result::expected_timeout, reason); + else if (status == "failed") + return atf_result(atf_result::failed, reason); + else if (status == "skipped") + return atf_result(atf_result::skipped, reason); + else + PRE_MSG(false, "Unexpected status"); +} + + +/// Converts a string to an integer. +/// +/// \param str The string containing the integer to convert. +/// +/// \return The converted integer; none if the parsing fails. +static optional< int > +parse_int(const std::string& str) +{ + try { + return utils::make_optional(text::to_type< int >(str)); + } catch (const text::value_error& e) { + return none; + } +} + + +/// Parses a test result that needs a reason and accepts an optional integer. +/// +/// \param status The result status name. +/// \param rest The rest of the line after the status name. +/// +/// \return The parsed test result if the data is valid, or a broken result if +/// the parsing failed. +/// +/// \pre status must be one of "expected_exit" or "expected_signal". +static engine::atf_result +parse_with_reason_and_arg(const std::string& status, const std::string& rest) +{ + using engine::atf_result; + + std::string::size_type delim = rest.find_first_of(":("); + if (delim == std::string::npos) + throw engine::format_error(F("Invalid format for '%s' test case " + "result; must be followed by '[(num)]: " + "' but found '%s'") % + status % rest); + + optional< int > arg; + if (rest[delim] == '(') { + const std::string::size_type delim2 = rest.find("):", delim); + if (delim == std::string::npos) + throw engine::format_error(F("Mismatched '(' in %s") % rest); + + const std::string argstr = rest.substr(delim + 1, delim2 - delim - 1); + arg = parse_int(argstr); + if (!arg) + throw engine::format_error(F("Invalid integer argument '%s' to " + "'%s' test case result") % + argstr % status); + delim = delim2 + 1; + } + + const std::string reason = rest.substr(delim + 2); + + if (status == "expected_exit") + return atf_result(atf_result::expected_exit, arg, reason); + else if (status == "expected_signal") + return atf_result(atf_result::expected_signal, arg, reason); + else + PRE_MSG(false, "Unexpected status"); +} + + +/// Formats the termination status of a process to be used with validate_result. +/// +/// \param status The status to format. +/// +/// \return A string describing the status. +static std::string +format_status(const process::status& status) +{ + if (status.exited()) + return F("exited with code %s") % status.exitstatus(); + else if (status.signaled()) + return F("received signal %s%s") % status.termsig() % + (status.coredump() ? " (core dumped)" : ""); + else + return F("terminated in an unknown manner"); +} + + +} // anonymous namespace + + +/// Constructs a raw result with a type. +/// +/// The reason and the argument are left uninitialized. +/// +/// \param type_ The type of the result. +engine::atf_result::atf_result(const types type_) : + _type(type_) +{ +} + + +/// Constructs a raw result with a type and a reason. +/// +/// The argument is left uninitialized. +/// +/// \param type_ The type of the result. +/// \param reason_ The reason for the result. +engine::atf_result::atf_result(const types type_, const std::string& reason_) : + _type(type_), _reason(reason_) +{ +} + + +/// Constructs a raw result with a type, an optional argument and a reason. +/// +/// \param type_ The type of the result. +/// \param argument_ The optional argument for the result. +/// \param reason_ The reason for the result. +engine::atf_result::atf_result(const types type_, + const utils::optional< int >& argument_, + const std::string& reason_) : + _type(type_), _argument(argument_), _reason(reason_) +{ +} + + +/// Parses an input stream to extract a test result. +/// +/// If the parsing fails for any reason, the test result is 'broken' and it +/// contains the reason for the parsing failure. Test cases that report results +/// in an inconsistent state cannot be trusted (e.g. the test program code may +/// have a bug), and thus why they are reported as broken instead of just failed +/// (which is a legitimate result for a test case). +/// +/// \param input The stream to read from. +/// +/// \return A generic representation of the result of the test case. +/// +/// \throw format_error If the input is invalid. +engine::atf_result +engine::atf_result::parse(std::istream& input) +{ + const std::pair< size_t, std::string > data = read_lines(input); + if (data.first == 0) + throw format_error("Empty test result or no new line"); + else if (data.first > 1) + throw format_error("Test result contains multiple lines: " + + data.second); + else { + const std::string::size_type delim = data.second.find_first_not_of( + "abcdefghijklmnopqrstuvwxyz_"); + const std::string status = data.second.substr(0, delim); + const std::string rest = data.second.substr(status.length()); + + if (status == "broken") + return parse_with_reason(status, rest); + else if (status == "expected_death") + return parse_with_reason(status, rest); + else if (status == "expected_exit") + return parse_with_reason_and_arg(status, rest); + else if (status == "expected_failure") + return parse_with_reason(status, rest); + else if (status == "expected_signal") + return parse_with_reason_and_arg(status, rest); + else if (status == "expected_timeout") + return parse_with_reason(status, rest); + else if (status == "failed") + return parse_with_reason(status, rest); + else if (status == "passed") + return parse_without_reason(status, rest); + else if (status == "skipped") + return parse_with_reason(status, rest); + else + throw format_error(F("Unknown test result '%s'") % status); + } +} + + +/// Loads a test case result from a file. +/// +/// \param file The file to parse. +/// +/// \return The parsed test case result if all goes well. +/// +/// \throw std::runtime_error If the file does not exist. +/// \throw engine::format_error If the contents of the file are bogus. +engine::atf_result +engine::atf_result::load(const fs::path& file) +{ + std::ifstream input(file.c_str()); + if (!input) + throw std::runtime_error("Cannot open results file"); + else + return parse(input); +} + + +/// Gets the type of the result. +/// +/// \return A result type. +engine::atf_result::types +engine::atf_result::type(void) const +{ + return _type; +} + + +/// Gets the optional argument of the result. +/// +/// \return The argument of the result if present; none otherwise. +const optional< int >& +engine::atf_result::argument(void) const +{ + return _argument; +} + + +/// Gets the optional reason of the result. +/// +/// \return The reason of the result if present; none otherwise. +const optional< std::string >& +engine::atf_result::reason(void) const +{ + return _reason; +} + + +/// Checks whether the result should be reported as good or not. +/// +/// \return True if the result can be considered "good", false otherwise. +bool +engine::atf_result::good(void) const +{ + switch (_type) { + case atf_result::expected_death: + case atf_result::expected_exit: + case atf_result::expected_failure: + case atf_result::expected_signal: + case atf_result::expected_timeout: + case atf_result::passed: + case atf_result::skipped: + return true; + + case atf_result::broken: + case atf_result::failed: + return false; + + default: + UNREACHABLE; + } +} + + +/// Reinterprets a raw result based on the termination status of the test case. +/// +/// This reinterpretation ensures that the termination conditions of the program +/// match what is expected of the paticular result reported by the test program. +/// If such conditions do not match, the test program is considered bogus and is +/// thus reported as broken. +/// +/// This is just a helper function for calculate_result(); the real result of +/// the test case cannot be inferred from apply() only. +/// +/// \param status The exit status of the test program, or none if the test +/// program timed out. +/// +/// \result The adjusted result. The original result is transformed into broken +/// if the exit status of the program does not match our expectations. +engine::atf_result +engine::atf_result::apply(const optional< process::status >& status) + const +{ + if (!status) { + if (_type != atf_result::expected_timeout) + return atf_result(atf_result::broken, "Test case body timed out"); + else + return *this; + } + + INV(status); + switch (_type) { + case atf_result::broken: + return *this; + + case atf_result::expected_death: + return *this; + + case atf_result::expected_exit: + if (status.get().exited()) { + if (_argument) { + if (_argument.get() == status.get().exitstatus()) + return *this; + else + return atf_result( + atf_result::failed, + F("Test case expected to exit with code %s but got " + "code %s") % + _argument.get() % status.get().exitstatus()); + } else + return *this; + } else + return atf_result(atf_result::broken, "Expected clean exit but " + + format_status(status.get())); + + case atf_result::expected_failure: + if (status.get().exited() && status.get().exitstatus() == EXIT_SUCCESS) + return *this; + else + return atf_result(atf_result::broken, "Expected failure should " + "have reported success but " + + format_status(status.get())); + + case atf_result::expected_signal: + if (status.get().signaled()) { + if (_argument) { + if (_argument.get() == status.get().termsig()) + return *this; + else + return atf_result( + atf_result::failed, + F("Test case expected to receive signal %s but " + "got %s") % + _argument.get() % status.get().termsig()); + } else + return *this; + } else + return atf_result(atf_result::broken, "Expected signal but " + + format_status(status.get())); + + case atf_result::expected_timeout: + return atf_result(atf_result::broken, "Expected timeout but " + + format_status(status.get())); + + case atf_result::failed: + if (status.get().exited() && status.get().exitstatus() == EXIT_FAILURE) + return *this; + else + return atf_result(atf_result::broken, "Failed test case should " + "have reported failure but " + + format_status(status.get())); + + case atf_result::passed: + if (status.get().exited() && status.get().exitstatus() == EXIT_SUCCESS) + return *this; + else + return atf_result(atf_result::broken, "Passed test case should " + "have reported success but " + + format_status(status.get())); + + case atf_result::skipped: + if (status.get().exited() && status.get().exitstatus() == EXIT_SUCCESS) + return *this; + else + return atf_result(atf_result::broken, "Skipped test case should " + "have reported success but " + + format_status(status.get())); + } + + UNREACHABLE; +} + + +/// Converts an internal result to the interface-agnostic representation. +/// +/// \return A generic result instance representing this result. +model::test_result +engine::atf_result::externalize(void) const +{ + switch (_type) { + case atf_result::broken: + return model::test_result(model::test_result_broken, _reason.get()); + + case atf_result::expected_death: + case atf_result::expected_exit: + case atf_result::expected_failure: + case atf_result::expected_signal: + case atf_result::expected_timeout: + return model::test_result(model::test_result_expected_failure, + _reason.get()); + + case atf_result::failed: + return model::test_result(model::test_result_failed, _reason.get()); + + case atf_result::passed: + return model::test_result(model::test_result_passed); + + case atf_result::skipped: + return model::test_result(model::test_result_skipped, _reason.get()); + + default: + UNREACHABLE; + } +} + + +/// Compares two raw results for equality. +/// +/// \param other The result to compare to. +/// +/// \return True if the two raw results are equal; false otherwise. +bool +engine::atf_result::operator==(const atf_result& other) const +{ + return _type == other._type && _argument == other._argument && + _reason == other._reason; +} + + +/// Compares two raw results for inequality. +/// +/// \param other The result to compare to. +/// +/// \return True if the two raw results are different; false otherwise. +bool +engine::atf_result::operator!=(const atf_result& other) const +{ + return !(*this == other); +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +engine::operator<<(std::ostream& output, const atf_result& object) +{ + std::string result_name; + switch (object.type()) { + case atf_result::broken: result_name = "broken"; break; + case atf_result::expected_death: result_name = "expected_death"; break; + case atf_result::expected_exit: result_name = "expected_exit"; break; + case atf_result::expected_failure: result_name = "expected_failure"; break; + case atf_result::expected_signal: result_name = "expected_signal"; break; + case atf_result::expected_timeout: result_name = "expected_timeout"; break; + case atf_result::failed: result_name = "failed"; break; + case atf_result::passed: result_name = "passed"; break; + case atf_result::skipped: result_name = "skipped"; break; + } + + const optional< int >& argument = object.argument(); + + const optional< std::string >& reason = object.reason(); + + output << F("model::test_result{type=%s, argument=%s, reason=%s}") + % text::quote(result_name, '\'') + % (argument ? (F("%s") % argument.get()).str() : "none") + % (reason ? text::quote(reason.get(), '\'') : "none"); + + return output; +} + + +/// Calculates the user-visible result of a test case. +/// +/// This function needs to perform magic to ensure that what the test case +/// reports as its result is what the user should really see: i.e. it adjusts +/// the reported status of the test to the exit conditions of its body and +/// cleanup parts. +/// +/// \param body_status The termination status of the process that executed +/// the body of the test. None if the body timed out. +/// \param results_file The path to the results file that the test case body is +/// supposed to have created. +/// +/// \return The calculated test case result. +model::test_result +engine::calculate_atf_result(const optional< process::status >& body_status, + const fs::path& results_file) +{ + using engine::atf_result; + + atf_result result(atf_result::broken, "Unknown result"); + try { + result = atf_result::load(results_file); + } catch (const engine::format_error& error) { + result = atf_result(atf_result::broken, error.what()); + } catch (const std::runtime_error& error) { + if (body_status) + result = atf_result( + atf_result::broken, F("Premature exit; test case %s") % + format_status(body_status.get())); + else { + // The test case timed out. apply() handles this case later. + } + } + + result = result.apply(body_status); + + return result.externalize(); +} diff --git a/engine/atf_result.hpp b/engine/atf_result.hpp new file mode 100644 index 000000000000..55f8a117a237 --- /dev/null +++ b/engine/atf_result.hpp @@ -0,0 +1,114 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/atf_result.hpp +/// Functions and types to process the results of ATF-based test cases. + +#if !defined(ENGINE_ATF_RESULT_HPP) +#define ENGINE_ATF_RESULT_HPP + +#include "engine/atf_result_fwd.hpp" + +#include +#include + +#include "model/test_result_fwd.hpp" +#include "utils/optional.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace engine { + + +/// Internal representation of the raw result files of ATF-based tests. +/// +/// This class is used exclusively to represent the transient result files read +/// from test cases before generating the "public" version of the result. This +/// class should actually not be exposed in the header files, but it is for +/// testing purposes only. +class atf_result { +public: + /// List of possible types for the test case result. + enum types { + broken, + expected_death, + expected_exit, + expected_failure, + expected_signal, + expected_timeout, + failed, + passed, + skipped, + }; + +private: + /// The test case result. + types _type; + + /// The optional integral argument that may accompany the result. + /// + /// Should only be present if the type is expected_exit or expected_signal. + utils::optional< int > _argument; + + /// A description of the test case result. + /// + /// Should always be present except for the passed type. + utils::optional< std::string > _reason; + +public: + atf_result(const types); + atf_result(const types, const std::string&); + atf_result(const types, const utils::optional< int >&, const std::string&); + + static atf_result parse(std::istream&); + static atf_result load(const utils::fs::path&); + + types type(void) const; + const utils::optional< int >& argument(void) const; + const utils::optional< std::string >& reason(void) const; + + bool good(void) const; + atf_result apply(const utils::optional< utils::process::status >&) const; + model::test_result externalize(void) const; + + bool operator==(const atf_result&) const; + bool operator!=(const atf_result&) const; +}; + + +std::ostream& operator<<(std::ostream&, const atf_result&); + + +model::test_result calculate_atf_result( + const utils::optional< utils::process::status >&, + const utils::fs::path&); + + +} // namespace engine + +#endif // !defined(ENGINE_ATF_IFACE_RESULTS_HPP) diff --git a/engine/atf_result_fwd.hpp b/engine/atf_result_fwd.hpp new file mode 100644 index 000000000000..2a1440e4929c --- /dev/null +++ b/engine/atf_result_fwd.hpp @@ -0,0 +1,43 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/atf_result_fwd.hpp +/// Forward declarations for engine/atf_result.hpp + +#if !defined(ENGINE_ATF_RESULT_FWD_HPP) +#define ENGINE_ATF_RESULT_FWD_HPP + +namespace engine { + + +class atf_result; + + +} // namespace engine + +#endif // !defined(ENGINE_ATF_RESULT_FWD_HPP) diff --git a/engine/atf_result_test.cpp b/engine/atf_result_test.cpp new file mode 100644 index 000000000000..8ec61dc3c07e --- /dev/null +++ b/engine/atf_result_test.cpp @@ -0,0 +1,788 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/atf_result.hpp" + +extern "C" { +#include +} + +#include +#include +#include +#include + +#include + +#include "engine/exceptions.hpp" +#include "model/test_result.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/status.hpp" + +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Performs a test for results::parse() that should succeed. +/// +/// \param exp_type The expected type of the result. +/// \param exp_argument The expected argument in the result, if any. +/// \param exp_reason The expected reason describing the result, if any. +/// \param text The literal input to parse; can include multiple lines. +static void +parse_ok_test(const engine::atf_result::types& exp_type, + const optional< int >& exp_argument, + const char* exp_reason, const char* text) +{ + std::istringstream input(text); + const engine::atf_result actual = engine::atf_result::parse(input); + ATF_REQUIRE(exp_type == actual.type()); + ATF_REQUIRE_EQ(exp_argument, actual.argument()); + if (exp_reason != NULL) { + ATF_REQUIRE(actual.reason()); + ATF_REQUIRE_EQ(exp_reason, actual.reason().get()); + } else { + ATF_REQUIRE(!actual.reason()); + } +} + + +/// Wrapper around parse_ok_test to define a test case. +/// +/// \param name The name of the test case; will be prefixed with +/// "atf_result__parse__". +/// \param exp_type The expected type of the result. +/// \param exp_argument The expected argument in the result, if any. +/// \param exp_reason The expected reason describing the result, if any. +/// \param input The literal input to parse. +#define PARSE_OK(name, exp_type, exp_argument, exp_reason, input) \ + ATF_TEST_CASE_WITHOUT_HEAD(atf_result__parse__ ## name); \ + ATF_TEST_CASE_BODY(atf_result__parse__ ## name) \ + { \ + parse_ok_test(exp_type, exp_argument, exp_reason, input); \ + } + + +/// Performs a test for results::parse() that should fail. +/// +/// \param reason_regexp The reason to match against the broken reason. +/// \param text The literal input to parse; can include multiple lines. +static void +parse_broken_test(const char* reason_regexp, const char* text) +{ + std::istringstream input(text); + ATF_REQUIRE_THROW_RE(engine::format_error, reason_regexp, + engine::atf_result::parse(input)); +} + + +/// Wrapper around parse_broken_test to define a test case. +/// +/// \param name The name of the test case; will be prefixed with +/// "atf_result__parse__". +/// \param reason_regexp The reason to match against the broken reason. +/// \param input The literal input to parse. +#define PARSE_BROKEN(name, reason_regexp, input) \ + ATF_TEST_CASE_WITHOUT_HEAD(atf_result__parse__ ## name); \ + ATF_TEST_CASE_BODY(atf_result__parse__ ## name) \ + { \ + parse_broken_test(reason_regexp, input); \ + } + + +} // anonymous namespace + + +PARSE_BROKEN(empty, + "Empty.*no new line", + ""); +PARSE_BROKEN(no_newline__unknown, + "Empty.*no new line", + "foo"); +PARSE_BROKEN(no_newline__known, + "Empty.*no new line", + "passed"); +PARSE_BROKEN(multiline__no_newline, + "multiple lines.*foo<>bar", + "failed: foo\nbar"); +PARSE_BROKEN(multiline__with_newline, + "multiple lines.*foo<>bar", + "failed: foo\nbar\n"); +PARSE_BROKEN(unknown_status__no_reason, + "Unknown.*result.*'cba'", + "cba\n"); +PARSE_BROKEN(unknown_status__with_reason, + "Unknown.*result.*'hgf'", + "hgf: foo\n"); +PARSE_BROKEN(missing_reason__no_delim, + "failed.*followed by.*reason", + "failed\n"); +PARSE_BROKEN(missing_reason__bad_delim, + "failed.*followed by.*reason", + "failed:\n"); +PARSE_BROKEN(missing_reason__empty, + "failed.*followed by.*reason", + "failed: \n"); + + +PARSE_OK(broken__ok, + engine::atf_result::broken, none, "a b c", + "broken: a b c\n"); +PARSE_OK(broken__blanks, + engine::atf_result::broken, none, " ", + "broken: \n"); + + +PARSE_OK(expected_death__ok, + engine::atf_result::expected_death, none, "a b c", + "expected_death: a b c\n"); +PARSE_OK(expected_death__blanks, + engine::atf_result::expected_death, none, " ", + "expected_death: \n"); + + +PARSE_OK(expected_exit__ok__any, + engine::atf_result::expected_exit, none, "any exit code", + "expected_exit: any exit code\n"); +PARSE_OK(expected_exit__ok__specific, + engine::atf_result::expected_exit, optional< int >(712), + "some known exit code", + "expected_exit(712): some known exit code\n"); +PARSE_BROKEN(expected_exit__bad_int, + "Invalid integer.*45a3", + "expected_exit(45a3): this is broken\n"); + + +PARSE_OK(expected_failure__ok, + engine::atf_result::expected_failure, none, "a b c", + "expected_failure: a b c\n"); +PARSE_OK(expected_failure__blanks, + engine::atf_result::expected_failure, none, " ", + "expected_failure: \n"); + + +PARSE_OK(expected_signal__ok__any, + engine::atf_result::expected_signal, none, "any signal code", + "expected_signal: any signal code\n"); +PARSE_OK(expected_signal__ok__specific, + engine::atf_result::expected_signal, optional< int >(712), + "some known signal code", + "expected_signal(712): some known signal code\n"); +PARSE_BROKEN(expected_signal__bad_int, + "Invalid integer.*45a3", + "expected_signal(45a3): this is broken\n"); + + +PARSE_OK(expected_timeout__ok, + engine::atf_result::expected_timeout, none, "a b c", + "expected_timeout: a b c\n"); +PARSE_OK(expected_timeout__blanks, + engine::atf_result::expected_timeout, none, " ", + "expected_timeout: \n"); + + +PARSE_OK(failed__ok, + engine::atf_result::failed, none, "a b c", + "failed: a b c\n"); +PARSE_OK(failed__blanks, + engine::atf_result::failed, none, " ", + "failed: \n"); + + +PARSE_OK(passed__ok, + engine::atf_result::passed, none, NULL, + "passed\n"); +PARSE_BROKEN(passed__reason, + "cannot have a reason", + "passed a b c\n"); + + +PARSE_OK(skipped__ok, + engine::atf_result::skipped, none, "a b c", + "skipped: a b c\n"); +PARSE_OK(skipped__blanks, + engine::atf_result::skipped, none, " ", + "skipped: \n"); + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__load__ok); +ATF_TEST_CASE_BODY(atf_result__load__ok) +{ + std::ofstream output("result.txt"); + ATF_REQUIRE(output); + output << "skipped: a b c\n"; + output.close(); + + const engine::atf_result result = engine::atf_result::load( + utils::fs::path("result.txt")); + ATF_REQUIRE(engine::atf_result::skipped == result.type()); + ATF_REQUIRE(!result.argument()); + ATF_REQUIRE(result.reason()); + ATF_REQUIRE_EQ("a b c", result.reason().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__load__missing_file); +ATF_TEST_CASE_BODY(atf_result__load__missing_file) +{ + ATF_REQUIRE_THROW_RE( + std::runtime_error, "Cannot open", + engine::atf_result::load(utils::fs::path("result.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__load__format_error); +ATF_TEST_CASE_BODY(atf_result__load__format_error) +{ + std::ofstream output("abc.txt"); + ATF_REQUIRE(output); + output << "passed: foo\n"; + output.close(); + + ATF_REQUIRE_THROW_RE(engine::format_error, "cannot have a reason", + engine::atf_result::load(utils::fs::path("abc.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__broken__ok); +ATF_TEST_CASE_BODY(atf_result__apply__broken__ok) +{ + const engine::atf_result in_result(engine::atf_result::broken, + "Passthrough"); + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + ATF_REQUIRE_EQ(in_result, in_result.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__timed_out); +ATF_TEST_CASE_BODY(atf_result__apply__timed_out) +{ + const engine::atf_result timed_out(engine::atf_result::broken, + "Some arbitrary error"); + ATF_REQUIRE_EQ(engine::atf_result(engine::atf_result::broken, + "Test case body timed out"), + timed_out.apply(none)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_death__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_death__ok) +{ + const engine::atf_result in_result(engine::atf_result::expected_death, + "Passthrough"); + const process::status status = process::status::fake_signaled(SIGINT, true); + ATF_REQUIRE_EQ(in_result, in_result.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_exit__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_exit__ok) +{ + const process::status success = process::status::fake_exited(EXIT_SUCCESS); + const process::status failure = process::status::fake_exited(EXIT_FAILURE); + + const engine::atf_result any_code(engine::atf_result::expected_exit, none, + "The reason"); + ATF_REQUIRE_EQ(any_code, any_code.apply(utils::make_optional(success))); + ATF_REQUIRE_EQ(any_code, any_code.apply(utils::make_optional(failure))); + + const engine::atf_result a_code(engine::atf_result::expected_exit, + utils::make_optional(EXIT_FAILURE), "The reason"); + ATF_REQUIRE_EQ(a_code, a_code.apply(utils::make_optional(failure))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_exit__failed); +ATF_TEST_CASE_BODY(atf_result__apply__expected_exit__failed) +{ + const process::status success = process::status::fake_exited(EXIT_SUCCESS); + + const engine::atf_result a_code(engine::atf_result::expected_exit, + utils::make_optional(EXIT_FAILURE), "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::failed, + "Test case expected to exit with code 1 but got " + "code 0"), + a_code.apply(utils::make_optional(success))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_exit__broken); +ATF_TEST_CASE_BODY(atf_result__apply__expected_exit__broken) +{ + const process::status sig3 = process::status::fake_signaled(3, false); + + const engine::atf_result any_code(engine::atf_result::expected_exit, none, + "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected clean exit but received signal 3"), + any_code.apply(utils::make_optional(sig3))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_failure__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_failure__ok) +{ + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + const engine::atf_result xfailure(engine::atf_result::expected_failure, + "The reason"); + ATF_REQUIRE_EQ(xfailure, xfailure.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_failure__broken); +ATF_TEST_CASE_BODY(atf_result__apply__expected_failure__broken) +{ + const process::status failure = process::status::fake_exited(EXIT_FAILURE); + const process::status sig3 = process::status::fake_signaled(3, true); + const process::status sig4 = process::status::fake_signaled(4, false); + + const engine::atf_result xfailure(engine::atf_result::expected_failure, + "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected failure should have reported success but " + "exited with code 1"), + xfailure.apply(utils::make_optional(failure))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected failure should have reported success but " + "received signal 3 (core dumped)"), + xfailure.apply(utils::make_optional(sig3))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected failure should have reported success but " + "received signal 4"), + xfailure.apply(utils::make_optional(sig4))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_signal__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_signal__ok) +{ + const process::status sig1 = process::status::fake_signaled(1, false); + const process::status sig3 = process::status::fake_signaled(3, true); + + const engine::atf_result any_sig(engine::atf_result::expected_signal, none, + "The reason"); + ATF_REQUIRE_EQ(any_sig, any_sig.apply(utils::make_optional(sig1))); + ATF_REQUIRE_EQ(any_sig, any_sig.apply(utils::make_optional(sig3))); + + const engine::atf_result a_sig(engine::atf_result::expected_signal, + utils::make_optional(3), "The reason"); + ATF_REQUIRE_EQ(a_sig, a_sig.apply(utils::make_optional(sig3))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_signal__failed); +ATF_TEST_CASE_BODY(atf_result__apply__expected_signal__failed) +{ + const process::status sig5 = process::status::fake_signaled(5, false); + + const engine::atf_result a_sig(engine::atf_result::expected_signal, + utils::make_optional(4), "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::failed, + "Test case expected to receive signal 4 but got 5"), + a_sig.apply(utils::make_optional(sig5))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_signal__broken); +ATF_TEST_CASE_BODY(atf_result__apply__expected_signal__broken) +{ + const process::status success = process::status::fake_exited(EXIT_SUCCESS); + + const engine::atf_result any_sig(engine::atf_result::expected_signal, none, + "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected signal but exited with code 0"), + any_sig.apply(utils::make_optional(success))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_timeout__ok); +ATF_TEST_CASE_BODY(atf_result__apply__expected_timeout__ok) +{ + const engine::atf_result timeout(engine::atf_result::expected_timeout, + "The reason"); + ATF_REQUIRE_EQ(timeout, timeout.apply(none)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__expected_timeout__broken); +ATF_TEST_CASE_BODY(atf_result__apply__expected_timeout__broken) +{ + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + const engine::atf_result timeout(engine::atf_result::expected_timeout, + "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Expected timeout but exited with code 0"), + timeout.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__failed__ok); +ATF_TEST_CASE_BODY(atf_result__apply__failed__ok) +{ + const process::status status = process::status::fake_exited(EXIT_FAILURE); + const engine::atf_result failed(engine::atf_result::failed, "The reason"); + ATF_REQUIRE_EQ(failed, failed.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__failed__broken); +ATF_TEST_CASE_BODY(atf_result__apply__failed__broken) +{ + const process::status success = process::status::fake_exited(EXIT_SUCCESS); + const process::status sig3 = process::status::fake_signaled(3, true); + const process::status sig4 = process::status::fake_signaled(4, false); + + const engine::atf_result failed(engine::atf_result::failed, "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Failed test case should have reported failure but " + "exited with code 0"), + failed.apply(utils::make_optional(success))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Failed test case should have reported failure but " + "received signal 3 (core dumped)"), + failed.apply(utils::make_optional(sig3))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Failed test case should have reported failure but " + "received signal 4"), + failed.apply(utils::make_optional(sig4))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__passed__ok); +ATF_TEST_CASE_BODY(atf_result__apply__passed__ok) +{ + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + const engine::atf_result passed(engine::atf_result::passed); + ATF_REQUIRE_EQ(passed, passed.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__passed__broken); +ATF_TEST_CASE_BODY(atf_result__apply__passed__broken) +{ + const process::status failure = process::status::fake_exited(EXIT_FAILURE); + const process::status sig3 = process::status::fake_signaled(3, true); + const process::status sig4 = process::status::fake_signaled(4, false); + + const engine::atf_result passed(engine::atf_result::passed); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Passed test case should have reported success but " + "exited with code 1"), + passed.apply(utils::make_optional(failure))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Passed test case should have reported success but " + "received signal 3 (core dumped)"), + passed.apply(utils::make_optional(sig3))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Passed test case should have reported success but " + "received signal 4"), + passed.apply(utils::make_optional(sig4))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__skipped__ok); +ATF_TEST_CASE_BODY(atf_result__apply__skipped__ok) +{ + const process::status status = process::status::fake_exited(EXIT_SUCCESS); + const engine::atf_result skipped(engine::atf_result::skipped, "The reason"); + ATF_REQUIRE_EQ(skipped, skipped.apply(utils::make_optional(status))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__apply__skipped__broken); +ATF_TEST_CASE_BODY(atf_result__apply__skipped__broken) +{ + const process::status failure = process::status::fake_exited(EXIT_FAILURE); + const process::status sig3 = process::status::fake_signaled(3, true); + const process::status sig4 = process::status::fake_signaled(4, false); + + const engine::atf_result skipped(engine::atf_result::skipped, "The reason"); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Skipped test case should have reported success but " + "exited with code 1"), + skipped.apply(utils::make_optional(failure))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Skipped test case should have reported success but " + "received signal 3 (core dumped)"), + skipped.apply(utils::make_optional(sig3))); + ATF_REQUIRE_EQ( + engine::atf_result(engine::atf_result::broken, + "Skipped test case should have reported success but " + "received signal 4"), + skipped.apply(utils::make_optional(sig4))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__broken); +ATF_TEST_CASE_BODY(atf_result__externalize__broken) +{ + const engine::atf_result raw(engine::atf_result::broken, "The reason"); + const model::test_result expected(model::test_result_broken, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_death); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_death) +{ + const engine::atf_result raw(engine::atf_result::expected_death, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_exit); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_exit) +{ + const engine::atf_result raw(engine::atf_result::expected_exit, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_failure); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_failure) +{ + const engine::atf_result raw(engine::atf_result::expected_failure, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_signal); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_signal) +{ + const engine::atf_result raw(engine::atf_result::expected_signal, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__expected_timeout); +ATF_TEST_CASE_BODY(atf_result__externalize__expected_timeout) +{ + const engine::atf_result raw(engine::atf_result::expected_timeout, + "The reason"); + const model::test_result expected(model::test_result_expected_failure, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__failed); +ATF_TEST_CASE_BODY(atf_result__externalize__failed) +{ + const engine::atf_result raw(engine::atf_result::failed, "The reason"); + const model::test_result expected(model::test_result_failed, + "The reason"); + ATF_REQUIRE(expected == raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__passed); +ATF_TEST_CASE_BODY(atf_result__externalize__passed) +{ + const engine::atf_result raw(engine::atf_result::passed); + const model::test_result expected(model::test_result_passed); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(atf_result__externalize__skipped); +ATF_TEST_CASE_BODY(atf_result__externalize__skipped) +{ + const engine::atf_result raw(engine::atf_result::skipped, "The reason"); + const model::test_result expected(model::test_result_skipped, + "The reason"); + ATF_REQUIRE_EQ(expected, raw.externalize()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(calculate_atf_result__missing_file); +ATF_TEST_CASE_BODY(calculate_atf_result__missing_file) +{ + using process::status; + + const status body_status = status::fake_exited(EXIT_SUCCESS); + const model::test_result expected( + model::test_result_broken, + "Premature exit; test case exited with code 0"); + ATF_REQUIRE_EQ(expected, engine::calculate_atf_result( + utils::make_optional(body_status), fs::path("foo"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(calculate_atf_result__bad_file); +ATF_TEST_CASE_BODY(calculate_atf_result__bad_file) +{ + using process::status; + + const status body_status = status::fake_exited(EXIT_SUCCESS); + atf::utils::create_file("foo", "invalid\n"); + const model::test_result expected(model::test_result_broken, + "Unknown test result 'invalid'"); + ATF_REQUIRE_EQ(expected, engine::calculate_atf_result( + utils::make_optional(body_status), fs::path("foo"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(calculate_atf_result__body_ok); +ATF_TEST_CASE_BODY(calculate_atf_result__body_ok) +{ + using process::status; + + atf::utils::create_file("result.txt", "skipped: Something\n"); + const status body_status = status::fake_exited(EXIT_SUCCESS); + ATF_REQUIRE_EQ( + model::test_result(model::test_result_skipped, "Something"), + engine::calculate_atf_result(utils::make_optional(body_status), + fs::path("result.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(calculate_atf_result__body_bad); +ATF_TEST_CASE_BODY(calculate_atf_result__body_bad) +{ + using process::status; + + atf::utils::create_file("result.txt", "skipped: Something\n"); + const status body_status = status::fake_exited(EXIT_FAILURE); + ATF_REQUIRE_EQ( + model::test_result(model::test_result_broken, "Skipped test case " + "should have reported success but exited with " + "code 1"), + engine::calculate_atf_result(utils::make_optional(body_status), + fs::path("result.txt"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, atf_result__parse__empty); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__no_newline__unknown); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__no_newline__known); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__multiline__no_newline); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__multiline__with_newline); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__unknown_status__no_reason); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__unknown_status__with_reason); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__missing_reason__no_delim); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__missing_reason__bad_delim); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__missing_reason__empty); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__broken__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__broken__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_death__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_death__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_exit__ok__any); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_exit__ok__specific); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_exit__bad_int); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_failure__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_failure__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_signal__ok__any); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_signal__ok__specific); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_signal__bad_int); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_timeout__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__expected_timeout__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__failed__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__failed__blanks); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__passed__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__passed__reason); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__skipped__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__parse__skipped__blanks); + + ATF_ADD_TEST_CASE(tcs, atf_result__load__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__load__missing_file); + ATF_ADD_TEST_CASE(tcs, atf_result__load__format_error); + + ATF_ADD_TEST_CASE(tcs, atf_result__apply__broken__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__timed_out); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_death__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_exit__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_exit__failed); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_exit__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_failure__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_failure__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_signal__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_signal__failed); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_signal__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_timeout__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__expected_timeout__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__failed__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__failed__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__passed__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__passed__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__skipped__ok); + ATF_ADD_TEST_CASE(tcs, atf_result__apply__skipped__broken); + + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__broken); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_death); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_exit); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_failure); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_signal); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__expected_timeout); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__failed); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__passed); + ATF_ADD_TEST_CASE(tcs, atf_result__externalize__skipped); + + ATF_ADD_TEST_CASE(tcs, calculate_atf_result__missing_file); + ATF_ADD_TEST_CASE(tcs, calculate_atf_result__bad_file); + ATF_ADD_TEST_CASE(tcs, calculate_atf_result__body_ok); + ATF_ADD_TEST_CASE(tcs, calculate_atf_result__body_bad); +} diff --git a/engine/atf_test.cpp b/engine/atf_test.cpp new file mode 100644 index 000000000000..9fe7797f4362 --- /dev/null +++ b/engine/atf_test.cpp @@ -0,0 +1,450 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/atf.hpp" + +extern "C" { +#include + +#include +} + +#include + +#include "engine/config.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/stacktrace.hpp" +#include "utils/test_utils.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; + + +namespace { + + +/// Lists the test cases associated with an ATF test program. +/// +/// \param program_name Basename of the test program to run. +/// \param root Path to the base of the test suite. +/// \param names_filter Whitespace-separated list of test cases that the helper +/// test program is allowed to expose. +/// \param user_config User-provided configuration. +/// +/// \return The list of loaded test cases. +static model::test_cases_map +list_one(const char* program_name, + const fs::path& root, + const char* names_filter = NULL, + config::tree user_config = engine::empty_config()) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const scheduler::lazy_test_program program( + "atf", fs::path(program_name), root, "the-suite", + model::metadata_builder().build(), user_config, handle); + + if (names_filter != NULL) + utils::setenv("TEST_CASES", names_filter); + const model::test_cases_map test_cases = handle.list_tests( + &program, user_config); + + handle.cleanup(); + + return test_cases; +} + + +/// Runs a bogus test program and checks the error result. +/// +/// \param exp_error Expected error string to find. +/// \param program_name Basename of the test program to run. +/// \param root Path to the base of the test suite. +/// \param names_filter Whitespace-separated list of test cases that the helper +/// test program is allowed to expose. +static void +check_list_one_fail(const char* exp_error, + const char* program_name, + const fs::path& root, + const char* names_filter = NULL) +{ + const model::test_cases_map test_cases = list_one( + program_name, root, names_filter); + + ATF_REQUIRE_EQ(1, test_cases.size()); + const model::test_case& test_case = test_cases.begin()->second; + ATF_REQUIRE_EQ("__test_cases_list__", test_case.name()); + ATF_REQUIRE(test_case.fake_result()); + ATF_REQUIRE_MATCH(exp_error, + test_case.fake_result().get().reason()); +} + + +/// Runs one ATF test program and checks its result. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param test_case_name Name of the "test case" to select from the helper +/// program. +/// \param exp_result The expected result. +/// \param user_config User-provided configuration. +/// \param check_empty_output If true, verify that the output of the test is +/// silent. This is just a hack to implement one of the test cases; we'd +/// easily have a nicer abstraction here... +static void +run_one(const atf::tests::tc* tc, const char* test_case_name, + const model::test_result& exp_result, + config::tree user_config = engine::empty_config(), + const bool check_empty_output = false) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + const model::test_program_ptr program(new scheduler::lazy_test_program( + "atf", fs::path("atf_helpers"), fs::path(tc->get_config_var("srcdir")), + "the-suite", model::metadata_builder().build(), + user_config, handle)); + + (void)handle.spawn_test(program, test_case_name, user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + atf::utils::cat_file(result_handle->stdout_file().str(), "stdout: "); + atf::utils::cat_file(result_handle->stderr_file().str(), "stderr: "); + ATF_REQUIRE_EQ(exp_result, test_result_handle->test_result()); + if (check_empty_output) { + ATF_REQUIRE(atf::utils::compare_file(result_handle->stdout_file().str(), + "")); + ATF_REQUIRE(atf::utils::compare_file(result_handle->stderr_file().str(), + "")); + } + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(list__ok); +ATF_TEST_CASE_BODY(list__ok) +{ + const model::test_cases_map test_cases = list_one( + "atf_helpers", fs::path(get_config_var("srcdir")), "pass crash"); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("crash") + .add("pass", model::metadata_builder() + .set_description("Always-passing test case") + .build()) + .build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__configuration_variables); +ATF_TEST_CASE_BODY(list__configuration_variables) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.var1", "value1"); + user_config.set_string("test_suites.the-suite.var2", "value2"); + + const model::test_cases_map test_cases = list_one( + "atf_helpers", fs::path(get_config_var("srcdir")), "check_list_config", + user_config); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("check_list_config", model::metadata_builder() + .set_description("Found: var1=value1 var2=value2") + .build()) + .build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__current_directory); +ATF_TEST_CASE_BODY(list__current_directory) +{ + const fs::path helpers = fs::path(get_config_var("srcdir")) / "atf_helpers"; + ATF_REQUIRE(::symlink(helpers.c_str(), "atf_helpers") != -1); + const model::test_cases_map test_cases = list_one( + "atf_helpers", fs::path("."), "pass"); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("pass", model::metadata_builder() + .set_description("Always-passing test case") + .build()) + .build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__relative_path); +ATF_TEST_CASE_BODY(list__relative_path) +{ + const fs::path helpers = fs::path(get_config_var("srcdir")) / "atf_helpers"; + ATF_REQUIRE(::mkdir("dir1", 0755) != -1); + ATF_REQUIRE(::mkdir("dir1/dir2", 0755) != -1); + ATF_REQUIRE(::symlink(helpers.c_str(), "dir1/dir2/atf_helpers") != -1); + const model::test_cases_map test_cases = list_one( + "dir2/atf_helpers", fs::path("dir1"), "pass"); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("pass", model::metadata_builder() + .set_description("Always-passing test case") + .build()) + .build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__missing_test_program); +ATF_TEST_CASE_BODY(list__missing_test_program) +{ + check_list_one_fail("Cannot find test program", "non-existent", + fs::current_path()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__not_a_test_program); +ATF_TEST_CASE_BODY(list__not_a_test_program) +{ + atf::utils::create_file("not-valid", "garbage\n"); + ATF_REQUIRE(::chmod("not-valid", 0755) != -1); + check_list_one_fail("Invalid test program format", "not-valid", + fs::current_path()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__no_permissions); +ATF_TEST_CASE_BODY(list__no_permissions) +{ + atf::utils::create_file("not-executable", "garbage\n"); + check_list_one_fail("Permission denied to run test program", + "not-executable", fs::current_path()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__abort); +ATF_TEST_CASE_BODY(list__abort) +{ + check_list_one_fail("Test program received signal", "atf_helpers", + fs::path(get_config_var("srcdir")), + "crash_head"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__empty); +ATF_TEST_CASE_BODY(list__empty) +{ + check_list_one_fail("No test cases", "atf_helpers", + fs::path(get_config_var("srcdir")), + ""); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list__stderr_not_quiet); +ATF_TEST_CASE_BODY(list__stderr_not_quiet) +{ + check_list_one_fail("Test case list wrote to stderr", "atf_helpers", + fs::path(get_config_var("srcdir")), + "output_in_list"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__passes); +ATF_TEST_CASE_BODY(test__body_only__passes) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "pass", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__crashes); +ATF_TEST_CASE_BODY(test__body_only__crashes) +{ + utils::prepare_coredump_test(this); + + const model::test_result exp_result( + model::test_result_broken, + F("Premature exit; test case received signal %s (core dumped)") % + SIGABRT); + run_one(this, "crash", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__times_out); +ATF_TEST_CASE_BODY(test__body_only__times_out) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.control_dir", + fs::current_path().str()); + user_config.set_string("test_suites.the-suite.timeout", "1"); + + const model::test_result exp_result( + model::test_result_broken, "Test case body timed out"); + run_one(this, "timeout_body", exp_result, user_config); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__configuration_variables); +ATF_TEST_CASE_BODY(test__body_only__configuration_variables) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.first", "some value"); + user_config.set_string("test_suites.the-suite.second", "some other value"); + + const model::test_result exp_result(model::test_result_passed); + run_one(this, "check_configuration_variables", exp_result, user_config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_only__no_atf_run_warning); +ATF_TEST_CASE_BODY(test__body_only__no_atf_run_warning) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "pass", exp_result, engine::empty_config(), true); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__body_times_out); +ATF_TEST_CASE_BODY(test__body_and_cleanup__body_times_out) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.control_dir", + fs::current_path().str()); + user_config.set_string("test_suites.the-suite.timeout", "1"); + + const model::test_result exp_result( + model::test_result_broken, "Test case body timed out"); + run_one(this, "timeout_body", exp_result, user_config); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); + ATF_REQUIRE(atf::utils::file_exists("cookie.cleanup")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__cleanup_crashes); +ATF_TEST_CASE_BODY(test__body_and_cleanup__cleanup_crashes) +{ + const model::test_result exp_result( + model::test_result_broken, + "Test case cleanup did not terminate successfully"); + run_one(this, "crash_cleanup", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__cleanup_times_out); +ATF_TEST_CASE_BODY(test__body_and_cleanup__cleanup_times_out) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.control_dir", + fs::current_path().str()); + + scheduler::cleanup_timeout = datetime::delta(1, 0); + const model::test_result exp_result( + model::test_result_broken, "Test case cleanup timed out"); + run_one(this, "timeout_cleanup", exp_result, user_config); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__expect_timeout); +ATF_TEST_CASE_BODY(test__body_and_cleanup__expect_timeout) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.control_dir", + fs::current_path().str()); + user_config.set_string("test_suites.the-suite.timeout", "1"); + + const model::test_result exp_result( + model::test_result_expected_failure, "Times out on purpose"); + run_one(this, "expect_timeout", exp_result, user_config); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); + ATF_REQUIRE(atf::utils::file_exists("cookie.cleanup")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__body_and_cleanup__shared_workdir); +ATF_TEST_CASE_BODY(test__body_and_cleanup__shared_workdir) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "shared_workdir", exp_result); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "atf", std::shared_ptr< scheduler::interface >( + new engine::atf_interface())); + + ATF_ADD_TEST_CASE(tcs, list__ok); + ATF_ADD_TEST_CASE(tcs, list__configuration_variables); + ATF_ADD_TEST_CASE(tcs, list__current_directory); + ATF_ADD_TEST_CASE(tcs, list__relative_path); + ATF_ADD_TEST_CASE(tcs, list__missing_test_program); + ATF_ADD_TEST_CASE(tcs, list__not_a_test_program); + ATF_ADD_TEST_CASE(tcs, list__no_permissions); + ATF_ADD_TEST_CASE(tcs, list__abort); + ATF_ADD_TEST_CASE(tcs, list__empty); + ATF_ADD_TEST_CASE(tcs, list__stderr_not_quiet); + + ATF_ADD_TEST_CASE(tcs, test__body_only__passes); + ATF_ADD_TEST_CASE(tcs, test__body_only__crashes); + ATF_ADD_TEST_CASE(tcs, test__body_only__times_out); + ATF_ADD_TEST_CASE(tcs, test__body_only__configuration_variables); + ATF_ADD_TEST_CASE(tcs, test__body_only__no_atf_run_warning); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__body_times_out); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__cleanup_crashes); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__cleanup_times_out); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__expect_timeout); + ATF_ADD_TEST_CASE(tcs, test__body_and_cleanup__shared_workdir); +} diff --git a/engine/config.cpp b/engine/config.cpp new file mode 100644 index 000000000000..3f162a94fbb5 --- /dev/null +++ b/engine/config.cpp @@ -0,0 +1,254 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/config.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include + +#include "engine/exceptions.hpp" +#include "utils/config/exceptions.hpp" +#include "utils/config/parser.hpp" +#include "utils/config/tree.ipp" +#include "utils/passwd.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace text = utils::text; + + +namespace { + + +/// Defines the schema of a configuration tree. +/// +/// \param [in,out] tree The tree to populate. The tree should be empty on +/// entry to prevent collisions with the keys defined in here. +static void +init_tree(config::tree& tree) +{ + tree.define< config::string_node >("architecture"); + tree.define< config::positive_int_node >("parallelism"); + tree.define< config::string_node >("platform"); + tree.define< engine::user_node >("unprivileged_user"); + tree.define_dynamic("test_suites"); +} + + +/// Fills in a configuration tree with default values. +/// +/// \param [in,out] tree The tree to populate. init_tree() must have been +/// called on it beforehand. +static void +set_defaults(config::tree& tree) +{ + tree.set< config::string_node >("architecture", KYUA_ARCHITECTURE); + // TODO(jmmv): Automatically derive this from the number of CPUs in the + // machine and forcibly set to a value greater than 1. Still testing + // the new parallel implementation as of 2015-02-27 though. + tree.set< config::positive_int_node >("parallelism", 1); + tree.set< config::string_node >("platform", KYUA_PLATFORM); +} + + +/// Configuration parser specialization for Kyua configuration files. +class config_parser : public config::parser { + /// Initializes the configuration tree. + /// + /// This is a callback executed when the configuration script invokes the + /// syntax() method. We populate the configuration tree from here with the + /// schema version requested by the file. + /// + /// \param [in,out] tree The tree to populate. + /// \param syntax_version The version of the file format as specified in the + /// configuration file. + /// + /// \throw config::syntax_error If the syntax_format/syntax_version + /// combination is not supported. + void + setup(config::tree& tree, const int syntax_version) + { + if (syntax_version < 1 || syntax_version > 2) + throw config::syntax_error(F("Unsupported config version %s") % + syntax_version); + + init_tree(tree); + set_defaults(tree); + } + +public: + /// Initializes the parser. + /// + /// \param [out] tree_ The tree in which the results of the parsing will be + /// stored when parse() is called. Should be empty on entry. Because + /// we grab a reference to this object, the tree must remain valid for + /// the existence of the parser object. + explicit config_parser(config::tree& tree_) : + config::parser(tree_) + { + } +}; + + +} // anonymous namespace + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +engine::user_node::deep_copy(void) const +{ + std::auto_ptr< user_node > new_node(new user_node()); + new_node->_value = _value; + return new_node.release(); +} + + +/// Pushes the node's value onto the Lua stack. +/// +/// \param state The Lua state onto which to push the value. +void +engine::user_node::push_lua(lutok::state& state) const +{ + state.push_string(value().name); +} + + +/// Sets the value of the node from an entry in the Lua stack. +/// +/// \param state The Lua state from which to get the value. +/// \param value_index The stack index in which the value resides. +/// +/// \throw value_error If the value in state(value_index) cannot be +/// processed by this node. +void +engine::user_node::set_lua(lutok::state& state, const int value_index) +{ + if (state.is_number(value_index)) { + config::typed_leaf_node< passwd::user >::set( + passwd::find_user_by_uid(state.to_integer(-1))); + } else if (state.is_string(value_index)) { + config::typed_leaf_node< passwd::user >::set( + passwd::find_user_by_name(state.to_string(-1))); + } else + throw config::value_error("Invalid user identifier"); +} + + +/// Sets the value of the node from a raw string representation. +/// +/// \param raw_value The value to set the node to. +/// +/// \throw value_error If the value is invalid. +void +engine::user_node::set_string(const std::string& raw_value) +{ + try { + config::typed_leaf_node< passwd::user >::set( + passwd::find_user_by_name(raw_value)); + } catch (const std::runtime_error& e) { + int uid; + try { + uid = text::to_type< int >(raw_value); + } catch (const text::value_error& e2) { + throw error(F("Cannot find user with name '%s'") % raw_value); + } + + try { + config::typed_leaf_node< passwd::user >::set( + passwd::find_user_by_uid(uid)); + } catch (const std::runtime_error& e2) { + throw error(F("Cannot find user with UID %s") % uid); + } + } +} + + +/// Converts the contents of the node to a string. +/// +/// \pre The node must have a value. +/// +/// \return A string representation of the value held by the node. +std::string +engine::user_node::to_string(void) const +{ + return config::typed_leaf_node< passwd::user >::value().name; +} + + +/// Constructs a config with the built-in settings. +/// +/// \return A default test suite configuration. +config::tree +engine::default_config(void) +{ + config::tree tree(false); + init_tree(tree); + set_defaults(tree); + return tree; +} + + +/// Constructs a config with the built-in settings. +/// +/// \return An empty test suite configuration. +config::tree +engine::empty_config(void) +{ + config::tree tree(false); + init_tree(tree); + return tree; +} + + +/// Parses a test suite configuration file. +/// +/// \param file The file to parse. +/// +/// \return High-level representation of the configuration file. +/// +/// \throw load_error If there is any problem loading the file. This includes +/// file access errors and syntax errors. +config::tree +engine::load_config(const utils::fs::path& file) +{ + config::tree tree(false); + try { + config_parser(tree).parse(file); + } catch (const config::error& e) { + throw load_error(file, e.what()); + } + return tree; +} diff --git a/engine/config.hpp b/engine/config.hpp new file mode 100644 index 000000000000..2c1b83481862 --- /dev/null +++ b/engine/config.hpp @@ -0,0 +1,65 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/config.hpp +/// Test suite configuration parsing and representation. + +#if !defined(ENGINE_CONFIG_HPP) +#define ENGINE_CONFIG_HPP + +#include "engine/config_fwd.hpp" + +#include "utils/config/nodes.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/passwd_fwd.hpp" + +namespace engine { + + +/// Tree node to hold a system user identifier. +class user_node : public utils::config::typed_leaf_node< utils::passwd::user > { +public: + virtual base_node* deep_copy(void) const; + + void push_lua(lutok::state&) const; + void set_lua(lutok::state&, const int); + + void set_string(const std::string&); + std::string to_string(void) const; +}; + + +utils::config::tree default_config(void); +utils::config::tree empty_config(void); +utils::config::tree load_config(const utils::fs::path&); + + +} // namespace engine + +#endif // !defined(ENGINE_CONFIG_HPP) diff --git a/engine/config_fwd.hpp b/engine/config_fwd.hpp new file mode 100644 index 000000000000..82da9b1382bd --- /dev/null +++ b/engine/config_fwd.hpp @@ -0,0 +1,43 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/config_fwd.hpp +/// Forward declarations for engine/config.hpp + +#if !defined(ENGINE_CONFIG_FWD_HPP) +#define ENGINE_CONFIG_FWD_HPP + +namespace engine { + + +class user_node; + + +} // namespace engine + +#endif // !defined(ENGINE_CONFIG_FWD_HPP) diff --git a/engine/config_test.cpp b/engine/config_test.cpp new file mode 100644 index 000000000000..e4eb27421078 --- /dev/null +++ b/engine/config_test.cpp @@ -0,0 +1,203 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/config.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include +#include + +#include + +#include "engine/exceptions.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/config/tree.ipp" +#include "utils/passwd.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Replaces the system user database with a fake one for testing purposes. +static void +set_mock_users(void) +{ + std::vector< passwd::user > users; + users.push_back(passwd::user("user1", 100, 150)); + users.push_back(passwd::user("user2", 200, 250)); + passwd::set_mock_users_for_testing(users); +} + + +/// Checks that the default values of a config object match our expectations. +/// +/// This fails the test case if any field of the input config object is not +/// what we expect. +/// +/// \param config The configuration to validate. +static void +validate_defaults(const config::tree& config) +{ + ATF_REQUIRE_EQ( + KYUA_ARCHITECTURE, + config.lookup< config::string_node >("architecture")); + + ATF_REQUIRE_EQ( + 1, + config.lookup< config::positive_int_node >("parallelism")); + + ATF_REQUIRE_EQ( + KYUA_PLATFORM, + config.lookup< config::string_node >("platform")); + + ATF_REQUIRE(!config.is_set("unprivileged_user")); + + ATF_REQUIRE(config.all_properties("test_suites").empty()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(config__defaults); +ATF_TEST_CASE_BODY(config__defaults) +{ + const config::tree user_config = engine::default_config(); + validate_defaults(user_config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__set__parallelism); +ATF_TEST_CASE_BODY(config__set__parallelism) +{ + config::tree user_config = engine::default_config(); + user_config.set_string("parallelism", "8"); + ATF_REQUIRE_THROW_RE( + config::error, "parallelism.*Must be a positive integer", + user_config.set_string("parallelism", "0")); + ATF_REQUIRE_THROW_RE( + config::error, "parallelism.*Must be a positive integer", + user_config.set_string("parallelism", "-1")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__defaults); +ATF_TEST_CASE_BODY(config__load__defaults) +{ + atf::utils::create_file("config", "syntax(2)\n"); + + const config::tree user_config = engine::load_config(fs::path("config")); + validate_defaults(user_config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__overrides); +ATF_TEST_CASE_BODY(config__load__overrides) +{ + set_mock_users(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "architecture = 'test-architecture'\n" + "parallelism = 16\n" + "platform = 'test-platform'\n" + "unprivileged_user = 'user2'\n" + "test_suites.mysuite.myvar = 'myvalue'\n"); + + const config::tree user_config = engine::load_config(fs::path("config")); + + ATF_REQUIRE_EQ("test-architecture", + user_config.lookup_string("architecture")); + ATF_REQUIRE_EQ("16", + user_config.lookup_string("parallelism")); + ATF_REQUIRE_EQ("test-platform", + user_config.lookup_string("platform")); + + const passwd::user& user = user_config.lookup< engine::user_node >( + "unprivileged_user"); + ATF_REQUIRE_EQ("user2", user.name); + ATF_REQUIRE_EQ(200, user.uid); + + config::properties_map exp_test_suites; + exp_test_suites["test_suites.mysuite.myvar"] = "myvalue"; + + ATF_REQUIRE(exp_test_suites == user_config.all_properties("test_suites")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__lua_error); +ATF_TEST_CASE_BODY(config__load__lua_error) +{ + atf::utils::create_file("config", "this syntax is invalid\n"); + + ATF_REQUIRE_THROW(engine::load_error, engine::load_config( + fs::path("config"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__bad_syntax__version); +ATF_TEST_CASE_BODY(config__load__bad_syntax__version) +{ + atf::utils::create_file("config", "syntax(123)\n"); + + ATF_REQUIRE_THROW_RE(engine::load_error, + "Unsupported config version 123", + engine::load_config(fs::path("config"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(config__load__missing_file); +ATF_TEST_CASE_BODY(config__load__missing_file) +{ + ATF_REQUIRE_THROW_RE(engine::load_error, "Load of 'missing' failed", + engine::load_config(fs::path("missing"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, config__defaults); + ATF_ADD_TEST_CASE(tcs, config__set__parallelism); + ATF_ADD_TEST_CASE(tcs, config__load__defaults); + ATF_ADD_TEST_CASE(tcs, config__load__overrides); + ATF_ADD_TEST_CASE(tcs, config__load__lua_error); + ATF_ADD_TEST_CASE(tcs, config__load__bad_syntax__version); + ATF_ADD_TEST_CASE(tcs, config__load__missing_file); +} diff --git a/engine/exceptions.cpp b/engine/exceptions.cpp new file mode 100644 index 000000000000..98a7b43a7de3 --- /dev/null +++ b/engine/exceptions.cpp @@ -0,0 +1,81 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/exceptions.hpp" + +#include "utils/format/macros.hpp" + +namespace fs = utils::fs; + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +engine::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +engine::error::~error(void) throw() +{ +} + + +/// Constructs a new format_error. +/// +/// \param reason_ Description of the format problem. +engine::format_error::format_error(const std::string& reason_) : + error(reason_) +{ +} + + +/// Destructor for the error. +engine::format_error::~format_error(void) throw() +{ +} + + +/// Constructs a new load_error. +/// +/// \param file_ The file in which the error was encountered. +/// \param reason_ Description of the load problem. +engine::load_error::load_error(const fs::path& file_, + const std::string& reason_) : + error(F("Load of '%s' failed: %s") % file_ % reason_), + file(file_), + reason(reason_) +{ +} + + +/// Destructor for the error. +engine::load_error::~load_error(void) throw() +{ +} diff --git a/engine/exceptions.hpp b/engine/exceptions.hpp new file mode 100644 index 000000000000..fccb04f1aff2 --- /dev/null +++ b/engine/exceptions.hpp @@ -0,0 +1,75 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/exceptions.hpp +/// Exception types raised by the engine module. + +#if !defined(ENGINE_EXCEPTIONS_HPP) +#define ENGINE_EXCEPTIONS_HPP + +#include + +#include "utils/fs/path.hpp" + +namespace engine { + + +/// Base exception for engine errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// Error while processing data. +class format_error : public error { +public: + explicit format_error(const std::string&); + virtual ~format_error(void) throw(); +}; + + +/// Error while parsing external data. +class load_error : public error { +public: + /// The path to the file that caused the load error. + utils::fs::path file; + + /// The reason for the error; may not include the file name. + std::string reason; + + explicit load_error(const utils::fs::path&, const std::string&); + virtual ~load_error(void) throw(); +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_EXCEPTIONS_HPP) diff --git a/engine/exceptions_test.cpp b/engine/exceptions_test.cpp new file mode 100644 index 000000000000..16e7c9f33d16 --- /dev/null +++ b/engine/exceptions_test.cpp @@ -0,0 +1,69 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/exceptions.hpp" + +#include + +#include + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const engine::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_error); +ATF_TEST_CASE_BODY(format_error) +{ + const engine::format_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_error); +ATF_TEST_CASE_BODY(load_error) +{ + const engine::load_error e(fs::path("/my/file"), "foo"); + ATF_REQUIRE_EQ(fs::path("/my/file"), e.file); + ATF_REQUIRE_EQ("foo", e.reason); + ATF_REQUIRE(std::strcmp("Load of '/my/file' failed: foo", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, format_error); + ATF_ADD_TEST_CASE(tcs, load_error); +} diff --git a/engine/filters.cpp b/engine/filters.cpp new file mode 100644 index 000000000000..753e64ae05f8 --- /dev/null +++ b/engine/filters.cpp @@ -0,0 +1,389 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/filters.hpp" + +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + +using utils::none; +using utils::optional; + + +/// Constructs a filter. +/// +/// \param test_program_ The name of the test program or of the subdirectory to +/// match. +/// \param test_case_ The name of the test case to match. +engine::test_filter::test_filter(const fs::path& test_program_, + const std::string& test_case_) : + test_program(test_program_), + test_case(test_case_) +{ +} + + +/// Parses a user-provided test filter. +/// +/// \param str The user-provided string representing a filter for tests. Must +/// be of the form <test_program%gt;[:<test_case%gt;]. +/// +/// \return The parsed filter. +/// +/// \throw std::runtime_error If the provided filter is invalid. +engine::test_filter +engine::test_filter::parse(const std::string& str) +{ + if (str.empty()) + throw std::runtime_error("Test filter cannot be empty"); + + const std::string::size_type pos = str.find(':'); + if (pos == 0) + throw std::runtime_error(F("Program name component in '%s' is empty") + % str); + if (pos == str.length() - 1) + throw std::runtime_error(F("Test case component in '%s' is empty") + % str); + + try { + const fs::path test_program_(str.substr(0, pos)); + if (test_program_.is_absolute()) + throw std::runtime_error(F("Program name '%s' must be relative " + "to the test suite, not absolute") % + test_program_.str()); + if (pos == std::string::npos) { + LD(F("Parsed user filter '%s': test program '%s', no test case") % + str % test_program_.str()); + return test_filter(test_program_, ""); + } else { + const std::string test_case_(str.substr(pos + 1)); + LD(F("Parsed user filter '%s': test program '%s', test case '%s'") % + str % test_program_.str() % test_case_); + return test_filter(test_program_, test_case_); + } + } catch (const fs::error& e) { + throw std::runtime_error(F("Invalid path in filter '%s': %s") % str % + e.what()); + } +} + + +/// Formats a filter for user presentation. +/// +/// \return A user-friendly string representing the filter. Note that this does +/// not necessarily match the string the user provided: in particular, the path +/// may have been internally normalized. +std::string +engine::test_filter::str(void) const +{ + if (!test_case.empty()) + return F("%s:%s") % test_program % test_case; + else + return test_program.str(); +} + + +/// Checks if this filter contains another. +/// +/// \param other The filter to compare to. +/// +/// \return True if this filter contains the other filter or if they are equal. +bool +engine::test_filter::contains(const test_filter& other) const +{ + if (*this == other) + return true; + else + return test_case.empty() && test_program.is_parent_of( + other.test_program); +} + + +/// Checks if this filter matches a given test program name or subdirectory. +/// +/// \param test_program_ The test program to compare to. +/// +/// \return Whether the filter matches the test program. This is a superset of +/// matches_test_case. +bool +engine::test_filter::matches_test_program(const fs::path& test_program_) const +{ + if (test_program == test_program_) + return true; + else { + // Check if the filter matches a subdirectory of the test program. + // The test case must be empty because we don't want foo:bar to match + // foo/baz. + return (test_case.empty() && test_program.is_parent_of(test_program_)); + } +} + + +/// Checks if this filter matches a given test case identifier. +/// +/// \param test_program_ The test program to compare to. +/// \param test_case_ The test case to compare to. +/// +/// \return Whether the filter matches the test case. +bool +engine::test_filter::matches_test_case(const fs::path& test_program_, + const std::string& test_case_) const +{ + if (matches_test_program(test_program_)) { + return test_case.empty() || test_case == test_case_; + } else + return false; +} + + +/// Less-than comparison for sorting purposes. +/// +/// \param other The filter to compare to. +/// +/// \return True if this filter sorts before the other filter. +bool +engine::test_filter::operator<(const test_filter& other) const +{ + return ( + test_program < other.test_program || + (test_program == other.test_program && test_case < other.test_case)); +} + + +/// Equality comparison. +/// +/// \param other The filter to compare to. +/// +/// \return True if this filter is equal to the other filter. +bool +engine::test_filter::operator==(const test_filter& other) const +{ + return test_program == other.test_program && test_case == other.test_case; +} + + +/// Non-equality comparison. +/// +/// \param other The filter to compare to. +/// +/// \return True if this filter is different than the other filter. +bool +engine::test_filter::operator!=(const test_filter& other) const +{ + return !(*this == other); +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +engine::operator<<(std::ostream& output, const test_filter& object) +{ + if (object.test_case.empty()) { + output << F("test_filter{test_program=%s}") % object.test_program; + } else { + output << F("test_filter{test_program=%s, test_case=%s}") + % object.test_program % object.test_case; + } + return output; +} + + +/// Constructs a new set of filters. +/// +/// \param filters_ The filters themselves; if empty, no filters are applied. +engine::test_filters::test_filters(const std::set< test_filter >& filters_) : + _filters(filters_) +{ +} + + +/// Checks if a given test program matches the set of filters. +/// +/// This is provided as an optimization only, and the results of this function +/// are less specific than those of match_test_case. Checking for the matching +/// of a test program should be done before loading the list of test cases from +/// a program, so as to avoid the delay in executing the test program, but +/// match_test_case must still be called afterwards. +/// +/// \param name The test program to check against the filters. +/// +/// \return True if the provided identifier matches any filter. +bool +engine::test_filters::match_test_program(const fs::path& name) const +{ + if (_filters.empty()) + return true; + + bool matches = false; + for (std::set< test_filter >::const_iterator iter = _filters.begin(); + !matches && iter != _filters.end(); iter++) { + matches = (*iter).matches_test_program(name); + } + return matches; +} + + +/// Checks if a given test case identifier matches the set of filters. +/// +/// \param test_program The test program to check against the filters. +/// \param test_case The test case to check against the filters. +/// +/// \return A boolean indicating if the test case is matched by any filter and, +/// if true, a string containing the filter name. The string is empty when +/// there are no filters defined. +engine::test_filters::match +engine::test_filters::match_test_case(const fs::path& test_program, + const std::string& test_case) const +{ + if (_filters.empty()) { + INV(match_test_program(test_program)); + return match(true, none); + } + + optional< test_filter > found = none; + for (std::set< test_filter >::const_iterator iter = _filters.begin(); + !found && iter != _filters.end(); iter++) { + if ((*iter).matches_test_case(test_program, test_case)) + found = *iter; + } + INV(!found || match_test_program(test_program)); + return match(static_cast< bool >(found), found); +} + + +/// Calculates the filters that have not matched any tests. +/// +/// \param matched The filters that did match some tests. This must be a subset +/// of the filters held by this object. +/// +/// \return The set of filters that have not been used. +std::set< engine::test_filter > +engine::test_filters::difference(const std::set< test_filter >& matched) const +{ + PRE(std::includes(_filters.begin(), _filters.end(), + matched.begin(), matched.end())); + + std::set< test_filter > filters; + std::set_difference(_filters.begin(), _filters.end(), + matched.begin(), matched.end(), + std::inserter(filters, filters.begin())); + return filters; +} + + +/// Checks if a collection of filters is disjoint. +/// +/// \param filters The filters to check. +/// +/// \throw std::runtime_error If the filters are not disjoint. +void +engine::check_disjoint_filters(const std::set< engine::test_filter >& filters) +{ + // Yes, this is an O(n^2) algorithm. However, we can assume that the number + // of test filters (which are provided by the user on the command line) on a + // particular run is in the order of tens, and thus this should not cause + // any serious performance trouble. + for (std::set< test_filter >::const_iterator i1 = filters.begin(); + i1 != filters.end(); i1++) { + for (std::set< test_filter >::const_iterator i2 = filters.begin(); + i2 != filters.end(); i2++) { + const test_filter& filter1 = *i1; + const test_filter& filter2 = *i2; + + if (i1 != i2 && filter1.contains(filter2)) { + throw std::runtime_error( + F("Filters '%s' and '%s' are not disjoint") % + filter1.str() % filter2.str()); + } + } + } +} + + +/// Constructs a filters_state instance. +/// +/// \param filters_ The set of filters to track. +engine::filters_state::filters_state( + const std::set< engine::test_filter >& filters_) : + _filters(test_filters(filters_)) +{ +} + + +/// Checks whether these filters match the given test program. +/// +/// \param test_program The test program to match against. +/// +/// \return True if these filters match the given test program name. +bool +engine::filters_state::match_test_program(const fs::path& test_program) const +{ + return _filters.match_test_program(test_program); +} + + +/// Checks whether these filters match the given test case. +/// +/// \param test_program The test program to match against. +/// \param test_case The test case to match against. +/// +/// \return True if these filters match the given test case identifier. +bool +engine::filters_state::match_test_case(const fs::path& test_program, + const std::string& test_case) +{ + engine::test_filters::match match = _filters.match_test_case( + test_program, test_case); + if (match.first && match.second) + _used_filters.insert(match.second.get()); + return match.first; +} + + +/// Calculates the unused filters in this set. +/// +/// \return Returns the set of filters that have not matched any tests. This +/// information is useful to report usage errors to the user. +std::set< engine::test_filter > +engine::filters_state::unused(void) const +{ + return _filters.difference(_used_filters); +} diff --git a/engine/filters.hpp b/engine/filters.hpp new file mode 100644 index 000000000000..91a667c3b46b --- /dev/null +++ b/engine/filters.hpp @@ -0,0 +1,134 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/filters.hpp +/// Representation and manipulation of filters for test cases. +/// +/// All the filter classes in this module are supposed to be purely functional: +/// they are mere filters that decide whether they match or not the input data +/// fed to them. User-interface filter manipulation must go somewhere else. + +#if !defined(ENGINE_FILTERS_HPP) +#define ENGINE_FILTERS_HPP + +#include "engine/filters_fwd.hpp" + +#include +#include +#include +#include + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + + +namespace engine { + + +/// Filter for test cases. +/// +/// A filter is one of: the name of a directory containing test cases, the name +/// of a test program, or the name of a test program plus the name of a test +/// case. +class test_filter { +public: + /// The name of the test program or subdirectory to match. + utils::fs::path test_program; + + /// The name of the test case to match; if empty, represents any test case. + std::string test_case; + + test_filter(const utils::fs::path&, const std::string&); + static test_filter parse(const std::string&); + + std::string str(void) const; + + bool contains(const test_filter&) const; + bool matches_test_program(const utils::fs::path&) const; + bool matches_test_case(const utils::fs::path&, const std::string&) const; + + bool operator<(const test_filter&) const; + bool operator==(const test_filter&) const; + bool operator!=(const test_filter&) const; +}; + + +std::ostream& operator<<(std::ostream&, const test_filter&); + + +/// Collection of user-provided filters to select test cases. +/// +/// An empty collection of filters is considered to match any test case. +/// +/// In general, the filters maintained by this class should be disjoint. If +/// they are not, some filters may never have a chance to do a match, which is +/// most likely the fault of the user. To check for non-disjoint filters before +/// constructing this object, use check_disjoint_filters. +class test_filters { + /// The user-provided filters. + std::set< test_filter > _filters; + +public: + explicit test_filters(const std::set< test_filter >&); + + /// Return type of match_test_case. Indicates whether the filters have + /// matched a particular test case and, if they have, which filter did the + /// match (if any). + typedef std::pair< bool, utils::optional< test_filter > > match; + + bool match_test_program(const utils::fs::path&) const; + match match_test_case(const utils::fs::path&, const std::string&) const; + + std::set< test_filter > difference(const std::set< test_filter >&) const; +}; + + +void check_disjoint_filters(const std::set< test_filter >&); + + +/// Tracks state of the filters that have matched tests during execution. +class filters_state { + /// The user-provided filters. + test_filters _filters; + + /// Collection of filters that have matched test cases so far. + std::set< test_filter > _used_filters; + +public: + explicit filters_state(const std::set< test_filter >&); + + bool match_test_program(const utils::fs::path&) const; + bool match_test_case(const utils::fs::path&, const std::string&); + + std::set< test_filter > unused(void) const; +}; + + +} // namespace engine + +#endif // !defined(ENGINE_FILTERS_HPP) diff --git a/engine/filters_fwd.hpp b/engine/filters_fwd.hpp new file mode 100644 index 000000000000..ee5d0c692ff5 --- /dev/null +++ b/engine/filters_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/filters_fwd.hpp +/// Forward declarations for engine/filters.hpp + +#if !defined(ENGINE_FILTERS_FWD_HPP) +#define ENGINE_FILTERS_FWD_HPP + +namespace engine { + + +class filters_state; +class test_filter; +class test_filters; + + +} // namespace engine + +#endif // !defined(ENGINE_FILTERS_FWD_HPP) diff --git a/engine/filters_test.cpp b/engine/filters_test.cpp new file mode 100644 index 000000000000..081755b2553f --- /dev/null +++ b/engine/filters_test.cpp @@ -0,0 +1,594 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/filters.hpp" + +#include + +#include + +namespace fs = utils::fs; + + +namespace { + + +/// Syntactic sugar to instantiate engine::test_filter objects. +/// +/// \param test_program Test program. +/// \param test_case Test case. +/// +/// \return A \p test_filter object, based on \p test_program and \p test_case. +inline engine::test_filter +mkfilter(const char* test_program, const char* test_case) +{ + return engine::test_filter(fs::path(test_program), test_case); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__public_fields); +ATF_TEST_CASE_BODY(test_filter__public_fields) +{ + const engine::test_filter filter(fs::path("foo/bar"), "baz"); + ATF_REQUIRE_EQ(fs::path("foo/bar"), filter.test_program); + ATF_REQUIRE_EQ("baz", filter.test_case); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__ok); +ATF_TEST_CASE_BODY(test_filter__parse__ok) +{ + const engine::test_filter filter(engine::test_filter::parse("foo")); + ATF_REQUIRE_EQ(fs::path("foo"), filter.test_program); + ATF_REQUIRE(filter.test_case.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__empty); +ATF_TEST_CASE_BODY(test_filter__parse__empty) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "empty", + engine::test_filter::parse("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__absolute); +ATF_TEST_CASE_BODY(test_filter__parse__absolute) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "'/foo/bar'.*relative", + engine::test_filter::parse("/foo//bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__bad_program_name); +ATF_TEST_CASE_BODY(test_filter__parse__bad_program_name) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "Program name.*':foo'", + engine::test_filter::parse(":foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__bad_test_case); +ATF_TEST_CASE_BODY(test_filter__parse__bad_test_case) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "Test case.*'bar/baz:'", + engine::test_filter::parse("bar/baz:")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__parse__bad_path); +ATF_TEST_CASE_BODY(test_filter__parse__bad_path) +{ + // TODO(jmmv): Not implemented. At the moment, the only reason for a path + // to be invalid is if it is empty... but we are checking this exact + // condition ourselves as part of the input validation. So we can't mock in + // an argument with an invalid non-empty path... +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__str); +ATF_TEST_CASE_BODY(test_filter__str) +{ + const engine::test_filter filter(fs::path("foo/bar"), "baz"); + ATF_REQUIRE_EQ("foo/bar:baz", filter.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__contains__same); +ATF_TEST_CASE_BODY(test_filter__contains__same) +{ + { + const engine::test_filter f(fs::path("foo/bar"), "baz"); + ATF_REQUIRE(f.contains(f)); + } + { + const engine::test_filter f(fs::path("foo/bar"), ""); + ATF_REQUIRE(f.contains(f)); + } + { + const engine::test_filter f(fs::path("foo"), ""); + ATF_REQUIRE(f.contains(f)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__contains__different); +ATF_TEST_CASE_BODY(test_filter__contains__different) +{ + { + const engine::test_filter f1(fs::path("foo"), ""); + const engine::test_filter f2(fs::path("foo"), "bar"); + ATF_REQUIRE( f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } + { + const engine::test_filter f1(fs::path("foo/bar"), ""); + const engine::test_filter f2(fs::path("foo/bar"), "baz"); + ATF_REQUIRE( f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } + { + const engine::test_filter f1(fs::path("foo/bar"), ""); + const engine::test_filter f2(fs::path("foo/baz"), ""); + ATF_REQUIRE(!f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } + { + const engine::test_filter f1(fs::path("foo"), ""); + const engine::test_filter f2(fs::path("foo/bar"), ""); + ATF_REQUIRE( f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } + { + const engine::test_filter f1(fs::path("foo"), "bar"); + const engine::test_filter f2(fs::path("foo/bar"), ""); + ATF_REQUIRE(!f1.contains(f2)); + ATF_REQUIRE(!f2.contains(f1)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__matches_test_program) +ATF_TEST_CASE_BODY(test_filter__matches_test_program) +{ + { + const engine::test_filter f(fs::path("top"), "unused"); + ATF_REQUIRE( f.matches_test_program(fs::path("top"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("top2"))); + } + + { + const engine::test_filter f(fs::path("dir1/dir2"), ""); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2/foo"))); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2/bar"))); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2/bar/baz"))); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir2/bar/baz"))); + } + + { + const engine::test_filter f(fs::path("dir1/dir2"), "unused"); + ATF_REQUIRE( f.matches_test_program(fs::path("dir1/dir2"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/dir2/foo"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/dir2/bar"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/dir2/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/dir2/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir1/bar/baz"))); + ATF_REQUIRE(!f.matches_test_program(fs::path("dir2/bar/baz"))); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__matches_test_case) +ATF_TEST_CASE_BODY(test_filter__matches_test_case) +{ + { + const engine::test_filter f(fs::path("top"), "foo"); + ATF_REQUIRE( f.matches_test_case(fs::path("top"), "foo")); + ATF_REQUIRE(!f.matches_test_case(fs::path("top"), "bar")); + } + + { + const engine::test_filter f(fs::path("top"), ""); + ATF_REQUIRE( f.matches_test_case(fs::path("top"), "foo")); + ATF_REQUIRE( f.matches_test_case(fs::path("top"), "bar")); + ATF_REQUIRE(!f.matches_test_case(fs::path("top2"), "foo")); + } + + { + const engine::test_filter f(fs::path("d1/d2/prog"), "t1"); + ATF_REQUIRE( f.matches_test_case(fs::path("d1/d2/prog"), "t1")); + ATF_REQUIRE(!f.matches_test_case(fs::path("d1/d2/prog"), "t2")); + } + + { + const engine::test_filter f(fs::path("d1/d2"), ""); + ATF_REQUIRE( f.matches_test_case(fs::path("d1/d2/prog"), "t1")); + ATF_REQUIRE( f.matches_test_case(fs::path("d1/d2/prog"), "t2")); + ATF_REQUIRE( f.matches_test_case(fs::path("d1/d2/prog2"), "t2")); + ATF_REQUIRE(!f.matches_test_case(fs::path("d1/d3"), "foo")); + ATF_REQUIRE(!f.matches_test_case(fs::path("d2"), "foo")); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__operator_lt) +ATF_TEST_CASE_BODY(test_filter__operator_lt) +{ + { + const engine::test_filter f1(fs::path("d1/d2"), ""); + ATF_REQUIRE(!(f1 < f1)); + } + { + const engine::test_filter f1(fs::path("d1/d2"), ""); + const engine::test_filter f2(fs::path("d1/d3"), ""); + ATF_REQUIRE( (f1 < f2)); + ATF_REQUIRE(!(f2 < f1)); + } + { + const engine::test_filter f1(fs::path("d1/d2"), ""); + const engine::test_filter f2(fs::path("d1/d2"), "foo"); + ATF_REQUIRE( (f1 < f2)); + ATF_REQUIRE(!(f2 < f1)); + } + { + const engine::test_filter f1(fs::path("d1/d2"), "bar"); + const engine::test_filter f2(fs::path("d1/d2"), "foo"); + ATF_REQUIRE( (f1 < f2)); + ATF_REQUIRE(!(f2 < f1)); + } + { + const engine::test_filter f1(fs::path("d1/d2"), "bar"); + const engine::test_filter f2(fs::path("d1/d3"), ""); + ATF_REQUIRE( (f1 < f2)); + ATF_REQUIRE(!(f2 < f1)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__operator_eq) +ATF_TEST_CASE_BODY(test_filter__operator_eq) +{ + const engine::test_filter f1(fs::path("d1/d2"), ""); + const engine::test_filter f2(fs::path("d1/d2"), "bar"); + ATF_REQUIRE( (f1 == f1)); + ATF_REQUIRE(!(f1 == f2)); + ATF_REQUIRE(!(f2 == f1)); + ATF_REQUIRE( (f2 == f2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__operator_ne) +ATF_TEST_CASE_BODY(test_filter__operator_ne) +{ + const engine::test_filter f1(fs::path("d1/d2"), ""); + const engine::test_filter f2(fs::path("d1/d2"), "bar"); + ATF_REQUIRE(!(f1 != f1)); + ATF_REQUIRE( (f1 != f2)); + ATF_REQUIRE( (f2 != f1)); + ATF_REQUIRE(!(f2 != f2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filter__output); +ATF_TEST_CASE_BODY(test_filter__output) +{ + { + std::ostringstream str; + str << engine::test_filter(fs::path("d1/d2"), ""); + ATF_REQUIRE_EQ( + "test_filter{test_program=d1/d2}", + str.str()); + } + { + std::ostringstream str; + str << engine::test_filter(fs::path("d1/d2"), "bar"); + ATF_REQUIRE_EQ( + "test_filter{test_program=d1/d2, test_case=bar}", + str.str()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__match_test_case__no_filters) +ATF_TEST_CASE_BODY(test_filters__match_test_case__no_filters) +{ + const std::set< engine::test_filter > raw_filters; + + const engine::test_filters filters(raw_filters); + engine::test_filters::match match; + + match = filters.match_test_case(fs::path("foo"), "baz"); + ATF_REQUIRE(match.first); + ATF_REQUIRE(!match.second); + + match = filters.match_test_case(fs::path("foo/bar"), "baz"); + ATF_REQUIRE(match.first); + ATF_REQUIRE(!match.second); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__match_test_case__some_filters) +ATF_TEST_CASE_BODY(test_filters__match_test_case__some_filters) +{ + std::set< engine::test_filter > raw_filters; + raw_filters.insert(mkfilter("top_test", "")); + raw_filters.insert(mkfilter("subdir_1", "")); + raw_filters.insert(mkfilter("subdir_2/a_test", "")); + raw_filters.insert(mkfilter("subdir_2/b_test", "foo")); + + const engine::test_filters filters(raw_filters); + engine::test_filters::match match; + + match = filters.match_test_case(fs::path("top_test"), "a"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("top_test", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_1/foo"), "a"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("subdir_1", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_1/bar"), "z"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("subdir_1", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_2/a_test"), "bar"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("subdir_2/a_test", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_2/b_test"), "foo"); + ATF_REQUIRE(match.first); + ATF_REQUIRE_EQ("subdir_2/b_test:foo", match.second.get().str()); + + match = filters.match_test_case(fs::path("subdir_2/b_test"), "bar"); + ATF_REQUIRE(!match.first); + + match = filters.match_test_case(fs::path("subdir_2/c_test"), "foo"); + ATF_REQUIRE(!match.first); + + match = filters.match_test_case(fs::path("subdir_3"), "hello"); + ATF_REQUIRE(!match.first); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__match_test_program__no_filters) +ATF_TEST_CASE_BODY(test_filters__match_test_program__no_filters) +{ + const std::set< engine::test_filter > raw_filters; + + const engine::test_filters filters(raw_filters); + ATF_REQUIRE(filters.match_test_program(fs::path("foo"))); + ATF_REQUIRE(filters.match_test_program(fs::path("foo/bar"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__match_test_program__some_filters) +ATF_TEST_CASE_BODY(test_filters__match_test_program__some_filters) +{ + std::set< engine::test_filter > raw_filters; + raw_filters.insert(mkfilter("top_test", "")); + raw_filters.insert(mkfilter("subdir_1", "")); + raw_filters.insert(mkfilter("subdir_2/a_test", "")); + raw_filters.insert(mkfilter("subdir_2/b_test", "foo")); + + const engine::test_filters filters(raw_filters); + ATF_REQUIRE( filters.match_test_program(fs::path("top_test"))); + ATF_REQUIRE( filters.match_test_program(fs::path("subdir_1/foo"))); + ATF_REQUIRE( filters.match_test_program(fs::path("subdir_1/bar"))); + ATF_REQUIRE( filters.match_test_program(fs::path("subdir_2/a_test"))); + ATF_REQUIRE( filters.match_test_program(fs::path("subdir_2/b_test"))); + ATF_REQUIRE(!filters.match_test_program(fs::path("subdir_2/c_test"))); + ATF_REQUIRE(!filters.match_test_program(fs::path("subdir_3"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__difference__no_filters); +ATF_TEST_CASE_BODY(test_filters__difference__no_filters) +{ + const std::set< engine::test_filter > in_filters; + const std::set< engine::test_filter > used; + const std::set< engine::test_filter > diff = engine::test_filters( + in_filters).difference(used); + ATF_REQUIRE(diff.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__difference__some_filters__all_used); +ATF_TEST_CASE_BODY(test_filters__difference__some_filters__all_used) +{ + std::set< engine::test_filter > in_filters; + in_filters.insert(mkfilter("a", "")); + in_filters.insert(mkfilter("b", "c")); + + const std::set< engine::test_filter > used = in_filters; + + const std::set< engine::test_filter > diff = engine::test_filters( + in_filters).difference(used); + ATF_REQUIRE(diff.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_filters__difference__some_filters__some_unused); +ATF_TEST_CASE_BODY(test_filters__difference__some_filters__some_unused) +{ + std::set< engine::test_filter > in_filters; + in_filters.insert(mkfilter("a", "")); + in_filters.insert(mkfilter("b", "c")); + in_filters.insert(mkfilter("d", "")); + in_filters.insert(mkfilter("e", "f")); + + std::set< engine::test_filter > used; + used.insert(mkfilter("b", "c")); + used.insert(mkfilter("d", "")); + + const std::set< engine::test_filter > diff = engine::test_filters( + in_filters).difference(used); + ATF_REQUIRE_EQ(2, diff.size()); + ATF_REQUIRE(diff.find(mkfilter("a", "")) != diff.end()); + ATF_REQUIRE(diff.find(mkfilter("e", "f")) != diff.end()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_disjoint_filters__ok); +ATF_TEST_CASE_BODY(check_disjoint_filters__ok) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("a", "")); + filters.insert(mkfilter("b", "")); + filters.insert(mkfilter("c", "a")); + filters.insert(mkfilter("c", "b")); + + engine::check_disjoint_filters(filters); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_disjoint_filters__fail); +ATF_TEST_CASE_BODY(check_disjoint_filters__fail) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("a", "")); + filters.insert(mkfilter("b", "")); + filters.insert(mkfilter("c", "a")); + filters.insert(mkfilter("d", "b")); + filters.insert(mkfilter("c", "")); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "'c'.*'c:a'.*not disjoint", + engine::check_disjoint_filters(filters)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filters_state__match_test_program); +ATF_TEST_CASE_BODY(filters_state__match_test_program) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("foo/bar", "")); + filters.insert(mkfilter("baz", "tc")); + engine::filters_state state(filters); + + ATF_REQUIRE(state.match_test_program(fs::path("foo/bar/something"))); + ATF_REQUIRE(state.match_test_program(fs::path("baz"))); + + ATF_REQUIRE(!state.match_test_program(fs::path("foo/baz"))); + ATF_REQUIRE(!state.match_test_program(fs::path("hello"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filters_state__match_test_case); +ATF_TEST_CASE_BODY(filters_state__match_test_case) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("foo/bar", "")); + filters.insert(mkfilter("baz", "tc")); + engine::filters_state state(filters); + + ATF_REQUIRE(state.match_test_case(fs::path("foo/bar/something"), "any")); + ATF_REQUIRE(state.match_test_case(fs::path("baz"), "tc")); + + ATF_REQUIRE(!state.match_test_case(fs::path("foo/baz/something"), "tc")); + ATF_REQUIRE(!state.match_test_case(fs::path("baz"), "tc2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filters_state__unused__none); +ATF_TEST_CASE_BODY(filters_state__unused__none) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("a/b", "")); + filters.insert(mkfilter("baz", "tc")); + filters.insert(mkfilter("hey/d", "yes")); + engine::filters_state state(filters); + + state.match_test_case(fs::path("a/b/c"), "any"); + state.match_test_case(fs::path("baz"), "tc"); + state.match_test_case(fs::path("hey/d"), "yes"); + + ATF_REQUIRE(state.unused().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filters_state__unused__some); +ATF_TEST_CASE_BODY(filters_state__unused__some) +{ + std::set< engine::test_filter > filters; + filters.insert(mkfilter("a/b", "")); + filters.insert(mkfilter("baz", "tc")); + filters.insert(mkfilter("hey/d", "yes")); + engine::filters_state state(filters); + + state.match_test_program(fs::path("a/b/c")); + state.match_test_case(fs::path("baz"), "tc"); + + std::set< engine::test_filter > exp_unused; + exp_unused.insert(mkfilter("a/b", "")); + exp_unused.insert(mkfilter("hey/d", "yes")); + + ATF_REQUIRE(exp_unused == state.unused()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, test_filter__public_fields); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__ok); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__empty); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__absolute); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__bad_program_name); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__bad_test_case); + ATF_ADD_TEST_CASE(tcs, test_filter__parse__bad_path); + ATF_ADD_TEST_CASE(tcs, test_filter__str); + ATF_ADD_TEST_CASE(tcs, test_filter__contains__same); + ATF_ADD_TEST_CASE(tcs, test_filter__contains__different); + ATF_ADD_TEST_CASE(tcs, test_filter__matches_test_program); + ATF_ADD_TEST_CASE(tcs, test_filter__matches_test_case); + ATF_ADD_TEST_CASE(tcs, test_filter__operator_lt); + ATF_ADD_TEST_CASE(tcs, test_filter__operator_eq); + ATF_ADD_TEST_CASE(tcs, test_filter__operator_ne); + ATF_ADD_TEST_CASE(tcs, test_filter__output); + + ATF_ADD_TEST_CASE(tcs, test_filters__match_test_case__no_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__match_test_case__some_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__match_test_program__no_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__match_test_program__some_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__difference__no_filters); + ATF_ADD_TEST_CASE(tcs, test_filters__difference__some_filters__all_used); + ATF_ADD_TEST_CASE(tcs, test_filters__difference__some_filters__some_unused); + + ATF_ADD_TEST_CASE(tcs, check_disjoint_filters__ok); + ATF_ADD_TEST_CASE(tcs, check_disjoint_filters__fail); + + ATF_ADD_TEST_CASE(tcs, filters_state__match_test_program); + ATF_ADD_TEST_CASE(tcs, filters_state__match_test_case); + ATF_ADD_TEST_CASE(tcs, filters_state__unused__none); + ATF_ADD_TEST_CASE(tcs, filters_state__unused__some); +} diff --git a/engine/kyuafile.cpp b/engine/kyuafile.cpp new file mode 100644 index 000000000000..4dca3193832b --- /dev/null +++ b/engine/kyuafile.cpp @@ -0,0 +1,694 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/kyuafile.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +#include "engine/exceptions.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/config/exceptions.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/lua_module.hpp" +#include "utils/fs/operations.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; +using utils::optional; + + +// History of Kyuafile file versions: +// +// 3 - DOES NOT YET EXIST. Pending changes for when this is introduced: +// +// * Revisit what to do about the test_suite definition. Support for +// per-test program overrides is deprecated and should be removed. +// But, maybe, the whole test_suite definition idea is wrong and we +// should instead be explicitly telling which configuration variables +// to "inject" into each test program. +// +// 2 - Changed the syntax() call to take only a version number, instead of the +// word 'config' as the first argument and the version as the second one. +// Files now start with syntax(2) instead of syntax('kyuafile', 1). +// +// 1 - Initial version. + + +namespace { + + +static int lua_current_kyuafile(lutok::state&); +static int lua_generic_test_program(lutok::state&); +static int lua_include(lutok::state&); +static int lua_syntax(lutok::state&); +static int lua_test_suite(lutok::state&); + + +/// Concatenates two paths while avoiding paths to start with './'. +/// +/// \param root Path to the directory containing the file. +/// \param file Path to concatenate to root. Cannot be absolute. +/// +/// \return The concatenated path. +static fs::path +relativize(const fs::path& root, const fs::path& file) +{ + PRE(!file.is_absolute()); + + if (root == fs::path(".")) + return file; + else + return root / file; +} + + +/// Implementation of a parser for Kyuafiles. +/// +/// The main purpose of having this as a class is to keep track of global state +/// within the Lua files and allowing the Lua callbacks to easily access such +/// data. +class parser : utils::noncopyable { + /// Lua state to parse a single Kyuafile file. + lutok::state _state; + + /// Root directory of the test suite represented by the Kyuafile. + const fs::path _source_root; + + /// Root directory of the test programs. + const fs::path _build_root; + + /// Name of the Kyuafile to load relative to _source_root. + const fs::path _relative_filename; + + /// Version of the Kyuafile file format requested by the parsed file. + /// + /// This is set once the Kyuafile invokes the syntax() call. + optional< int > _version; + + /// Name of the test suite defined by the Kyuafile. + /// + /// This is set once the Kyuafile invokes the test_suite() call. + optional< std::string > _test_suite; + + /// Collection of test programs defined by the Kyuafile. + /// + /// This acts as an accumulator for all the *_test_program() calls within + /// the Kyuafile. + model::test_programs_vector _test_programs; + + /// Safely gets _test_suite and respects any test program overrides. + /// + /// \param program_override The test program-specific test suite name. May + /// be empty to indicate no override. + /// + /// \return The name of the test suite. + /// + /// \throw std::runtime_error If program_override is empty and the Kyuafile + /// did not yet define the global name of the test suite. + std::string + get_test_suite(const std::string& program_override) + { + std::string test_suite; + + if (program_override.empty()) { + if (!_test_suite) { + throw std::runtime_error("No test suite defined in the " + "Kyuafile and no override provided in " + "the test_program definition"); + } + test_suite = _test_suite.get(); + } else { + test_suite = program_override; + } + + return test_suite; + } + +public: + /// Initializes the parser and the Lua state. + /// + /// \param source_root_ The root directory of the test suite represented by + /// the Kyuafile. + /// \param build_root_ The root directory of the test programs. + /// \param relative_filename_ Name of the Kyuafile to load relative to + /// source_root_. + /// \param user_config User configuration holding any test suite properties + /// to be passed to the list operation. + /// \param scheduler_handle The scheduler context to use for loading the + /// test case lists. + parser(const fs::path& source_root_, const fs::path& build_root_, + const fs::path& relative_filename_, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) : + _source_root(source_root_), _build_root(build_root_), + _relative_filename(relative_filename_) + { + lutok::stack_cleaner cleaner(_state); + + _state.push_cxx_function(lua_syntax); + _state.set_global("syntax"); + + *_state.new_userdata< parser* >() = this; + _state.set_global("_parser"); + + _state.push_cxx_function(lua_current_kyuafile); + _state.set_global("current_kyuafile"); + + *_state.new_userdata< const config::tree* >() = &user_config; + *_state.new_userdata< scheduler::scheduler_handle* >() = + &scheduler_handle; + _state.push_cxx_closure(lua_include, 2); + _state.set_global("include"); + + _state.push_cxx_function(lua_test_suite); + _state.set_global("test_suite"); + + const std::set< std::string > interfaces = + scheduler::registered_interface_names(); + for (std::set< std::string >::const_iterator iter = interfaces.begin(); + iter != interfaces.end(); ++iter) { + const std::string& interface = *iter; + + _state.push_string(interface); + *_state.new_userdata< const config::tree* >() = &user_config; + *_state.new_userdata< scheduler::scheduler_handle* >() = + &scheduler_handle; + _state.push_cxx_closure(lua_generic_test_program, 3); + _state.set_global(interface + "_test_program"); + } + + _state.open_base(); + _state.open_string(); + _state.open_table(); + fs::open_fs(_state, callback_current_kyuafile().branch_path()); + } + + /// Destructor. + ~parser(void) + { + } + + /// Gets the parser object associated to a Lua state. + /// + /// \param state The Lua state from which to obtain the parser object. + /// + /// \return A pointer to the parser. + static parser* + get_from_state(lutok::state& state) + { + lutok::stack_cleaner cleaner(state); + state.get_global("_parser"); + return *state.to_userdata< parser* >(-1); + } + + /// Callback for the Kyuafile current_kyuafile() function. + /// + /// \return Returns the absolute path to the current Kyuafile. + fs::path + callback_current_kyuafile(void) const + { + const fs::path file = relativize(_source_root, _relative_filename); + if (file.is_absolute()) + return file; + else + return file.to_absolute(); + } + + /// Callback for the Kyuafile include() function. + /// + /// \post _test_programs is extended with the the test programs defined by + /// the included file. + /// + /// \param raw_file Path to the file to include. + /// \param user_config User configuration holding any test suite properties + /// to be passed to the list operation. + /// \param scheduler_handle Scheduler context to run test programs in. + void + callback_include(const fs::path& raw_file, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) + { + const fs::path file = relativize(_relative_filename.branch_path(), + raw_file); + const model::test_programs_vector subtps = + parser(_source_root, _build_root, file, user_config, + scheduler_handle).parse(); + + std::copy(subtps.begin(), subtps.end(), + std::back_inserter(_test_programs)); + } + + /// Callback for the Kyuafile syntax() function. + /// + /// \post _version is set to the requested version. + /// + /// \param version Version of the Kyuafile syntax requested by the file. + /// + /// \throw std::runtime_error If the format or the version are invalid, or + /// if syntax() has already been called. + void + callback_syntax(const int version) + { + if (_version) + throw std::runtime_error("Can only call syntax() once"); + + if (version < 1 || version > 2) + throw std::runtime_error(F("Unsupported file version %s") % + version); + + _version = utils::make_optional(version); + } + + /// Callback for the various Kyuafile *_test_program() functions. + /// + /// \post _test_programs is extended to include the newly defined test + /// program. + /// + /// \param interface Name of the test program interface. + /// \param raw_path Path to the test program, relative to the Kyuafile. + /// This has to be adjusted according to the relative location of this + /// Kyuafile to _source_root. + /// \param test_suite_override Name of the test suite this test program + /// belongs to, if explicitly defined at the test program level. + /// \param metadata Metadata variables passed to the test program. + /// \param user_config User configuration holding any test suite properties + /// to be passed to the list operation. + /// \param scheduler_handle Scheduler context to run test programs in. + /// + /// \throw std::runtime_error If the test program definition is invalid or + /// if the test program does not exist. + void + callback_test_program(const std::string& interface, + const fs::path& raw_path, + const std::string& test_suite_override, + const model::metadata& metadata, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) + { + if (raw_path.is_absolute()) + throw std::runtime_error(F("Got unexpected absolute path for test " + "program '%s'") % raw_path); + else if (raw_path.str() != raw_path.leaf_name()) + throw std::runtime_error(F("Test program '%s' cannot contain path " + "components") % raw_path); + + const fs::path path = relativize(_relative_filename.branch_path(), + raw_path); + + if (!fs::exists(_build_root / path)) + throw std::runtime_error(F("Non-existent test program '%s'") % + path); + + const std::string test_suite = get_test_suite(test_suite_override); + + _test_programs.push_back(model::test_program_ptr( + new scheduler::lazy_test_program(interface, path, _build_root, + test_suite, metadata, user_config, + scheduler_handle))); + } + + /// Callback for the Kyuafile test_suite() function. + /// + /// \post _version is set to the requested version. + /// + /// \param name Name of the test suite. + /// + /// \throw std::runtime_error If test_suite() has already been called. + void + callback_test_suite(const std::string& name) + { + if (_test_suite) + throw std::runtime_error("Can only call test_suite() once"); + _test_suite = utils::make_optional(name); + } + + /// Parses the Kyuafile. + /// + /// \pre Can only be invoked once. + /// + /// \return The collection of test programs defined by the Kyuafile. + /// + /// \throw load_error If there is any problem parsing the file. + const model::test_programs_vector& + parse(void) + { + PRE(_test_programs.empty()); + + const fs::path load_path = relativize(_source_root, _relative_filename); + try { + lutok::do_file(_state, load_path.str(), 0, 0, 0); + } catch (const std::runtime_error& e) { + // It is tempting to think that all of our various auxiliary + // functions above could raise load_error by themselves thus making + // this exception rewriting here unnecessary. Howver, that would + // not work because the helper functions above are executed within a + // Lua context, and we lose their type when they are propagated out + // of it. + throw engine::load_error(load_path, e.what()); + } + + if (!_version) + throw engine::load_error(load_path, "syntax() never called"); + + return _test_programs; + } +}; + + +/// Glue to invoke parser::callback_test_program() from Lua. +/// +/// This is a helper function for the various *_test_program() calls, as they +/// only differ in the interface of the defined test program. +/// +/// \pre state(-1) A table with the arguments that define the test program. The +/// special argument 'test_suite' provides an override to the global test suite +/// name. The rest of the arguments are part of the test program metadata. +/// \pre state(upvalue 1) String with the name of the interface. +/// \pre state(upvalue 2) User configuration with the per-test suite settings. +/// \pre state(upvalue 3) Scheduler context to run test programs in. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +/// +/// \throw std::runtime_error If the arguments to the function are invalid. +static int +lua_generic_test_program(lutok::state& state) +{ + if (!state.is_string(state.upvalue_index(1))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + const std::string interface = state.to_string(state.upvalue_index(1)); + + if (!state.is_userdata(state.upvalue_index(2))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + const config::tree* user_config = *state.to_userdata< const config::tree* >( + state.upvalue_index(2)); + + if (!state.is_userdata(state.upvalue_index(3))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + scheduler::scheduler_handle* scheduler_handle = + *state.to_userdata< scheduler::scheduler_handle* >( + state.upvalue_index(3)); + + if (!state.is_table(-1)) + throw std::runtime_error( + F("%s_test_program expects a table of properties as its single " + "argument") % interface); + + scheduler::ensure_valid_interface(interface); + + lutok::stack_cleaner cleaner(state); + + state.push_string("name"); + state.get_table(-2); + if (!state.is_string(-1)) + throw std::runtime_error("Test program name not defined or not a " + "string"); + const fs::path path(state.to_string(-1)); + state.pop(1); + + state.push_string("test_suite"); + state.get_table(-2); + std::string test_suite; + if (state.is_nil(-1)) { + // Leave empty to use the global test-suite value. + } else if (state.is_string(-1)) { + test_suite = state.to_string(-1); + } else { + throw std::runtime_error(F("Found non-string value in the test_suite " + "property of test program '%s'") % path); + } + state.pop(1); + + model::metadata_builder mdbuilder; + state.push_nil(); + while (state.next(-2)) { + if (!state.is_string(-2)) + throw std::runtime_error(F("Found non-string metadata property " + "name in test program '%s'") % + path); + const std::string property = state.to_string(-2); + + if (property != "name" && property != "test_suite") { + std::string value; + if (state.is_boolean(-1)) { + value = F("%s") % state.to_boolean(-1); + } else if (state.is_number(-1)) { + value = F("%s") % state.to_integer(-1); + } else if (state.is_string(-1)) { + value = state.to_string(-1); + } else { + throw std::runtime_error( + F("Metadata property '%s' in test program '%s' cannot be " + "converted to a string") % property % path); + } + + mdbuilder.set_string(property, value); + } + + state.pop(1); + } + + parser::get_from_state(state)->callback_test_program( + interface, path, test_suite, mdbuilder.build(), *user_config, + *scheduler_handle); + return 0; +} + + +/// Glue to invoke parser::callback_current_kyuafile() from Lua. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_current_kyuafile(lutok::state& state) +{ + state.push_string(parser::get_from_state(state)-> + callback_current_kyuafile().str()); + return 1; +} + + +/// Glue to invoke parser::callback_include() from Lua. +/// +/// \param state The Lua state that executed the function. +/// +/// \pre state(upvalue 1) User configuration with the per-test suite settings. +/// \pre state(upvalue 2) Scheduler context to run test programs in. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_include(lutok::state& state) +{ + if (!state.is_userdata(state.upvalue_index(1))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + const config::tree* user_config = *state.to_userdata< const config::tree* >( + state.upvalue_index(1)); + + if (!state.is_userdata(state.upvalue_index(2))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + scheduler::scheduler_handle* scheduler_handle = + *state.to_userdata< scheduler::scheduler_handle* >( + state.upvalue_index(2)); + + parser::get_from_state(state)->callback_include( + fs::path(state.to_string(-1)), *user_config, *scheduler_handle); + return 0; +} + + +/// Glue to invoke parser::callback_syntax() from Lua. +/// +/// \pre state(-2) The syntax format name, if a v1 file. +/// \pre state(-1) The syntax format version. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_syntax(lutok::state& state) +{ + if (!state.is_number(-1)) + throw std::runtime_error("Last argument to syntax must be a number"); + const int syntax_version = state.to_integer(-1); + + if (syntax_version == 1) { + if (state.get_top() != 2) + throw std::runtime_error("Version 1 files need two arguments to " + "syntax()"); + if (!state.is_string(-2) || state.to_string(-2) != "kyuafile") + throw std::runtime_error("First argument to syntax must be " + "'kyuafile' for version 1 files"); + } else { + if (state.get_top() != 1) + throw std::runtime_error("syntax() only takes one argument"); + } + + parser::get_from_state(state)->callback_syntax(syntax_version); + return 0; +} + + +/// Glue to invoke parser::callback_test_suite() from Lua. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_test_suite(lutok::state& state) +{ + parser::get_from_state(state)->callback_test_suite(state.to_string(-1)); + return 0; +} + + +} // anonymous namespace + + +/// Constructs a kyuafile form initialized data. +/// +/// Use load() to parse a test suite configuration file and construct a +/// kyuafile object. +/// +/// \param source_root_ The root directory for the test suite represented by the +/// Kyuafile. In other words, the directory containing the first Kyuafile +/// processed. +/// \param build_root_ The root directory for the test programs themselves. In +/// general, this will be the same as source_root_. If different, the +/// specified directory must follow the exact same layout of source_root_. +/// \param tps_ Collection of test programs that belong to this test suite. +engine::kyuafile::kyuafile(const fs::path& source_root_, + const fs::path& build_root_, + const model::test_programs_vector& tps_) : + _source_root(source_root_), + _build_root(build_root_), + _test_programs(tps_) +{ +} + + +/// Destructor. +engine::kyuafile::~kyuafile(void) +{ +} + + +/// Parses a test suite configuration file. +/// +/// \param file The file to parse. +/// \param user_build_root If not none, specifies a path to a directory +/// containing the test programs themselves. The layout of the build root +/// must match the layout of the source root (which is just the directory +/// from which the Kyuafile is being read). +/// \param user_config User configuration holding any test suite properties +/// to be passed to the list operation. +/// \param scheduler_handle The scheduler context to use for loading the test +/// case lists. +/// +/// \return High-level representation of the configuration file. +/// +/// \throw load_error If there is any problem loading the file. This includes +/// file access errors and syntax errors. +engine::kyuafile +engine::kyuafile::load(const fs::path& file, + const optional< fs::path > user_build_root, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) +{ + const fs::path source_root_ = file.branch_path(); + const fs::path build_root_ = user_build_root ? + user_build_root.get() : source_root_; + + // test_program.absolute_path() uses the current work directory and that + // fails to resolve the correct path once we have used chdir to enter the + // test work directory. To prevent this causing issues down the road, + // force the build root to be absolute so that absolute_path() does not + // need to rely on the current work directory. + const fs::path abs_build_root = build_root_.is_absolute() ? + build_root_ : build_root_.to_absolute(); + + return kyuafile(source_root_, build_root_, + parser(source_root_, abs_build_root, + fs::path(file.leaf_name()), user_config, + scheduler_handle).parse()); +} + + +/// Gets the root directory of the test suite. +/// +/// \return A path. +const fs::path& +engine::kyuafile::source_root(void) const +{ + return _source_root; +} + + +/// Gets the root directory of the test programs. +/// +/// \return A path. +const fs::path& +engine::kyuafile::build_root(void) const +{ + return _build_root; +} + + +/// Gets the collection of test programs that belong to this test suite. +/// +/// \return Collection of test program executable names. +const model::test_programs_vector& +engine::kyuafile::test_programs(void) const +{ + return _test_programs; +} diff --git a/engine/kyuafile.hpp b/engine/kyuafile.hpp new file mode 100644 index 000000000000..161f4305f4d1 --- /dev/null +++ b/engine/kyuafile.hpp @@ -0,0 +1,96 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/kyuafile.hpp +/// Test suite configuration parsing and representation. + +#if !defined(ENGINE_KYUAFILE_HPP) +#define ENGINE_KYUAFILE_HPP + +#include "engine/kyuafile_fwd.hpp" + +#include +#include + +#include + +#include "engine/scheduler_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional_fwd.hpp" + +namespace engine { + + +/// Representation of the configuration of a test suite. +/// +/// Test suites are collections of related test programs. They are described by +/// a configuration file. +/// +/// Test suites have two path references: one to the "source root" and another +/// one to the "build root". The source root points to the directory from which +/// the Kyuafile is being read, and all recursive inclusions are resolved +/// relative to that directory. The build root points to the directory +/// containing the generated test programs and is prepended to the absolute path +/// of the test programs referenced by the Kyuafiles. In general, the build +/// root will be the same as the source root; however, when using a build system +/// that supports "build directories", providing this option comes in handy to +/// allow running the tests without much hassle. +/// +/// This class provides the parser for test suite configuration files and +/// methods to access the parsed data. +class kyuafile { + /// Path to the directory containing the top-level Kyuafile loaded. + utils::fs::path _source_root; + + /// Path to the directory containing the test programs. + utils::fs::path _build_root; + + /// Collection of the test programs defined in the Kyuafile. + model::test_programs_vector _test_programs; + +public: + explicit kyuafile(const utils::fs::path&, const utils::fs::path&, + const model::test_programs_vector&); + ~kyuafile(void); + + static kyuafile load(const utils::fs::path&, + const utils::optional< utils::fs::path >, + const utils::config::tree&, + scheduler::scheduler_handle&); + + const utils::fs::path& source_root(void) const; + const utils::fs::path& build_root(void) const; + const model::test_programs_vector& test_programs(void) const; +}; + + +} // namespace engine + +#endif // !defined(ENGINE_KYUAFILE_HPP) diff --git a/engine/kyuafile_fwd.hpp b/engine/kyuafile_fwd.hpp new file mode 100644 index 000000000000..60a98f65e3ab --- /dev/null +++ b/engine/kyuafile_fwd.hpp @@ -0,0 +1,43 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/kyuafile_fwd.hpp +/// Forward declarations for engine/kyuafile.hpp + +#if !defined(ENGINE_KYUAFILE_FWD_HPP) +#define ENGINE_KYUAFILE_FWD_HPP + +namespace engine { + + +class kyuafile; + + +} // namespace engine + +#endif // !defined(ENGINE_KYUAFILE_FWD_HPP) diff --git a/engine/kyuafile_test.cpp b/engine/kyuafile_test.cpp new file mode 100644 index 000000000000..d95f28c71acb --- /dev/null +++ b/engine/kyuafile_test.cpp @@ -0,0 +1,606 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/kyuafile.hpp" + +extern "C" { +#include +} + +#include +#include + +#include +#include +#include +#include + +#include "engine/atf.hpp" +#include "engine/exceptions.hpp" +#include "engine/plain.hpp" +#include "engine/scheduler.hpp" +#include "engine/tap.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/optional.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__empty); +ATF_TEST_CASE_BODY(kyuafile__load__empty) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file("config", "syntax(2)\n"); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(0, suite.test_programs().size()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__real_interfaces); +ATF_TEST_CASE_BODY(kyuafile__load__real_interfaces) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "test_suite('one-suite')\n" + "atf_test_program{name='1st'}\n" + "atf_test_program{name='2nd', test_suite='first'}\n" + "plain_test_program{name='3rd'}\n" + "tap_test_program{name='4th', test_suite='second'}\n" + "include('dir/config')\n"); + + fs::mkdir(fs::path("dir"), 0755); + atf::utils::create_file( + "dir/config", + "syntax(2)\n" + "atf_test_program{name='1st', test_suite='other-suite'}\n" + "include('subdir/config')\n"); + + fs::mkdir(fs::path("dir/subdir"), 0755); + atf::utils::create_file( + "dir/subdir/config", + "syntax(2)\n" + "atf_test_program{name='5th', test_suite='last-suite'}\n"); + + atf::utils::create_file("1st", ""); + atf::utils::create_file("2nd", ""); + atf::utils::create_file("3rd", ""); + atf::utils::create_file("4th", ""); + atf::utils::create_file("dir/1st", ""); + atf::utils::create_file("dir/subdir/5th", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(6, suite.test_programs().size()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[0]->interface_name()); + ATF_REQUIRE_EQ(fs::path("1st"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("one-suite", suite.test_programs()[0]->test_suite_name()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[1]->interface_name()); + ATF_REQUIRE_EQ(fs::path("2nd"), suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("first", suite.test_programs()[1]->test_suite_name()); + + ATF_REQUIRE_EQ("plain", suite.test_programs()[2]->interface_name()); + ATF_REQUIRE_EQ(fs::path("3rd"), suite.test_programs()[2]->relative_path()); + ATF_REQUIRE_EQ("one-suite", suite.test_programs()[2]->test_suite_name()); + + ATF_REQUIRE_EQ("tap", suite.test_programs()[3]->interface_name()); + ATF_REQUIRE_EQ(fs::path("4th"), suite.test_programs()[3]->relative_path()); + ATF_REQUIRE_EQ("second", suite.test_programs()[3]->test_suite_name()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[4]->interface_name()); + ATF_REQUIRE_EQ(fs::path("dir/1st"), + suite.test_programs()[4]->relative_path()); + ATF_REQUIRE_EQ("other-suite", suite.test_programs()[4]->test_suite_name()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[5]->interface_name()); + ATF_REQUIRE_EQ(fs::path("dir/subdir/5th"), + suite.test_programs()[5]->relative_path()); + ATF_REQUIRE_EQ("last-suite", suite.test_programs()[5]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__mock_interfaces); +ATF_TEST_CASE_BODY(kyuafile__load__mock_interfaces) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + std::shared_ptr< scheduler::interface > mock_interface( + new engine::plain_interface()); + + scheduler::register_interface("some", mock_interface); + scheduler::register_interface("random", mock_interface); + scheduler::register_interface("names", mock_interface); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "test_suite('one-suite')\n" + "some_test_program{name='1st'}\n" + "random_test_program{name='2nd'}\n" + "names_test_program{name='3rd'}\n"); + + atf::utils::create_file("1st", ""); + atf::utils::create_file("2nd", ""); + atf::utils::create_file("3rd", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(3, suite.test_programs().size()); + + ATF_REQUIRE_EQ("some", suite.test_programs()[0]->interface_name()); + ATF_REQUIRE_EQ(fs::path("1st"), suite.test_programs()[0]->relative_path()); + + ATF_REQUIRE_EQ("random", suite.test_programs()[1]->interface_name()); + ATF_REQUIRE_EQ(fs::path("2nd"), suite.test_programs()[1]->relative_path()); + + ATF_REQUIRE_EQ("names", suite.test_programs()[2]->interface_name()); + ATF_REQUIRE_EQ(fs::path("3rd"), suite.test_programs()[2]->relative_path()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__metadata); +ATF_TEST_CASE_BODY(kyuafile__load__metadata) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "atf_test_program{name='1st', test_suite='first'," + " allowed_architectures='amd64 i386', timeout=15}\n" + "plain_test_program{name='2nd', test_suite='second'," + " required_files='foo /bar//baz', required_user='root'," + " ['custom.a-number']=123, ['custom.a-bool']=true}\n"); + atf::utils::create_file("1st", ""); + atf::utils::create_file("2nd", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(2, suite.test_programs().size()); + + ATF_REQUIRE_EQ("atf", suite.test_programs()[0]->interface_name()); + ATF_REQUIRE_EQ(fs::path("1st"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("first", suite.test_programs()[0]->test_suite_name()); + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("amd64") + .add_allowed_architecture("i386") + .set_timeout(datetime::delta(15, 0)) + .build(); + ATF_REQUIRE_EQ(md1, suite.test_programs()[0]->get_metadata()); + + ATF_REQUIRE_EQ("plain", suite.test_programs()[1]->interface_name()); + ATF_REQUIRE_EQ(fs::path("2nd"), suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("second", suite.test_programs()[1]->test_suite_name()); + const model::metadata md2 = model::metadata_builder() + .add_required_file(fs::path("foo")) + .add_required_file(fs::path("/bar/baz")) + .add_custom("a-bool", "true") + .add_custom("a-number", "123") + .set_required_user("root") + .build(); + ATF_REQUIRE_EQ(md2, suite.test_programs()[1]->get_metadata()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__current_directory); +ATF_TEST_CASE_BODY(kyuafile__load__current_directory) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "atf_test_program{name='one', test_suite='first'}\n" + "include('config2')\n"); + + atf::utils::create_file( + "config2", + "syntax(2)\n" + "test_suite('second')\n" + "atf_test_program{name='two'}\n"); + + atf::utils::create_file("one", ""); + atf::utils::create_file("two", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(2, suite.test_programs().size()); + ATF_REQUIRE_EQ(fs::path("one"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("first", suite.test_programs()[0]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("two"), + suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("second", suite.test_programs()[1]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__other_directory); +ATF_TEST_CASE_BODY(kyuafile__load__other_directory) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file( + "root/config", + "syntax(2)\n" + "test_suite('abc')\n" + "atf_test_program{name='one'}\n" + "include('dir/config')\n"); + + fs::mkdir(fs::path("root/dir"), 0755); + atf::utils::create_file( + "root/dir/config", + "syntax(2)\n" + "test_suite('foo')\n" + "atf_test_program{name='two', test_suite='def'}\n" + "atf_test_program{name='three'}\n"); + + atf::utils::create_file("root/one", ""); + atf::utils::create_file("root/dir/two", ""); + atf::utils::create_file("root/dir/three", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("root/config"), none, config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("root"), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("root"), suite.build_root()); + ATF_REQUIRE_EQ(3, suite.test_programs().size()); + ATF_REQUIRE_EQ(fs::path("one"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("abc", suite.test_programs()[0]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("dir/two"), + suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("def", suite.test_programs()[1]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("dir/three"), + suite.test_programs()[2]->relative_path()); + ATF_REQUIRE_EQ("foo", suite.test_programs()[2]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__build_directory); +ATF_TEST_CASE_BODY(kyuafile__load__build_directory) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + fs::mkdir(fs::path("srcdir"), 0755); + atf::utils::create_file( + "srcdir/config", + "syntax(2)\n" + "test_suite('abc')\n" + "atf_test_program{name='one'}\n" + "include('dir/config')\n"); + + fs::mkdir(fs::path("srcdir/dir"), 0755); + atf::utils::create_file( + "srcdir/dir/config", + "syntax(2)\n" + "test_suite('foo')\n" + "atf_test_program{name='two', test_suite='def'}\n" + "atf_test_program{name='three'}\n"); + + fs::mkdir(fs::path("builddir"), 0755); + atf::utils::create_file("builddir/one", ""); + fs::mkdir(fs::path("builddir/dir"), 0755); + atf::utils::create_file("builddir/dir/two", ""); + atf::utils::create_file("builddir/dir/three", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("srcdir/config"), utils::make_optional(fs::path("builddir")), + config::tree(), handle); + ATF_REQUIRE_EQ(fs::path("srcdir"), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("builddir"), suite.build_root()); + ATF_REQUIRE_EQ(3, suite.test_programs().size()); + ATF_REQUIRE_EQ(fs::path("builddir/one").to_absolute(), + suite.test_programs()[0]->absolute_path()); + ATF_REQUIRE_EQ(fs::path("one"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("abc", suite.test_programs()[0]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("builddir/dir/two").to_absolute(), + suite.test_programs()[1]->absolute_path()); + ATF_REQUIRE_EQ(fs::path("dir/two"), + suite.test_programs()[1]->relative_path()); + ATF_REQUIRE_EQ("def", suite.test_programs()[1]->test_suite_name()); + ATF_REQUIRE_EQ(fs::path("builddir/dir/three").to_absolute(), + suite.test_programs()[2]->absolute_path()); + ATF_REQUIRE_EQ(fs::path("dir/three"), + suite.test_programs()[2]->relative_path()); + ATF_REQUIRE_EQ("foo", suite.test_programs()[2]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__absolute_paths_are_stable); +ATF_TEST_CASE_BODY(kyuafile__load__absolute_paths_are_stable) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "config", + "syntax(2)\n" + "atf_test_program{name='one', test_suite='first'}\n"); + atf::utils::create_file("one", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("config"), none, config::tree(), handle); + + const fs::path previous_dir = fs::current_path(); + fs::mkdir(fs::path("other"), 0755); + // Change the directory. We want later calls to absolute_path() on the test + // programs to return references to previous_dir instead. + ATF_REQUIRE(::chdir("other") != -1); + + ATF_REQUIRE_EQ(fs::path("."), suite.source_root()); + ATF_REQUIRE_EQ(fs::path("."), suite.build_root()); + ATF_REQUIRE_EQ(1, suite.test_programs().size()); + ATF_REQUIRE_EQ(previous_dir / "one", + suite.test_programs()[0]->absolute_path()); + ATF_REQUIRE_EQ(fs::path("one"), suite.test_programs()[0]->relative_path()); + ATF_REQUIRE_EQ("first", suite.test_programs()[0]->test_suite_name()); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__fs_calls_are_relative); +ATF_TEST_CASE_BODY(kyuafile__load__fs_calls_are_relative) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + atf::utils::create_file( + "Kyuafile", + "syntax(2)\n" + "if fs.exists('one') then\n" + " plain_test_program{name='one', test_suite='first'}\n" + "end\n" + "if fs.exists('two') then\n" + " plain_test_program{name='two', test_suite='first'}\n" + "end\n" + "include('dir/Kyuafile')\n"); + atf::utils::create_file("one", ""); + fs::mkdir(fs::path("dir"), 0755); + atf::utils::create_file( + "dir/Kyuafile", + "syntax(2)\n" + "if fs.exists('one') then\n" + " plain_test_program{name='one', test_suite='first'}\n" + "end\n" + "if fs.exists('two') then\n" + " plain_test_program{name='two', test_suite='first'}\n" + "end\n"); + atf::utils::create_file("dir/two", ""); + + const engine::kyuafile suite = engine::kyuafile::load( + fs::path("Kyuafile"), none, config::tree(), handle); + + ATF_REQUIRE_EQ(2, suite.test_programs().size()); + ATF_REQUIRE_EQ(fs::current_path() / "one", + suite.test_programs()[0]->absolute_path()); + ATF_REQUIRE_EQ(fs::current_path() / "dir/two", + suite.test_programs()[1]->absolute_path()); + + handle.cleanup(); +} + + +/// Verifies that load raises a load_error on a given input. +/// +/// \param file Name of the file to load. +/// \param regex Expression to match on load_error's contents. +static void +do_load_error_test(const char* file, const char* regex) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + ATF_REQUIRE_THROW_RE(engine::load_error, regex, + engine::kyuafile::load(fs::path(file), none, + config::tree(), handle)); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__test_program_not_basename); +ATF_TEST_CASE_BODY(kyuafile__load__test_program_not_basename) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "test_suite('abc')\n" + "atf_test_program{name='one'}\n" + "atf_test_program{name='./ls'}\n"); + + atf::utils::create_file("one", ""); + do_load_error_test("config", "./ls.*path components"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__lua_error); +ATF_TEST_CASE_BODY(kyuafile__load__lua_error) +{ + atf::utils::create_file("config", "this syntax is invalid\n"); + + do_load_error_test("config", ".*"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__syntax__not_called); +ATF_TEST_CASE_BODY(kyuafile__load__syntax__not_called) +{ + atf::utils::create_file("config", ""); + + do_load_error_test("config", "syntax.* never called"); +} + + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__syntax__deprecated_format); +ATF_TEST_CASE_BODY(kyuafile__load__syntax__deprecated_format) +{ + atf::utils::create_file("config", "syntax('foo', 1)\n"); + do_load_error_test("config", "must be 'kyuafile'"); + + atf::utils::create_file("config", "syntax('config', 2)\n"); + do_load_error_test("config", "only takes one argument"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__syntax__twice); +ATF_TEST_CASE_BODY(kyuafile__load__syntax__twice) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "syntax(2)\n"); + + do_load_error_test("config", "Can only call syntax.* once"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__syntax__bad_version); +ATF_TEST_CASE_BODY(kyuafile__load__syntax__bad_version) +{ + atf::utils::create_file("config", "syntax(12)\n"); + + do_load_error_test("config", "Unsupported file version 12"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__test_suite__missing); +ATF_TEST_CASE_BODY(kyuafile__load__test_suite__missing) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "plain_test_program{name='one'}"); + + atf::utils::create_file("one", ""); + + do_load_error_test("config", "No test suite defined"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__test_suite__twice); +ATF_TEST_CASE_BODY(kyuafile__load__test_suite__twice) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "test_suite('foo')\n" + "test_suite('bar')\n"); + + do_load_error_test("config", "Can only call test_suite.* once"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__missing_file); +ATF_TEST_CASE_BODY(kyuafile__load__missing_file) +{ + do_load_error_test("missing", "Load of 'missing' failed"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile__load__missing_test_program); +ATF_TEST_CASE_BODY(kyuafile__load__missing_test_program) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "atf_test_program{name='one', test_suite='first'}\n" + "atf_test_program{name='two', test_suite='first'}\n"); + + atf::utils::create_file("one", ""); + + do_load_error_test("config", "Non-existent.*'two'"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "atf", std::shared_ptr< scheduler::interface >( + new engine::atf_interface())); + scheduler::register_interface( + "plain", std::shared_ptr< scheduler::interface >( + new engine::plain_interface())); + scheduler::register_interface( + "tap", std::shared_ptr< scheduler::interface >( + new engine::tap_interface())); + + ATF_ADD_TEST_CASE(tcs, kyuafile__load__empty); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__real_interfaces); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__mock_interfaces); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__metadata); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__current_directory); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__other_directory); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__build_directory); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__absolute_paths_are_stable); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__fs_calls_are_relative); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__test_program_not_basename); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__lua_error); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__syntax__not_called); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__syntax__deprecated_format); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__syntax__twice); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__syntax__bad_version); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__test_suite__missing); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__test_suite__twice); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__missing_file); + ATF_ADD_TEST_CASE(tcs, kyuafile__load__missing_test_program); +} diff --git a/engine/plain.cpp b/engine/plain.cpp new file mode 100644 index 000000000000..8346e50bbecf --- /dev/null +++ b/engine/plain.cpp @@ -0,0 +1,143 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/plain.hpp" + +extern "C" { +#include +} + +#include + +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::optional; + + +/// Executes a test program's list operation. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +void +engine::plain_interface::exec_list( + const model::test_program& /* test_program */, + const config::properties_map& /* vars */) const +{ + ::_exit(EXIT_SUCCESS); +} + + +/// Computes the test cases list of a test program. +/// +/// \return A list of test cases. +model::test_cases_map +engine::plain_interface::parse_list( + const optional< process::status >& /* status */, + const fs::path& /* stdout_path */, + const fs::path& /* stderr_path */) const +{ + return model::test_cases_map_builder().add("main").build(); +} + + +/// Executes a test case of the test program. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param test_case_name Name of the test case to invoke. +/// \param vars User-provided variables to pass to the test program. +void +engine::plain_interface::exec_test( + const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& /* control_directory */) const +{ + PRE(test_case_name == "main"); + + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + utils::setenv(F("TEST_ENV_%s") % (*iter).first, (*iter).second); + } + + process::args_vector args; + process::exec(test_program.absolute_path(), args); +} + + +/// Computes the result of a test case based on its termination status. +/// +/// \param status The termination status of the subprocess used to execute +/// the exec_test() method or none if the test timed out. +/// +/// \return A test result. +model::test_result +engine::plain_interface::compute_result( + const optional< process::status >& status, + const fs::path& /* control_directory */, + const fs::path& /* stdout_path */, + const fs::path& /* stderr_path */) const +{ + if (!status) { + return model::test_result(model::test_result_broken, + "Test case timed out"); + } + + if (status.get().exited()) { + const int exitstatus = status.get().exitstatus(); + if (exitstatus == EXIT_SUCCESS) { + return model::test_result(model::test_result_passed); + } else { + return model::test_result( + model::test_result_failed, + F("Returned non-success exit status %s") % exitstatus); + } + } else { + return model::test_result( + model::test_result_broken, + F("Received signal %s") % status.get().termsig()); + } +} diff --git a/engine/plain.hpp b/engine/plain.hpp new file mode 100644 index 000000000000..ee5f3e746781 --- /dev/null +++ b/engine/plain.hpp @@ -0,0 +1,67 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/plain.hpp +/// Execution engine for test programs that implement the plain interface. + +#if !defined(ENGINE_PLAIN_HPP) +#define ENGINE_PLAIN_HPP + +#include "engine/scheduler.hpp" + +namespace engine { + + +/// Implementation of the scheduler interface for plain test programs. +class plain_interface : public engine::scheduler::interface { +public: + void exec_list(const model::test_program&, + const utils::config::properties_map&) const UTILS_NORETURN; + + model::test_cases_map parse_list( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&) const; + + void exec_test(const model::test_program&, const std::string&, + const utils::config::properties_map&, + const utils::fs::path&) const + UTILS_NORETURN; + + model::test_result compute_result( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&, + const utils::fs::path&) const; +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_PLAIN_HPP) diff --git a/engine/plain_helpers.cpp b/engine/plain_helpers.cpp new file mode 100644 index 000000000000..52b1bc74fe10 --- /dev/null +++ b/engine/plain_helpers.cpp @@ -0,0 +1,238 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +extern "C" { +#include + +#include + +extern char** environ; +} + +#include +#include +#include +#include +#include + +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; + +using utils::optional; + + +namespace { + + +/// Gets the name of the test case to run. +/// +/// We use the value of the TEST_CASE environment variable if present, or +/// else the basename of the test program. +/// +/// \param arg0 Value of argv[0] as passed to main(). +/// +/// \return A test case name. The name may not be valid. +static std::string +guess_test_case_name(const char* arg0) +{ + const optional< std::string > test_case_env = utils::getenv("TEST_CASE"); + if (test_case_env) { + return test_case_env.get(); + } else { + return fs::path(arg0).leaf_name(); + } +} + + +/// Logs an error message and exits the test with an error code. +/// +/// \param str The error message to log. +static void +fail(const std::string& str) +{ + std::cerr << str << '\n'; + std::exit(EXIT_FAILURE); +} + + +/// A test case that validates the TEST_ENV_* variables. +static void +test_check_configuration_variables(void) +{ + std::set< std::string > vars; + char** iter; + for (iter = environ; *iter != NULL; ++iter) { + if (std::strstr(*iter, "TEST_ENV_") == *iter) { + vars.insert(*iter); + } + } + + std::set< std::string > exp_vars; + exp_vars.insert("TEST_ENV_first=some value"); + exp_vars.insert("TEST_ENV_second=some other value"); + if (vars != exp_vars) { + fail(F("Expected: %s\nFound: %s\n") % exp_vars % vars); + } +} + + +/// A test case that crashes. +static void +test_crash(void) +{ + utils::abort_without_coredump(); +} + + +/// A test case that exits with a non-zero exit code, and not 1. +static void +test_fail(void) +{ + std::exit(8); +} + + +/// A test case that passes. +static void +test_pass(void) +{ +} + + +/// A test case that spawns a subchild that gets stuck. +/// +/// This test case is used by the caller to validate that the whole process tree +/// is terminated when the test case is killed. +static void +test_spawn_blocking_child(void) +{ + pid_t pid = ::fork(); + if (pid == -1) + fail("Cannot fork subprocess"); + else if (pid == 0) { + for (;;) + ::pause(); + } else { + const fs::path name = fs::path(utils::getenv("CONTROL_DIR").get()) / + "pid"; + std::ofstream pidfile(name.c_str()); + if (!pidfile) + fail("Failed to create the pidfile"); + pidfile << pid; + pidfile.close(); + } +} + + +/// A test case that times out. +/// +/// Note that the timeout is defined in the Kyuafile, as the plain interface has +/// no means for test programs to specify this by themselves. +static void +test_timeout(void) +{ + ::sleep(10); + const fs::path control_dir = fs::path(utils::getenv("CONTROL_DIR").get()); + std::ofstream file((control_dir / "cookie").c_str()); + if (!file) + fail("Failed to create the control cookie"); + file.close(); +} + + +/// A test case that performs basic checks on the runtime environment. +/// +/// If the runtime environment does not look clean (according to the rules in +/// the Kyua runtime properties), the test fails. +static void +test_validate_isolation(void) +{ + if (utils::getenv("HOME").get() == "fake-value") + fail("HOME not reset"); + if (utils::getenv("LANG")) + fail("LANG not unset"); +} + + +} // anonymous namespace + + +/// Entry point to the test program. +/// +/// The caller can select which test case to run by defining the TEST_CASE +/// environment variable. This is not "standard", in the sense this is not a +/// generic property of the plain test case interface. +/// +/// \todo It may be worth to split this binary into separate, smaller binaries, +/// one for every "test case". We use this program as a dispatcher for +/// different "main"s, the only reason being to keep the amount of helper test +/// programs to a minimum. However, putting this each function in its own +/// binary could simplify many other things. +/// +/// \param argc The number of CLI arguments. +/// \param argv The CLI arguments themselves. These are not used because +/// Kyua will not pass any arguments to the plain test program. +int +main(int argc, char** argv) +{ + if (argc != 1) { + std::cerr << "No arguments allowed; select the test case with the " + "TEST_CASE variable"; + return EXIT_FAILURE; + } + + const std::string& test_case = guess_test_case_name(argv[0]); + + if (test_case == "check_configuration_variables") + test_check_configuration_variables(); + else if (test_case == "crash") + test_crash(); + else if (test_case == "fail") + test_fail(); + else if (test_case == "pass") + test_pass(); + else if (test_case == "spawn_blocking_child") + test_spawn_blocking_child(); + else if (test_case == "timeout") + test_timeout(); + else if (test_case == "validate_isolation") + test_validate_isolation(); + else { + std::cerr << "Unknown test case"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/engine/plain_test.cpp b/engine/plain_test.cpp new file mode 100644 index 000000000000..cc3326e4c581 --- /dev/null +++ b/engine/plain_test.cpp @@ -0,0 +1,207 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/plain.hpp" + +extern "C" { +#include +} + +#include + +#include "engine/config.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; + + +namespace { + + +/// Copies the plain helper to the work directory, selecting a specific helper. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param name Name of the new binary to create. Must match the name of a +/// valid helper, as the binary name is used to select it. +static void +copy_plain_helper(const atf::tests::tc* tc, const char* name) +{ + const fs::path srcdir(tc->get_config_var("srcdir")); + atf::utils::copy_file((srcdir / "plain_helpers").str(), name); +} + + +/// Runs one plain test program and checks its result. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param test_case_name Name of the "test case" to select from the helper +/// program. +/// \param exp_result The expected result. +/// \param metadata The test case metadata. +/// \param user_config User-provided configuration variables. +static void +run_one(const atf::tests::tc* tc, const char* test_case_name, + const model::test_result& exp_result, + const model::metadata& metadata = model::metadata_builder().build(), + const config::tree& user_config = engine::empty_config()) +{ + copy_plain_helper(tc, test_case_name); + const model::test_program_ptr program = model::test_program_builder( + "plain", fs::path(test_case_name), fs::current_path(), "the-suite") + .add_test_case("main", metadata).build_ptr(); + + scheduler::scheduler_handle handle = scheduler::setup(); + (void)handle.spawn_test(program, "main", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + atf::utils::cat_file(result_handle->stdout_file().str(), "stdout: "); + atf::utils::cat_file(result_handle->stderr_file().str(), "stderr: "); + ATF_REQUIRE_EQ(exp_result, test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(list); +ATF_TEST_CASE_BODY(list) +{ + const model::test_program program = model::test_program_builder( + "plain", fs::path("non-existent"), fs::path("."), "unused-suite") + .build(); + + scheduler::scheduler_handle handle = scheduler::setup(); + const model::test_cases_map test_cases = handle.list_tests( + &program, engine::empty_config()); + handle.cleanup(); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("main").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__exit_success_is_pass); +ATF_TEST_CASE_BODY(test__exit_success_is_pass) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "pass", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__exit_non_zero_is_fail); +ATF_TEST_CASE_BODY(test__exit_non_zero_is_fail) +{ + const model::test_result exp_result( + model::test_result_failed, + "Returned non-success exit status 8"); + run_one(this, "fail", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__signal_is_broken); +ATF_TEST_CASE_BODY(test__signal_is_broken) +{ + const model::test_result exp_result(model::test_result_broken, + F("Received signal %s") % SIGABRT); + run_one(this, "crash", exp_result); +} + + +ATF_TEST_CASE(test__timeout_is_broken); +ATF_TEST_CASE_HEAD(test__timeout_is_broken) +{ + set_md_var("timeout", "60"); +} +ATF_TEST_CASE_BODY(test__timeout_is_broken) +{ + utils::setenv("CONTROL_DIR", fs::current_path().str()); + + const model::metadata metadata = model::metadata_builder() + .set_timeout(datetime::delta(1, 0)).build(); + const model::test_result exp_result(model::test_result_broken, + "Test case timed out"); + run_one(this, "timeout", exp_result, metadata); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__configuration_variables); +ATF_TEST_CASE_BODY(test__configuration_variables) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.a-suite.first", "unused"); + user_config.set_string("test_suites.the-suite.first", "some value"); + user_config.set_string("test_suites.the-suite.second", "some other value"); + user_config.set_string("test_suites.other-suite.first", "unused"); + + const model::test_result exp_result(model::test_result_passed); + run_one(this, "check_configuration_variables", exp_result, + model::metadata_builder().build(), user_config); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "plain", std::shared_ptr< scheduler::interface >( + new engine::plain_interface())); + + ATF_ADD_TEST_CASE(tcs, list); + + ATF_ADD_TEST_CASE(tcs, test__exit_success_is_pass); + ATF_ADD_TEST_CASE(tcs, test__exit_non_zero_is_fail); + ATF_ADD_TEST_CASE(tcs, test__signal_is_broken); + ATF_ADD_TEST_CASE(tcs, test__timeout_is_broken); + ATF_ADD_TEST_CASE(tcs, test__configuration_variables); +} diff --git a/engine/requirements.cpp b/engine/requirements.cpp new file mode 100644 index 000000000000..a7b0a90d97db --- /dev/null +++ b/engine/requirements.cpp @@ -0,0 +1,293 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/requirements.hpp" + +#include "model/metadata.hpp" +#include "model/types.hpp" +#include "utils/config/nodes.ipp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/memory.hpp" +#include "utils/passwd.hpp" +#include "utils/sanity.hpp" +#include "utils/units.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace units = utils::units; + + +namespace { + + +/// Checks if all required configuration variables are present. +/// +/// \param required_configs Set of required variable names. +/// \param user_config Runtime user configuration. +/// \param test_suite_name Name of the test suite the test belongs to. +/// +/// \return Empty if all variables are present or an error message otherwise. +static std::string +check_required_configs(const model::strings_set& required_configs, + const config::tree& user_config, + const std::string& test_suite_name) +{ + for (model::strings_set::const_iterator iter = required_configs.begin(); + iter != required_configs.end(); iter++) { + std::string property; + // TODO(jmmv): All this rewrite logic belongs in the ATF interface. + if ((*iter) == "unprivileged-user" || (*iter) == "unprivileged_user") + property = "unprivileged_user"; + else + property = F("test_suites.%s.%s") % test_suite_name % (*iter); + + if (!user_config.is_set(property)) + return F("Required configuration property '%s' not defined") % + (*iter); + } + return ""; +} + + +/// Checks if the allowed architectures match the current architecture. +/// +/// \param allowed_architectures Set of allowed architectures. +/// \param user_config Runtime user configuration. +/// +/// \return Empty if the current architecture is in the list or an error +/// message otherwise. +static std::string +check_allowed_architectures(const model::strings_set& allowed_architectures, + const config::tree& user_config) +{ + if (!allowed_architectures.empty()) { + const std::string architecture = + user_config.lookup< config::string_node >("architecture"); + if (allowed_architectures.find(architecture) == + allowed_architectures.end()) + return F("Current architecture '%s' not supported") % architecture; + } + return ""; +} + + +/// Checks if the allowed platforms match the current architecture. +/// +/// \param allowed_platforms Set of allowed platforms. +/// \param user_config Runtime user configuration. +/// +/// \return Empty if the current platform is in the list or an error message +/// otherwise. +static std::string +check_allowed_platforms(const model::strings_set& allowed_platforms, + const config::tree& user_config) +{ + if (!allowed_platforms.empty()) { + const std::string platform = + user_config.lookup< config::string_node >("platform"); + if (allowed_platforms.find(platform) == allowed_platforms.end()) + return F("Current platform '%s' not supported") % platform; + } + return ""; +} + + +/// Checks if the current user matches the required user. +/// +/// \param required_user Name of the required user category. +/// \param user_config Runtime user configuration. +/// +/// \return Empty if the current user fits the required user characteristics or +/// an error message otherwise. +static std::string +check_required_user(const std::string& required_user, + const config::tree& user_config) +{ + if (!required_user.empty()) { + const passwd::user user = passwd::current_user(); + if (required_user == "root") { + if (!user.is_root()) + return "Requires root privileges"; + } else if (required_user == "unprivileged") { + if (user.is_root()) + if (!user_config.is_set("unprivileged_user")) + return "Requires an unprivileged user but the " + "unprivileged-user configuration variable is not " + "defined"; + } else + UNREACHABLE_MSG("Value of require.user not properly validated"); + } + return ""; +} + + +/// Checks if all required files exist. +/// +/// \param required_files Set of paths. +/// +/// \return Empty if the required files all exist or an error message otherwise. +static std::string +check_required_files(const model::paths_set& required_files) +{ + for (model::paths_set::const_iterator iter = required_files.begin(); + iter != required_files.end(); iter++) { + INV((*iter).is_absolute()); + if (!fs::exists(*iter)) + return F("Required file '%s' not found") % *iter; + } + return ""; +} + + +/// Checks if all required programs exist. +/// +/// \param required_programs Set of paths. +/// +/// \return Empty if the required programs all exist or an error message +/// otherwise. +static std::string +check_required_programs(const model::paths_set& required_programs) +{ + for (model::paths_set::const_iterator iter = required_programs.begin(); + iter != required_programs.end(); iter++) { + if ((*iter).is_absolute()) { + if (!fs::exists(*iter)) + return F("Required program '%s' not found") % *iter; + } else { + if (!fs::find_in_path((*iter).c_str())) + return F("Required program '%s' not found in PATH") % *iter; + } + } + return ""; +} + + +/// Checks if the current system has the specified amount of memory. +/// +/// \param required_memory Amount of required physical memory, or zero if not +/// applicable. +/// +/// \return Empty if the current system has the required amount of memory or an +/// error message otherwise. +static std::string +check_required_memory(const units::bytes& required_memory) +{ + if (required_memory > 0) { + const units::bytes physical_memory = utils::physical_memory(); + if (physical_memory > 0 && physical_memory < required_memory) + return F("Requires %s bytes of physical memory but only %s " + "available") % + required_memory.format() % physical_memory.format(); + } + return ""; +} + + +/// Checks if the work directory's file system has enough free disk space. +/// +/// \param required_disk_space Amount of required free disk space, or zero if +/// not applicable. +/// \param work_directory Path to where the test case will be run. +/// +/// \return Empty if the file system where the work directory is hosted has +/// enough free disk space or an error message otherwise. +static std::string +check_required_disk_space(const units::bytes& required_disk_space, + const fs::path& work_directory) +{ + if (required_disk_space > 0) { + const units::bytes free_disk_space = fs::free_disk_space( + work_directory); + if (free_disk_space < required_disk_space) + return F("Requires %s bytes of free disk space but only %s " + "available") % + required_disk_space.format() % free_disk_space.format(); + } + return ""; +} + + +} // anonymous namespace + + +/// Checks if all the requirements specified by the test case are met. +/// +/// \param md The test metadata. +/// \param cfg The engine configuration. +/// \param test_suite Name of the test suite the test belongs to. +/// \param work_directory Path to where the test case will be run. +/// +/// \return A string describing the reason for skipping the test, or empty if +/// the test should be executed. +std::string +engine::check_reqs(const model::metadata& md, const config::tree& cfg, + const std::string& test_suite, + const fs::path& work_directory) +{ + std::string reason; + + reason = check_required_configs(md.required_configs(), cfg, test_suite); + if (!reason.empty()) + return reason; + + reason = check_allowed_architectures(md.allowed_architectures(), cfg); + if (!reason.empty()) + return reason; + + reason = check_allowed_platforms(md.allowed_platforms(), cfg); + if (!reason.empty()) + return reason; + + reason = check_required_user(md.required_user(), cfg); + if (!reason.empty()) + return reason; + + reason = check_required_files(md.required_files()); + if (!reason.empty()) + return reason; + + reason = check_required_programs(md.required_programs()); + if (!reason.empty()) + return reason; + + reason = check_required_memory(md.required_memory()); + if (!reason.empty()) + return reason; + + reason = check_required_disk_space(md.required_disk_space(), + work_directory); + if (!reason.empty()) + return reason; + + INV(reason.empty()); + return reason; +} diff --git a/engine/requirements.hpp b/engine/requirements.hpp new file mode 100644 index 000000000000..a36a938b3034 --- /dev/null +++ b/engine/requirements.hpp @@ -0,0 +1,51 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/requirements.hpp +/// Handling of test case requirements. + +#if !defined(ENGINE_REQUIREMENTS_HPP) +#define ENGINE_REQUIREMENTS_HPP + +#include + +#include "model/metadata_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace engine { + + +std::string check_reqs(const model::metadata&, const utils::config::tree&, + const std::string&, const utils::fs::path&); + + +} // namespace engine + + +#endif // !defined(ENGINE_REQUIREMENTS_HPP) diff --git a/engine/requirements_test.cpp b/engine/requirements_test.cpp new file mode 100644 index 000000000000..5052da932cb6 --- /dev/null +++ b/engine/requirements_test.cpp @@ -0,0 +1,511 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/metadata.hpp" + +#include + +#include "engine/config.hpp" +#include "engine/requirements.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/memory.hpp" +#include "utils/passwd.hpp" +#include "utils/units.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__none); +ATF_TEST_CASE_BODY(check_reqs__none) +{ + const model::metadata md = model::metadata_builder().build(); + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_architectures__one_ok); +ATF_TEST_CASE_BODY(check_reqs__allowed_architectures__one_ok) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("x86_64") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "x86_64"); + user_config.set_string("platform", ""); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_architectures__one_fail); +ATF_TEST_CASE_BODY(check_reqs__allowed_architectures__one_fail) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("x86_64") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "i386"); + user_config.set_string("platform", ""); + ATF_REQUIRE_MATCH("Current architecture 'i386' not supported", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_architectures__many_ok); +ATF_TEST_CASE_BODY(check_reqs__allowed_architectures__many_ok) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("x86_64") + .add_allowed_architecture("i386") + .add_allowed_architecture("powerpc") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "i386"); + user_config.set_string("platform", ""); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_architectures__many_fail); +ATF_TEST_CASE_BODY(check_reqs__allowed_architectures__many_fail) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("x86_64") + .add_allowed_architecture("i386") + .add_allowed_architecture("powerpc") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "arm"); + user_config.set_string("platform", ""); + ATF_REQUIRE_MATCH("Current architecture 'arm' not supported", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_platforms__one_ok); +ATF_TEST_CASE_BODY(check_reqs__allowed_platforms__one_ok) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_platform("amd64") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", ""); + user_config.set_string("platform", "amd64"); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_platforms__one_fail); +ATF_TEST_CASE_BODY(check_reqs__allowed_platforms__one_fail) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_platform("amd64") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", ""); + user_config.set_string("platform", "i386"); + ATF_REQUIRE_MATCH("Current platform 'i386' not supported", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_platforms__many_ok); +ATF_TEST_CASE_BODY(check_reqs__allowed_platforms__many_ok) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_platform("amd64") + .add_allowed_platform("i386") + .add_allowed_platform("macppc") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", ""); + user_config.set_string("platform", "i386"); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__allowed_platforms__many_fail); +ATF_TEST_CASE_BODY(check_reqs__allowed_platforms__many_fail) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_platform("amd64") + .add_allowed_platform("i386") + .add_allowed_platform("macppc") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", ""); + user_config.set_string("platform", "shark"); + ATF_REQUIRE_MATCH("Current platform 'shark' not supported", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__one_ok); +ATF_TEST_CASE_BODY(check_reqs__required_configs__one_ok) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("my-var") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("test_suites.suite.aaa", "value1"); + user_config.set_string("test_suites.suite.my-var", "value2"); + user_config.set_string("test_suites.suite.zzz", "value3"); + ATF_REQUIRE(engine::check_reqs(md, user_config, "suite", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__one_fail); +ATF_TEST_CASE_BODY(check_reqs__required_configs__one_fail) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("unprivileged_user") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("test_suites.suite.aaa", "value1"); + user_config.set_string("test_suites.suite.my-var", "value2"); + user_config.set_string("test_suites.suite.zzz", "value3"); + ATF_REQUIRE_MATCH("Required configuration property 'unprivileged_user' not " + "defined", + engine::check_reqs(md, user_config, "suite", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__many_ok); +ATF_TEST_CASE_BODY(check_reqs__required_configs__many_ok) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("foo") + .add_required_config("bar") + .add_required_config("baz") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("test_suites.suite.aaa", "value1"); + user_config.set_string("test_suites.suite.foo", "value2"); + user_config.set_string("test_suites.suite.bar", "value3"); + user_config.set_string("test_suites.suite.baz", "value4"); + user_config.set_string("test_suites.suite.zzz", "value5"); + ATF_REQUIRE(engine::check_reqs(md, user_config, "suite", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__many_fail); +ATF_TEST_CASE_BODY(check_reqs__required_configs__many_fail) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("foo") + .add_required_config("bar") + .add_required_config("baz") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set_string("test_suites.suite.aaa", "value1"); + user_config.set_string("test_suites.suite.foo", "value2"); + user_config.set_string("test_suites.suite.zzz", "value3"); + ATF_REQUIRE_MATCH("Required configuration property 'bar' not defined", + engine::check_reqs(md, user_config, "suite", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_configs__special); +ATF_TEST_CASE_BODY(check_reqs__required_configs__special) +{ + const model::metadata md = model::metadata_builder() + .add_required_config("unprivileged-user") + .build(); + + config::tree user_config = engine::default_config(); + ATF_REQUIRE_MATCH("Required configuration property 'unprivileged-user' " + "not defined", + engine::check_reqs(md, user_config, "", fs::path("."))); + user_config.set< engine::user_node >( + "unprivileged_user", passwd::user("foo", 1, 2)); + ATF_REQUIRE(engine::check_reqs(md, user_config, "foo", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__root__ok); +ATF_TEST_CASE_BODY(check_reqs__required_user__root__ok) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("root") + .build(); + + config::tree user_config = engine::default_config(); + ATF_REQUIRE(!user_config.is_set("unprivileged_user")); + + passwd::set_current_user_for_testing(passwd::user("", 0, 1)); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__root__fail); +ATF_TEST_CASE_BODY(check_reqs__required_user__root__fail) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("root") + .build(); + + passwd::set_current_user_for_testing(passwd::user("", 123, 1)); + ATF_REQUIRE_MATCH("Requires root privileges", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__unprivileged__same); +ATF_TEST_CASE_BODY(check_reqs__required_user__unprivileged__same) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("unprivileged") + .build(); + + config::tree user_config = engine::default_config(); + ATF_REQUIRE(!user_config.is_set("unprivileged_user")); + + passwd::set_current_user_for_testing(passwd::user("", 123, 1)); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__unprivileged__ok); +ATF_TEST_CASE_BODY(check_reqs__required_user__unprivileged__ok) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("unprivileged") + .build(); + + config::tree user_config = engine::default_config(); + user_config.set< engine::user_node >( + "unprivileged_user", passwd::user("", 123, 1)); + + passwd::set_current_user_for_testing(passwd::user("", 0, 1)); + ATF_REQUIRE(engine::check_reqs(md, user_config, "", fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_user__unprivileged__fail); +ATF_TEST_CASE_BODY(check_reqs__required_user__unprivileged__fail) +{ + const model::metadata md = model::metadata_builder() + .set_required_user("unprivileged") + .build(); + + config::tree user_config = engine::default_config(); + ATF_REQUIRE(!user_config.is_set("unprivileged_user")); + + passwd::set_current_user_for_testing(passwd::user("", 0, 1)); + ATF_REQUIRE_MATCH("Requires.*unprivileged.*unprivileged-user", + engine::check_reqs(md, user_config, "", fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_disk_space__ok); +ATF_TEST_CASE_BODY(check_reqs__required_disk_space__ok) +{ + const model::metadata md = model::metadata_builder() + .set_required_disk_space(units::bytes::parse("1m")) + .build(); + + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_disk_space__fail); +ATF_TEST_CASE_BODY(check_reqs__required_disk_space__fail) +{ + const model::metadata md = model::metadata_builder() + .set_required_disk_space(units::bytes::parse("1000t")) + .build(); + + ATF_REQUIRE_MATCH("Requires 1000.00T .*disk space", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_files__ok); +ATF_TEST_CASE_BODY(check_reqs__required_files__ok) +{ + const model::metadata md = model::metadata_builder() + .add_required_file(fs::current_path() / "test-file") + .build(); + + atf::utils::create_file("test-file", ""); + + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_files__fail); +ATF_TEST_CASE_BODY(check_reqs__required_files__fail) +{ + const model::metadata md = model::metadata_builder() + .add_required_file(fs::path("/non-existent/file")) + .build(); + + ATF_REQUIRE_MATCH("'/non-existent/file' not found$", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_memory__ok); +ATF_TEST_CASE_BODY(check_reqs__required_memory__ok) +{ + const model::metadata md = model::metadata_builder() + .set_required_memory(units::bytes::parse("1m")) + .build(); + + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_memory__fail); +ATF_TEST_CASE_BODY(check_reqs__required_memory__fail) +{ + const model::metadata md = model::metadata_builder() + .set_required_memory(units::bytes::parse("100t")) + .build(); + + if (utils::physical_memory() == 0) + skip("Don't know how to query the amount of physical memory"); + ATF_REQUIRE_MATCH("Requires 100.00T .*memory", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE(check_reqs__required_programs__ok); +ATF_TEST_CASE_HEAD(check_reqs__required_programs__ok) +{ + set_md_var("require.progs", "/bin/ls /bin/mv"); +} +ATF_TEST_CASE_BODY(check_reqs__required_programs__ok) +{ + const model::metadata md = model::metadata_builder() + .add_required_program(fs::path("/bin/ls")) + .add_required_program(fs::path("foo")) + .add_required_program(fs::path("/bin/mv")) + .build(); + + fs::mkdir(fs::path("bin"), 0755); + atf::utils::create_file("bin/foo", ""); + utils::setenv("PATH", (fs::current_path() / "bin").str()); + + ATF_REQUIRE(engine::check_reqs(md, engine::empty_config(), "", + fs::path(".")).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_programs__fail_absolute); +ATF_TEST_CASE_BODY(check_reqs__required_programs__fail_absolute) +{ + const model::metadata md = model::metadata_builder() + .add_required_program(fs::path("/non-existent/program")) + .build(); + + ATF_REQUIRE_MATCH("'/non-existent/program' not found$", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(check_reqs__required_programs__fail_relative); +ATF_TEST_CASE_BODY(check_reqs__required_programs__fail_relative) +{ + const model::metadata md = model::metadata_builder() + .add_required_program(fs::path("foo")) + .add_required_program(fs::path("bar")) + .build(); + + fs::mkdir(fs::path("bin"), 0755); + atf::utils::create_file("bin/foo", ""); + utils::setenv("PATH", (fs::current_path() / "bin").str()); + + ATF_REQUIRE_MATCH("'bar' not found in PATH$", + engine::check_reqs(md, engine::empty_config(), "", + fs::path("."))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, check_reqs__none); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_architectures__one_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_architectures__one_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_architectures__many_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_architectures__many_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_platforms__one_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_platforms__one_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_platforms__many_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__allowed_platforms__many_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__one_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__one_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__many_ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__many_fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_configs__special); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__root__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__root__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__unprivileged__same); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__unprivileged__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_user__unprivileged__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_disk_space__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_disk_space__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_files__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_files__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_memory__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_memory__fail); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_programs__ok); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_programs__fail_absolute); + ATF_ADD_TEST_CASE(tcs, check_reqs__required_programs__fail_relative); +} diff --git a/engine/scanner.cpp b/engine/scanner.cpp new file mode 100644 index 000000000000..b42b089c3c3c --- /dev/null +++ b/engine/scanner.cpp @@ -0,0 +1,216 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/scanner.hpp" + +#include +#include + +#include "engine/filters.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +using utils::none; +using utils::optional; + + +namespace { + + +/// Extracts the keys of a map as a deque. +/// +/// \tparam KeyType The type of the map keys. +/// \tparam ValueType The type of the map values. +/// \param map The input map. +/// +/// \return A deque with the keys of the map. +template< typename KeyType, typename ValueType > +static std::deque< KeyType > +map_keys(const std::map< KeyType, ValueType >& map) +{ + std::deque< KeyType > keys; + for (typename std::map< KeyType, ValueType >::const_iterator iter = + map.begin(); iter != map.end(); ++iter) { + keys.push_back((*iter).first); + } + return keys; +} + + +} // anonymous namespace + + +/// Internal implementation for the scanner class. +struct engine::scanner::impl : utils::noncopyable { + /// Collection of test programs not yet scanned. + /// + /// The first element in this deque is the "active" test program when + /// first_test_cases is defined. + std::deque< model::test_program_ptr > pending_test_programs; + + /// Current state of the provided filters. + engine::filters_state filters; + + /// Collection of test cases not yet scanned. + /// + /// These are the test cases for the first test program in + /// pending_test_programs when such test program is active. + optional< std::deque< std::string > > first_test_cases; + + /// Constructor. + /// + /// \param test_programs_ Collection of test programs to scan through. + /// \param filters_ List of scan filters as provided by the user. + impl(const model::test_programs_vector& test_programs_, + const std::set< engine::test_filter >& filters_) : + pending_test_programs(test_programs_.begin(), test_programs_.end()), + filters(filters_) + { + } + + /// Positions the internal state to return the next element if any. + /// + /// \post If there are more elements to read, returns true and + /// pending_test_programs[0] points to the active test program and + /// first_test_cases[0] has the test case to be returned. + /// + /// \return True if there is one more result available. + bool + advance(void) + { + for (;;) { + if (first_test_cases) { + if (first_test_cases.get().empty()) { + pending_test_programs.pop_front(); + first_test_cases = none; + } + } + if (pending_test_programs.empty()) { + break; + } + + model::test_program_ptr test_program = pending_test_programs[0]; + if (!first_test_cases) { + if (!filters.match_test_program( + test_program->relative_path())) { + pending_test_programs.pop_front(); + continue; + } + + first_test_cases = utils::make_optional( + map_keys(test_program->test_cases())); + } + + if (!first_test_cases.get().empty()) { + std::deque< std::string >::iterator iter = + first_test_cases.get().begin(); + const std::string test_case_name = *iter; + if (!filters.match_test_case(test_program->relative_path(), + test_case_name)) { + first_test_cases.get().erase(iter); + continue; + } + return true; + } else { + pending_test_programs.pop_front(); + first_test_cases = none; + } + } + return false; + } + + /// Extracts the current element. + /// + /// \pre Must be called only if advance() returns true, and immediately + /// afterwards. + /// + /// \return The current scan result. + engine::scan_result + consume(void) + { + const std::string test_case_name = first_test_cases.get()[0]; + first_test_cases.get().pop_front(); + return scan_result(pending_test_programs[0], test_case_name); + } +}; + + +/// Constructor. +/// +/// \param test_programs Collection of test programs to scan through. +/// \param filters List of scan filters as provided by the user. +engine::scanner::scanner(const model::test_programs_vector& test_programs, + const std::set< engine::test_filter >& filters) : + _pimpl(new impl(test_programs, filters)) +{ +} + + +/// Destructor. +engine::scanner::~scanner(void) +{ +} + + +/// Returns the next scan result. +/// +/// \return A scan result if there are still pending test cases to be processed, +/// or none otherwise. +optional< engine::scan_result > +engine::scanner::yield(void) +{ + if (_pimpl->advance()) { + return utils::make_optional(_pimpl->consume()); + } else { + return none; + } +} + + +/// Checks whether the scan is finished. +/// +/// \return True if the scan is finished, in which case yield() will return +/// none; false otherwise. +bool +engine::scanner::done(void) +{ + return !_pimpl->advance(); +} + + +/// Returns the list of test filters that did not match any test case. +/// +/// \return The collection of unmatched test filters. +std::set< engine::test_filter > +engine::scanner::unused_filters(void) const +{ + return _pimpl->filters.unused(); +} diff --git a/engine/scanner.hpp b/engine/scanner.hpp new file mode 100644 index 000000000000..722bc9be5f4c --- /dev/null +++ b/engine/scanner.hpp @@ -0,0 +1,76 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/scanner.hpp +/// Utilities to scan through list of tests in a test suite. + +#if !defined(ENGINE_SCANNER_HPP) +#define ENGINE_SCANNER_HPP + +#include "engine/scanner_fwd.hpp" + +#include +#include + +#include "engine/filters_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace engine { + + +/// Scans a list of test programs, yielding one test case at a time. +/// +/// This class contains the state necessary to process a collection of test +/// programs (possibly as provided by the Kyuafile) and to extract an arbitrary +/// (test program, test_case) pair out of them one at a time. +/// +/// The scanning algorithm guarantees that test programs are initialized +/// dynamically, should they need to load their list of test cases from disk. +/// +/// The order of the extraction is not guaranteed. +class scanner { + struct impl; + /// Pointer to the internal implementation data. + std::shared_ptr< impl > _pimpl; + +public: + scanner(const model::test_programs_vector&, const std::set< test_filter >&); + ~scanner(void); + + bool done(void); + utils::optional< scan_result > yield(void); + + std::set< test_filter > unused_filters(void) const; +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_SCANNER_HPP) diff --git a/engine/scanner_fwd.hpp b/engine/scanner_fwd.hpp new file mode 100644 index 000000000000..5c91888fa266 --- /dev/null +++ b/engine/scanner_fwd.hpp @@ -0,0 +1,59 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/scanner_fwd.hpp +/// Forward declarations for engine/scanner.hpp + +#if !defined(ENGINE_SCANNER_FWD_HPP) +#define ENGINE_SCANNER_FWD_HPP + +#include +#include + +#include "model/test_program_fwd.hpp" + +namespace engine { + + +/// Result type yielded by the scanner: a (test program, test case name) pair. +/// +/// We must use model::test_program_ptr here instead of model::test_program +/// because we must keep the polimorphic properties of the test program. In +/// particular, if the test program comes from the Kyuafile and is of the type +/// model::lazy_test_program, we must keep access to the loaded list of test +/// cases (which, for obscure reasons, is kept in the subclass). +/// TODO(jmmv): This is ugly, very ugly. There has to be a better way. +typedef std::pair< model::test_program_ptr, std::string > scan_result; + + +class scanner; + + +} // namespace engine + +#endif // !defined(ENGINE_SCANNER_FWD_HPP) diff --git a/engine/scanner_test.cpp b/engine/scanner_test.cpp new file mode 100644 index 000000000000..f79717eca49e --- /dev/null +++ b/engine/scanner_test.cpp @@ -0,0 +1,476 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/scanner.hpp" + +#include +#include +#include + +#include + +#include "engine/filters.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + +namespace fs = utils::fs; + +using utils::optional; + + +namespace { + + +/// Test program that implements a mock test_cases() lazy call. +class mock_test_program : public model::test_program { + /// Number of times test_cases has been called. + mutable std::size_t _num_calls; + + /// Collection of test cases; lazily initialized. + mutable model::test_cases_map _test_cases; + +public: + /// Constructs a new test program. + /// + /// \param binary_ The name of the test program binary relative to root_. + mock_test_program(const fs::path& binary_) : + test_program("unused-interface", binary_, fs::path("unused-root"), + "unused-suite", model::metadata_builder().build(), + model::test_cases_map()), + _num_calls(0) + { + } + + /// Gets or loads the list of test cases from the test program. + /// + /// \return The list of test cases provided by the test program. + const model::test_cases_map& + test_cases(void) const + { + if (_num_calls == 0) { + const model::metadata metadata = model::metadata_builder().build(); + const model::test_case tc1("one", metadata); + const model::test_case tc2("two", metadata); + _test_cases.insert(model::test_cases_map::value_type("one", tc1)); + _test_cases.insert(model::test_cases_map::value_type("two", tc2)); + } + _num_calls++; + return _test_cases; + } + + /// Returns the number of times test_cases() has been called. + /// + /// \return A counter. + std::size_t + num_calls(void) const + { + return _num_calls; + } +}; + + +/// Syntactic sugar to instantiate a test program with various test cases. +/// +/// The scanner only cares about the relative path of the test program object +/// and the names of the test cases. This function helps in instantiating a +/// test program that has the minimum set of details only. +/// +/// \param relative_path Relative path to the test program. +/// \param ... List of test case names to add to the test program. Must be +/// NULL-terminated. +/// +/// \return A constructed test program. +static model::test_program_ptr +new_test_program(const char* relative_path, ...) +{ + model::test_program_builder builder( + "unused-interface", fs::path(relative_path), fs::path("unused-root"), + "unused-suite"); + + va_list ap; + va_start(ap, relative_path); + const char* test_case_name; + while ((test_case_name = va_arg(ap, const char*)) != NULL) { + builder.add_test_case(test_case_name); + } + va_end(ap); + + return builder.build_ptr(); +} + + +/// Yields all test cases in the scanner for simplicity of testing. +/// +/// In most of the tests below, we just care about the scanner returning the +/// full set of matching test cases, not the specific behavior of every single +/// yield() call. This function just returns the whole set, which helps in +/// writing functional tests. +/// +/// \param scanner The scanner on which to iterate. +/// +/// \return The full collection of results yielded by the scanner. +static std::set< engine::scan_result > +yield_all(engine::scanner& scanner) +{ + std::set< engine::scan_result > results; + while (!scanner.done()) { + const optional< engine::scan_result > result = scanner.yield(); + ATF_REQUIRE(result); + results.insert(result.get()); + } + ATF_REQUIRE(!scanner.yield()); + ATF_REQUIRE(scanner.done()); + return results; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__no_tests); +ATF_TEST_CASE_BODY(scanner__no_filters__no_tests) +{ + const model::test_programs_vector test_programs; + const std::set< engine::test_filter > filters; + + engine::scanner scanner(test_programs, filters); + ATF_REQUIRE(scanner.done()); + ATF_REQUIRE(!scanner.yield()); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__one_test_in_one_program); +ATF_TEST_CASE_BODY(scanner__no_filters__one_test_in_one_program) +{ + const model::test_program_ptr test_program = new_test_program( + "dir/program", "lone_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program, "lone_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__one_test_per_many_programs); +ATF_TEST_CASE_BODY(scanner__no_filters__one_test_per_many_programs) +{ + const model::test_program_ptr test_program1 = new_test_program( + "dir/program1", "foo_test", NULL); + const model::test_program_ptr test_program2 = new_test_program( + "program2", "bar_test", NULL); + const model::test_program_ptr test_program3 = new_test_program( + "a/b/c/d/e/program3", "baz_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + test_programs.push_back(test_program3); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "foo_test")); + exp_results.insert(engine::scan_result(test_program2, "bar_test")); + exp_results.insert(engine::scan_result(test_program3, "baz_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__many_tests_in_one_program); +ATF_TEST_CASE_BODY(scanner__no_filters__many_tests_in_one_program) +{ + const model::test_program_ptr test_program = new_test_program( + "dir/program", "first_test", "second_test", "third_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program, "first_test")); + exp_results.insert(engine::scan_result(test_program, "second_test")); + exp_results.insert(engine::scan_result(test_program, "third_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__many_tests_per_many_programs); +ATF_TEST_CASE_BODY(scanner__no_filters__many_tests_per_many_programs) +{ + const model::test_program_ptr test_program1 = new_test_program( + "dir/program1", "foo_test", "bar_test", "baz_test", NULL); + const model::test_program_ptr test_program2 = new_test_program( + "program2", "lone_test", NULL); + const model::test_program_ptr test_program3 = new_test_program( + "a/b/c/d/e/program3", "another_test", "last_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + test_programs.push_back(test_program3); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "foo_test")); + exp_results.insert(engine::scan_result(test_program1, "bar_test")); + exp_results.insert(engine::scan_result(test_program1, "baz_test")); + exp_results.insert(engine::scan_result(test_program2, "lone_test")); + exp_results.insert(engine::scan_result(test_program3, "another_test")); + exp_results.insert(engine::scan_result(test_program3, "last_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__no_filters__verify_lazy_loads); +ATF_TEST_CASE_BODY(scanner__no_filters__verify_lazy_loads) +{ + const model::test_program_ptr test_program1(new mock_test_program( + fs::path("first"))); + const mock_test_program* mock_program1 = + dynamic_cast< const mock_test_program* >(test_program1.get()); + const model::test_program_ptr test_program2(new mock_test_program( + fs::path("second"))); + const mock_test_program* mock_program2 = + dynamic_cast< const mock_test_program* >(test_program2.get()); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + + const std::set< engine::test_filter > filters; + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "one")); + exp_results.insert(engine::scan_result(test_program1, "two")); + exp_results.insert(engine::scan_result(test_program2, "one")); + exp_results.insert(engine::scan_result(test_program2, "two")); + + engine::scanner scanner(test_programs, filters); + std::set< engine::scan_result > results; + ATF_REQUIRE_EQ(0, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + + // This abuses the internal implementation of the scanner by making + // assumptions on the order of the results. + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(1, mock_program2->num_calls()); + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(1, mock_program2->num_calls()); + ATF_REQUIRE(scanner.done()); + + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); + + // Make sure we are still talking to the original objects. + for (std::set< engine::scan_result >::const_iterator iter = results.begin(); + iter != results.end(); ++iter) { + const mock_test_program* mock_program = + dynamic_cast< const mock_test_program* >((*iter).first.get()); + ATF_REQUIRE_EQ(1, mock_program->num_calls()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__with_filters__no_tests); +ATF_TEST_CASE_BODY(scanner__with_filters__no_tests) +{ + const model::test_programs_vector test_programs; + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("foo"), "bar")); + + engine::scanner scanner(test_programs, filters); + ATF_REQUIRE(scanner.done()); + ATF_REQUIRE(!scanner.yield()); + ATF_REQUIRE_EQ(filters, scanner.unused_filters()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__with_filters__no_matches); +ATF_TEST_CASE_BODY(scanner__with_filters__no_matches) +{ + const model::test_program_ptr test_program1 = new_test_program( + "dir/program1", "foo_test", "bar_test", "baz_test", NULL); + const model::test_program_ptr test_program2 = new_test_program( + "dir/program2", "bar_test", NULL); + const model::test_program_ptr test_program3 = new_test_program( + "program3", "another_test", "last_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + test_programs.push_back(test_program3); + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("dir/program2"), "baz_test")); + filters.insert(engine::test_filter(fs::path("program4"), "another_test")); + filters.insert(engine::test_filter(fs::path("dir/program3"), "")); + + const std::set< engine::scan_result > exp_results; + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE_EQ(filters, scanner.unused_filters()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__with_filters__some_matches); +ATF_TEST_CASE_BODY(scanner__with_filters__some_matches) +{ + const model::test_program_ptr test_program1 = new_test_program( + "dir/program1", "foo_test", "bar_test", "baz_test", NULL); + const model::test_program_ptr test_program2 = new_test_program( + "dir/program2", "bar_test", NULL); + const model::test_program_ptr test_program3 = new_test_program( + "program3", "another_test", "last_test", NULL); + const model::test_program_ptr test_program4 = new_test_program( + "program4", "more_test", NULL); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + test_programs.push_back(test_program3); + test_programs.push_back(test_program4); + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("dir/program1"), "baz_test")); + filters.insert(engine::test_filter(fs::path("dir/program2"), "foo_test")); + filters.insert(engine::test_filter(fs::path("program3"), "")); + + std::set< engine::test_filter > exp_filters; + exp_filters.insert(engine::test_filter(fs::path("dir/program2"), + "foo_test")); + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "baz_test")); + exp_results.insert(engine::scan_result(test_program3, "another_test")); + exp_results.insert(engine::scan_result(test_program3, "last_test")); + + engine::scanner scanner(test_programs, filters); + const std::set< engine::scan_result > results = yield_all(scanner); + ATF_REQUIRE_EQ(exp_results, results); + + ATF_REQUIRE_EQ(exp_filters, scanner.unused_filters()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scanner__with_filters__verify_lazy_loads); +ATF_TEST_CASE_BODY(scanner__with_filters__verify_lazy_loads) +{ + const model::test_program_ptr test_program1(new mock_test_program( + fs::path("first"))); + const mock_test_program* mock_program1 = + dynamic_cast< const mock_test_program* >(test_program1.get()); + const model::test_program_ptr test_program2(new mock_test_program( + fs::path("second"))); + const mock_test_program* mock_program2 = + dynamic_cast< const mock_test_program* >(test_program2.get()); + + model::test_programs_vector test_programs; + test_programs.push_back(test_program1); + test_programs.push_back(test_program2); + + std::set< engine::test_filter > filters; + filters.insert(engine::test_filter(fs::path("first"), "")); + + std::set< engine::scan_result > exp_results; + exp_results.insert(engine::scan_result(test_program1, "one")); + exp_results.insert(engine::scan_result(test_program1, "two")); + + engine::scanner scanner(test_programs, filters); + std::set< engine::scan_result > results; + ATF_REQUIRE_EQ(0, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + results.insert(scanner.yield().get()); + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); + ATF_REQUIRE(scanner.done()); + + ATF_REQUIRE_EQ(exp_results, results); + ATF_REQUIRE(scanner.unused_filters().empty()); + + ATF_REQUIRE_EQ(1, mock_program1->num_calls()); + ATF_REQUIRE_EQ(0, mock_program2->num_calls()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__no_tests); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__one_test_in_one_program); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__one_test_per_many_programs); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__many_tests_in_one_program); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__many_tests_per_many_programs); + ATF_ADD_TEST_CASE(tcs, scanner__no_filters__verify_lazy_loads); + + ATF_ADD_TEST_CASE(tcs, scanner__with_filters__no_tests); + ATF_ADD_TEST_CASE(tcs, scanner__with_filters__no_matches); + ATF_ADD_TEST_CASE(tcs, scanner__with_filters__some_matches); + ATF_ADD_TEST_CASE(tcs, scanner__with_filters__verify_lazy_loads); +} diff --git a/engine/scheduler.cpp b/engine/scheduler.cpp new file mode 100644 index 000000000000..e7b51d23acca --- /dev/null +++ b/engine/scheduler.cpp @@ -0,0 +1,1373 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/scheduler.hpp" + +extern "C" { +#include +} + +#include +#include +#include +#include +#include + +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "engine/requirements.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/directory.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/executor.ipp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/stacktrace.hpp" +#include "utils/stream.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace executor = utils::process::executor; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace passwd = utils::passwd; +namespace process = utils::process; +namespace scheduler = engine::scheduler; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +/// Timeout for the test case cleanup operation. +/// +/// TODO(jmmv): This is here only for testing purposes. Maybe we should expose +/// this setting as part of the user_config. +datetime::delta scheduler::cleanup_timeout(60, 0); + + +/// Timeout for the test case listing operation. +/// +/// TODO(jmmv): This is here only for testing purposes. Maybe we should expose +/// this setting as part of the user_config. +datetime::delta scheduler::list_timeout(300, 0); + + +namespace { + + +/// Magic exit status to indicate that the test case was probably skipped. +/// +/// The test case was only skipped if and only if we return this exit code and +/// we find the skipped_cookie file on disk. +static const int exit_skipped = 84; + + +/// Text file containing the skip reason for the test case. +/// +/// This will only be present within unique_work_directory if the test case +/// exited with the exit_skipped code. However, there is no guarantee that the +/// file is there (say if the test really decided to exit with code exit_skipped +/// on its own). +static const char* skipped_cookie = "skipped.txt"; + + +/// Mapping of interface names to interface definitions. +typedef std::map< std::string, std::shared_ptr< scheduler::interface > > + interfaces_map; + + +/// Mapping of interface names to interface definitions. +/// +/// Use register_interface() to add an entry to this global table. +static interfaces_map interfaces; + + +/// Scans the contents of a directory and appends the file listing to a file. +/// +/// \param dir_path The directory to scan. +/// \param output_file The file to which to append the listing. +/// +/// \throw engine::error If there are problems listing the files. +static void +append_files_listing(const fs::path& dir_path, const fs::path& output_file) +{ + std::ofstream output(output_file.c_str(), std::ios::app); + if (!output) + throw engine::error(F("Failed to open output file %s for append") + % output_file); + try { + std::set < std::string > names; + + const fs::directory dir(dir_path); + for (fs::directory::const_iterator iter = dir.begin(); + iter != dir.end(); ++iter) { + if (iter->name != "." && iter->name != "..") + names.insert(iter->name); + } + + if (!names.empty()) { + output << "Files left in work directory after failure: " + << text::join(names, ", ") << '\n'; + } + } catch (const fs::error& e) { + throw engine::error(F("Cannot append files listing to %s: %s") + % output_file % e.what()); + } +} + + +/// Maintenance data held while a test is being executed. +/// +/// This data structure exists from the moment when a test is executed via +/// scheduler::spawn_test() or scheduler::impl::spawn_cleanup() to when it is +/// cleaned up with result_handle::cleanup(). +/// +/// This is a base data type intended to be extended for the test and cleanup +/// cases so that each contains only the relevant data. +struct exec_data : utils::noncopyable { + /// Test program data for this test case. + const model::test_program_ptr test_program; + + /// Name of the test case. + const std::string test_case_name; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + exec_data(const model::test_program_ptr test_program_, + const std::string& test_case_name_) : + test_program(test_program_), test_case_name(test_case_name_) + { + } + + /// Destructor. + virtual ~exec_data(void) + { + } +}; + + +/// Maintenance data held while a test is being executed. +struct test_exec_data : public exec_data { + /// Test program-specific execution interface. + const std::shared_ptr< scheduler::interface > interface; + + /// User configuration passed to the execution of the test. We need this + /// here to recover it later when chaining the execution of a cleanup + /// routine (if any). + const config::tree user_config; + + /// Whether this test case still needs to have its cleanup routine executed. + /// + /// This is set externally when the cleanup routine is actually invoked to + /// denote that no further attempts shall be made at cleaning this up. + bool needs_cleanup; + + /// The exit_handle for this test once it has completed. + /// + /// This is set externally when the test case has finished, as we need this + /// information to invoke the followup cleanup routine in the right context, + /// as indicated by needs_cleanup. + optional< executor::exit_handle > exit_handle; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + /// \param interface_ Test program-specific execution interface. + /// \param user_config_ User configuration passed to the test. + test_exec_data(const model::test_program_ptr test_program_, + const std::string& test_case_name_, + const std::shared_ptr< scheduler::interface > interface_, + const config::tree& user_config_) : + exec_data(test_program_, test_case_name_), + interface(interface_), user_config(user_config_) + { + const model::test_case& test_case = test_program->find(test_case_name); + needs_cleanup = test_case.get_metadata().has_cleanup(); + } +}; + + +/// Maintenance data held while a test cleanup routine is being executed. +/// +/// Instances of this object are related to a previous test_exec_data, as +/// cleanup routines can only exist once the test has been run. +struct cleanup_exec_data : public exec_data { + /// The exit handle of the test. This is necessary so that we can return + /// the correct exit_handle to the user of the scheduler. + executor::exit_handle body_exit_handle; + + /// The final result of the test's body. This is necessary to compute the + /// right return value for a test with a cleanup routine: the body result is + /// respected if it is a "bad" result; else the result of the cleanup + /// routine is used if it has failed. + model::test_result body_result; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + /// \param body_exit_handle_ If not none, exit handle of the body + /// corresponding to the cleanup routine represented by this exec_data. + /// \param body_result_ If not none, result of the body corresponding to the + /// cleanup routine represented by this exec_data. + cleanup_exec_data(const model::test_program_ptr test_program_, + const std::string& test_case_name_, + const executor::exit_handle& body_exit_handle_, + const model::test_result& body_result_) : + exec_data(test_program_, test_case_name_), + body_exit_handle(body_exit_handle_), body_result(body_result_) + { + } +}; + + +/// Shared pointer to exec_data. +/// +/// We require this because we want exec_data to not be copyable, and thus we +/// cannot just store it in the map without move constructors. +typedef std::shared_ptr< exec_data > exec_data_ptr; + + +/// Mapping of active PIDs to their maintenance data. +typedef std::map< int, exec_data_ptr > exec_data_map; + + +/// Enforces a test program to hold an absolute path. +/// +/// TODO(jmmv): This function (which is a pretty ugly hack) exists because we +/// want the interface hooks to receive a test_program as their argument. +/// However, those hooks run after the test program has been isolated, which +/// means that the current directory has changed since when the test_program +/// objects were created. This causes the absolute_path() method of +/// test_program to return bogus values if the internal representation of their +/// path is relative. We should fix somehow: maybe making the fs module grab +/// its "current_path" view at program startup time; or maybe by grabbing the +/// current path at test_program creation time; or maybe something else. +/// +/// \param program The test program to modify. +/// +/// \return A new test program whose internal paths are absolute. +static model::test_program +force_absolute_paths(const model::test_program program) +{ + const std::string& relative = program.relative_path().str(); + const std::string absolute = program.absolute_path().str(); + + const std::string root = absolute.substr( + 0, absolute.length() - relative.length()); + + return model::test_program( + program.interface_name(), + program.relative_path(), fs::path(root), + program.test_suite_name(), + program.get_metadata(), program.test_cases()); +} + + +/// Functor to list the test cases of a test program. +class list_test_cases { + /// Interface of the test program to execute. + std::shared_ptr< scheduler::interface > _interface; + + /// Test program to execute. + const model::test_program _test_program; + + /// User-provided configuration variables. + const config::tree& _user_config; + +public: + /// Constructor. + /// + /// \param interface Interface of the test program to execute. + /// \param test_program Test program to execute. + /// \param user_config User-provided configuration variables. + list_test_cases( + const std::shared_ptr< scheduler::interface > interface, + const model::test_program* test_program, + const config::tree& user_config) : + _interface(interface), + _test_program(force_absolute_paths(*test_program)), + _user_config(user_config) + { + } + + /// Body of the subprocess. + void + operator()(const fs::path& /* control_directory */) + { + const config::properties_map vars = scheduler::generate_config( + _user_config, _test_program.test_suite_name()); + _interface->exec_list(_test_program, vars); + } +}; + + +/// Functor to execute a test program in a child process. +class run_test_program { + /// Interface of the test program to execute. + std::shared_ptr< scheduler::interface > _interface; + + /// Test program to execute. + const model::test_program _test_program; + + /// Name of the test case to execute. + const std::string& _test_case_name; + + /// User-provided configuration variables. + const config::tree& _user_config; + + /// Verifies if the test case needs to be skipped or not. + /// + /// We could very well run this on the scheduler parent process before + /// issuing the fork. However, doing this here in the child process is + /// better for two reasons: first, it allows us to continue using the simple + /// spawn/wait abstraction of the scheduler; and, second, we parallelize the + /// requirements checks among tests. + /// + /// \post If the test's preconditions are not met, the caller process is + /// terminated with a special exit code and a "skipped cookie" is written to + /// the disk with the reason for the failure. + /// + /// \param skipped_cookie_path File to create with the skip reason details + /// if this test is skipped. + void + do_requirements_check(const fs::path& skipped_cookie_path) + { + const model::test_case& test_case = _test_program.find( + _test_case_name); + + const std::string skip_reason = engine::check_reqs( + test_case.get_metadata(), _user_config, + _test_program.test_suite_name(), + fs::current_path()); + if (skip_reason.empty()) + return; + + std::ofstream output(skipped_cookie_path.c_str()); + if (!output) { + std::perror((F("Failed to open %s for write") % + skipped_cookie_path).str().c_str()); + std::abort(); + } + output << skip_reason; + output.close(); + + // Abruptly terminate the process. We don't want to run any destructors + // inherited from the parent process by mistake, which could, for + // example, delete our own control files! + ::_exit(exit_skipped); + } + +public: + /// Constructor. + /// + /// \param interface Interface of the test program to execute. + /// \param test_program Test program to execute. + /// \param test_case_name Name of the test case to execute. + /// \param user_config User-provided configuration variables. + run_test_program( + const std::shared_ptr< scheduler::interface > interface, + const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config) : + _interface(interface), + _test_program(force_absolute_paths(*test_program)), + _test_case_name(test_case_name), + _user_config(user_config) + { + } + + /// Body of the subprocess. + /// + /// \param control_directory The testcase directory where files will be + /// read from. + void + operator()(const fs::path& control_directory) + { + const model::test_case& test_case = _test_program.find( + _test_case_name); + if (test_case.fake_result()) + ::_exit(EXIT_SUCCESS); + + do_requirements_check(control_directory / skipped_cookie); + + const config::properties_map vars = scheduler::generate_config( + _user_config, _test_program.test_suite_name()); + _interface->exec_test(_test_program, _test_case_name, vars, + control_directory); + } +}; + + +/// Functor to execute a test program in a child process. +class run_test_cleanup { + /// Interface of the test program to execute. + std::shared_ptr< scheduler::interface > _interface; + + /// Test program to execute. + const model::test_program _test_program; + + /// Name of the test case to execute. + const std::string& _test_case_name; + + /// User-provided configuration variables. + const config::tree& _user_config; + +public: + /// Constructor. + /// + /// \param interface Interface of the test program to execute. + /// \param test_program Test program to execute. + /// \param test_case_name Name of the test case to execute. + /// \param user_config User-provided configuration variables. + run_test_cleanup( + const std::shared_ptr< scheduler::interface > interface, + const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config) : + _interface(interface), + _test_program(force_absolute_paths(*test_program)), + _test_case_name(test_case_name), + _user_config(user_config) + { + } + + /// Body of the subprocess. + /// + /// \param control_directory The testcase directory where cleanup will be + /// run from. + void + operator()(const fs::path& control_directory) + { + const config::properties_map vars = scheduler::generate_config( + _user_config, _test_program.test_suite_name()); + _interface->exec_cleanup(_test_program, _test_case_name, vars, + control_directory); + } +}; + + +/// Obtains the right scheduler interface for a given test program. +/// +/// \param name The name of the interface of the test program. +/// +/// \return An scheduler interface. +std::shared_ptr< scheduler::interface > +find_interface(const std::string& name) +{ + const interfaces_map::const_iterator iter = interfaces.find(name); + PRE(interfaces.find(name) != interfaces.end()); + return (*iter).second; +} + + +} // anonymous namespace + + +void +scheduler::interface::exec_cleanup( + const model::test_program& /* test_program */, + const std::string& /* test_case_name */, + const config::properties_map& /* vars */, + const utils::fs::path& /* control_directory */) const +{ + // Most test interfaces do not support standalone cleanup routines so + // provide a default implementation that does nothing. + UNREACHABLE_MSG("exec_cleanup not implemented for an interface that " + "supports standalone cleanup routines"); +} + + +/// Internal implementation of a lazy_test_program. +struct engine::scheduler::lazy_test_program::impl : utils::noncopyable { + /// Whether the test cases list has been yet loaded or not. + bool _loaded; + + /// User configuration to pass to the test program list operation. + config::tree _user_config; + + /// Scheduler context to use to load test cases. + scheduler::scheduler_handle& _scheduler_handle; + + /// Constructor. + /// + /// \param user_config_ User configuration to pass to the test program list + /// operation. + /// \param scheduler_handle_ Scheduler context to use when loading test + /// cases. + impl(const config::tree& user_config_, + scheduler::scheduler_handle& scheduler_handle_) : + _loaded(false), _user_config(user_config_), + _scheduler_handle(scheduler_handle_) + { + } +}; + + +/// Constructs a new test program. +/// +/// \param interface_name_ Name of the test program interface. +/// \param binary_ The name of the test program binary relative to root_. +/// \param root_ The root of the test suite containing the test program. +/// \param test_suite_name_ The name of the test suite this program belongs to. +/// \param md_ Metadata of the test program. +/// \param user_config_ User configuration to pass to the scheduler. +/// \param scheduler_handle_ Scheduler context to use to load test cases. +scheduler::lazy_test_program::lazy_test_program( + const std::string& interface_name_, + const fs::path& binary_, + const fs::path& root_, + const std::string& test_suite_name_, + const model::metadata& md_, + const config::tree& user_config_, + scheduler::scheduler_handle& scheduler_handle_) : + test_program(interface_name_, binary_, root_, test_suite_name_, md_, + model::test_cases_map()), + _pimpl(new impl(user_config_, scheduler_handle_)) +{ +} + + +/// Gets or loads the list of test cases from the test program. +/// +/// \return The list of test cases provided by the test program. +const model::test_cases_map& +scheduler::lazy_test_program::test_cases(void) const +{ + _pimpl->_scheduler_handle.check_interrupt(); + + if (!_pimpl->_loaded) { + const model::test_cases_map tcs = _pimpl->_scheduler_handle.list_tests( + this, _pimpl->_user_config); + + // Due to the restrictions on when set_test_cases() may be called (as a + // way to lazily initialize the test cases list before it is ever + // returned), this cast is valid. + const_cast< scheduler::lazy_test_program* >(this)->set_test_cases(tcs); + + _pimpl->_loaded = true; + + _pimpl->_scheduler_handle.check_interrupt(); + } + + INV(_pimpl->_loaded); + return test_program::test_cases(); +} + + +/// Internal implementation for the result_handle class. +struct engine::scheduler::result_handle::bimpl : utils::noncopyable { + /// Generic executor exit handle for this result handle. + executor::exit_handle generic; + + /// Mutable pointer to the corresponding scheduler state. + /// + /// This object references a member of the scheduler_handle that yielded + /// this result_handle instance. We need this direct access to clean up + /// after ourselves when the result is destroyed. + exec_data_map& all_exec_data; + + /// Constructor. + /// + /// \param generic_ Generic executor exit handle for this result handle. + /// \param [in,out] all_exec_data_ Global object keeping track of all active + /// executions for an scheduler. This is a pointer to a member of the + /// scheduler_handle object. + bimpl(const executor::exit_handle generic_, exec_data_map& all_exec_data_) : + generic(generic_), all_exec_data(all_exec_data_) + { + } + + /// Destructor. + ~bimpl(void) + { + LD(F("Removing %s from all_exec_data") % generic.original_pid()); + all_exec_data.erase(generic.original_pid()); + } +}; + + +/// Constructor. +/// +/// \param pbimpl Constructed internal implementation. +scheduler::result_handle::result_handle(std::shared_ptr< bimpl > pbimpl) : + _pbimpl(pbimpl) +{ +} + + +/// Destructor. +scheduler::result_handle::~result_handle(void) +{ +} + + +/// Cleans up the test case results. +/// +/// This function should be called explicitly as it provides the means to +/// control any exceptions raised during cleanup. Do not rely on the destructor +/// to clean things up. +/// +/// \throw engine::error If the cleanup fails, especially due to the inability +/// to remove the work directory. +void +scheduler::result_handle::cleanup(void) +{ + _pbimpl->generic.cleanup(); +} + + +/// Returns the original PID corresponding to this result. +/// +/// \return An exec_handle. +int +scheduler::result_handle::original_pid(void) const +{ + return _pbimpl->generic.original_pid(); +} + + +/// Returns the timestamp of when spawn_test was called. +/// +/// \return A timestamp. +const datetime::timestamp& +scheduler::result_handle::start_time(void) const +{ + return _pbimpl->generic.start_time(); +} + + +/// Returns the timestamp of when wait_any_test returned this object. +/// +/// \return A timestamp. +const datetime::timestamp& +scheduler::result_handle::end_time(void) const +{ + return _pbimpl->generic.end_time(); +} + + +/// Returns the path to the test-specific work directory. +/// +/// This is guaranteed to be clear of files created by the scheduler. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +scheduler::result_handle::work_directory(void) const +{ + return _pbimpl->generic.work_directory(); +} + + +/// Returns the path to the test's stdout file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +scheduler::result_handle::stdout_file(void) const +{ + return _pbimpl->generic.stdout_file(); +} + + +/// Returns the path to the test's stderr file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +scheduler::result_handle::stderr_file(void) const +{ + return _pbimpl->generic.stderr_file(); +} + + +/// Internal implementation for the test_result_handle class. +struct engine::scheduler::test_result_handle::impl : utils::noncopyable { + /// Test program data for this test case. + model::test_program_ptr test_program; + + /// Name of the test case. + std::string test_case_name; + + /// The actual result of the test execution. + const model::test_result test_result; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + /// \param test_result_ The actual result of the test execution. + impl(const model::test_program_ptr test_program_, + const std::string& test_case_name_, + const model::test_result& test_result_) : + test_program(test_program_), + test_case_name(test_case_name_), + test_result(test_result_) + { + } +}; + + +/// Constructor. +/// +/// \param pbimpl Constructed internal implementation for the base object. +/// \param pimpl Constructed internal implementation. +scheduler::test_result_handle::test_result_handle( + std::shared_ptr< bimpl > pbimpl, std::shared_ptr< impl > pimpl) : + result_handle(pbimpl), _pimpl(pimpl) +{ +} + + +/// Destructor. +scheduler::test_result_handle::~test_result_handle(void) +{ +} + + +/// Returns the test program that yielded this result. +/// +/// \return A test program. +const model::test_program_ptr +scheduler::test_result_handle::test_program(void) const +{ + return _pimpl->test_program; +} + + +/// Returns the name of the test case that yielded this result. +/// +/// \return A test case name +const std::string& +scheduler::test_result_handle::test_case_name(void) const +{ + return _pimpl->test_case_name; +} + + +/// Returns the actual result of the test execution. +/// +/// \return A test result. +const model::test_result& +scheduler::test_result_handle::test_result(void) const +{ + return _pimpl->test_result; +} + + +/// Internal implementation for the scheduler_handle. +struct engine::scheduler::scheduler_handle::impl : utils::noncopyable { + /// Generic executor instance encapsulated by this one. + executor::executor_handle generic; + + /// Mapping of exec handles to the data required at run time. + exec_data_map all_exec_data; + + /// Collection of test_exec_data objects. + typedef std::vector< const test_exec_data* > test_exec_data_vector; + + /// Constructor. + impl(void) : generic(executor::setup()) + { + } + + /// Destructor. + /// + /// This runs any pending cleanup routines, which should only happen if the + /// scheduler is abruptly terminated (aka if a signal is received). + ~impl(void) + { + const test_exec_data_vector tests_data = tests_needing_cleanup(); + + for (test_exec_data_vector::const_iterator iter = tests_data.begin(); + iter != tests_data.end(); ++iter) { + const test_exec_data* test_data = *iter; + + try { + sync_cleanup(test_data); + } catch (const std::runtime_error& e) { + LW(F("Failed to run cleanup routine for %s:%s on abrupt " + "termination") + % test_data->test_program->relative_path() + % test_data->test_case_name); + } + } + } + + /// Finds any pending exec_datas that correspond to tests needing cleanup. + /// + /// \return The collection of test_exec_data objects that have their + /// needs_cleanup property set to true. + test_exec_data_vector + tests_needing_cleanup(void) + { + test_exec_data_vector tests_data; + + for (exec_data_map::const_iterator iter = all_exec_data.begin(); + iter != all_exec_data.end(); ++iter) { + const exec_data_ptr data = (*iter).second; + + try { + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *data.get()); + if (test_data->needs_cleanup) { + tests_data.push_back(test_data); + test_data->needs_cleanup = false; + } + } catch (const std::bad_cast& e) { + // Do nothing for cleanup_exec_data objects. + } + } + + return tests_data; + } + + /// Cleans up a single test case synchronously. + /// + /// \param test_data The data of the previously executed test case to be + /// cleaned up. + void + sync_cleanup(const test_exec_data* test_data) + { + // The message in this result should never be seen by the user, but use + // something reasonable just in case it leaks and we need to pinpoint + // the call site. + model::test_result result(model::test_result_broken, + "Test case died abruptly"); + + const executor::exec_handle cleanup_handle = spawn_cleanup( + test_data->test_program, test_data->test_case_name, + test_data->user_config, test_data->exit_handle.get(), + result); + generic.wait(cleanup_handle); + } + + /// Forks and executes a test case cleanup routine asynchronously. + /// + /// \param test_program The container test program. + /// \param test_case_name The name of the test case to run. + /// \param user_config User-provided configuration variables. + /// \param body_handle The exit handle of the test case's corresponding + /// body. The cleanup will be executed in the same context. + /// \param body_result The result of the test case's corresponding body. + /// + /// \return A handle for the background operation. Used to match the result + /// of the execution returned by wait_any() with this invocation. + executor::exec_handle + spawn_cleanup(const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config, + const executor::exit_handle& body_handle, + const model::test_result& body_result) + { + generic.check_interrupt(); + + const std::shared_ptr< scheduler::interface > interface = + find_interface(test_program->interface_name()); + + LI(F("Spawning %s:%s (cleanup)") % test_program->absolute_path() % + test_case_name); + + const executor::exec_handle handle = generic.spawn_followup( + run_test_cleanup(interface, test_program, test_case_name, + user_config), + body_handle, cleanup_timeout); + + const exec_data_ptr data(new cleanup_exec_data( + test_program, test_case_name, body_handle, body_result)); + LD(F("Inserting %s into all_exec_data (cleanup)") % handle.pid()); + INV_MSG(all_exec_data.find(handle.pid()) == all_exec_data.end(), + F("PID %s already in all_exec_data; not properly cleaned " + "up or reused too fast") % handle.pid());; + all_exec_data.insert(exec_data_map::value_type(handle.pid(), data)); + + return handle; + } +}; + + +/// Constructor. +scheduler::scheduler_handle::scheduler_handle(void) : _pimpl(new impl()) +{ +} + + +/// Destructor. +scheduler::scheduler_handle::~scheduler_handle(void) +{ +} + + +/// Queries the path to the root of the work directory for all tests. +/// +/// \return A path. +const fs::path& +scheduler::scheduler_handle::root_work_directory(void) const +{ + return _pimpl->generic.root_work_directory(); +} + + +/// Cleans up the scheduler state. +/// +/// This function should be called explicitly as it provides the means to +/// control any exceptions raised during cleanup. Do not rely on the destructor +/// to clean things up. +/// +/// \throw engine::error If there are problems cleaning up the scheduler. +void +scheduler::scheduler_handle::cleanup(void) +{ + _pimpl->generic.cleanup(); +} + + +/// Checks if the given interface name is valid. +/// +/// \param name The name of the interface to validate. +/// +/// \throw engine::error If the given interface is not supported. +void +scheduler::ensure_valid_interface(const std::string& name) +{ + if (interfaces.find(name) == interfaces.end()) + throw engine::error(F("Unsupported test interface '%s'") % name); +} + + +/// Registers a new interface. +/// +/// \param name The name of the interface. Must not have yet been registered. +/// \param spec Interface specification. +void +scheduler::register_interface(const std::string& name, + const std::shared_ptr< interface > spec) +{ + PRE(interfaces.find(name) == interfaces.end()); + interfaces.insert(interfaces_map::value_type(name, spec)); +} + + +/// Returns the names of all registered interfaces. +/// +/// \return A collection of interface names. +std::set< std::string > +scheduler::registered_interface_names(void) +{ + std::set< std::string > names; + for (interfaces_map::const_iterator iter = interfaces.begin(); + iter != interfaces.end(); ++iter) { + names.insert((*iter).first); + } + return names; +} + + +/// Initializes the scheduler. +/// +/// \pre This function can only be called if there is no other scheduler_handle +/// object alive. +/// +/// \return A handle to the operations of the scheduler. +scheduler::scheduler_handle +scheduler::setup(void) +{ + return scheduler_handle(); +} + + +/// Retrieves the list of test cases from a test program. +/// +/// This operation is currently synchronous. +/// +/// This operation should never throw. Any errors during the processing of the +/// test case list are subsumed into a single test case in the return value that +/// represents the failed retrieval. +/// +/// \param test_program The test program from which to obtain the list of test +/// cases. +/// \param user_config User-provided configuration variables. +/// +/// \return The list of test cases. +model::test_cases_map +scheduler::scheduler_handle::list_tests( + const model::test_program* test_program, + const config::tree& user_config) +{ + _pimpl->generic.check_interrupt(); + + const std::shared_ptr< scheduler::interface > interface = find_interface( + test_program->interface_name()); + + try { + const executor::exec_handle exec_handle = _pimpl->generic.spawn( + list_test_cases(interface, test_program, user_config), + list_timeout, none); + executor::exit_handle exit_handle = _pimpl->generic.wait(exec_handle); + + const model::test_cases_map test_cases = interface->parse_list( + exit_handle.status(), + exit_handle.stdout_file(), + exit_handle.stderr_file()); + + exit_handle.cleanup(); + + if (test_cases.empty()) + throw std::runtime_error("Empty test cases list"); + + return test_cases; + } catch (const std::runtime_error& e) { + // TODO(jmmv): This is a very ugly workaround for the fact that we + // cannot report failures at the test-program level. + LW(F("Failed to load test cases list: %s") % e.what()); + model::test_cases_map fake_test_cases; + fake_test_cases.insert(model::test_cases_map::value_type( + "__test_cases_list__", + model::test_case( + "__test_cases_list__", + "Represents the correct processing of the test cases list", + model::test_result(model::test_result_broken, e.what())))); + return fake_test_cases; + } +} + + +/// Forks and executes a test case asynchronously. +/// +/// Note that the caller needn't know if the test has a cleanup routine or not. +/// If there indeed is a cleanup routine, we trigger it at wait_any() time. +/// +/// \param test_program The container test program. +/// \param test_case_name The name of the test case to run. +/// \param user_config User-provided configuration variables. +/// +/// \return A handle for the background operation. Used to match the result of +/// the execution returned by wait_any() with this invocation. +scheduler::exec_handle +scheduler::scheduler_handle::spawn_test( + const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config) +{ + _pimpl->generic.check_interrupt(); + + const std::shared_ptr< scheduler::interface > interface = find_interface( + test_program->interface_name()); + + LI(F("Spawning %s:%s") % test_program->absolute_path() % test_case_name); + + const model::test_case& test_case = test_program->find(test_case_name); + + optional< passwd::user > unprivileged_user; + if (user_config.is_set("unprivileged_user") && + test_case.get_metadata().required_user() == "unprivileged") { + unprivileged_user = user_config.lookup< engine::user_node >( + "unprivileged_user"); + } + + const executor::exec_handle handle = _pimpl->generic.spawn( + run_test_program(interface, test_program, test_case_name, + user_config), + test_case.get_metadata().timeout(), + unprivileged_user); + + const exec_data_ptr data(new test_exec_data( + test_program, test_case_name, interface, user_config)); + LD(F("Inserting %s into all_exec_data") % handle.pid()); + INV_MSG( + _pimpl->all_exec_data.find(handle.pid()) == _pimpl->all_exec_data.end(), + F("PID %s already in all_exec_data; not cleaned up or reused too fast") + % handle.pid());; + _pimpl->all_exec_data.insert(exec_data_map::value_type(handle.pid(), data)); + + return handle.pid(); +} + + +/// Waits for completion of any forked test case. +/// +/// Note that if the terminated test case has a cleanup routine, this function +/// is the one in charge of spawning the cleanup routine asynchronously. +/// +/// \return The result of the execution of a subprocess. This is a dynamically +/// allocated object because the scheduler can spawn subprocesses of various +/// types and, at wait time, we don't know upfront what we are going to get. +scheduler::result_handle_ptr +scheduler::scheduler_handle::wait_any(void) +{ + _pimpl->generic.check_interrupt(); + + executor::exit_handle handle = _pimpl->generic.wait_any(); + + const exec_data_map::iterator iter = _pimpl->all_exec_data.find( + handle.original_pid()); + exec_data_ptr data = (*iter).second; + + utils::dump_stacktrace_if_available(data->test_program->absolute_path(), + _pimpl->generic, handle); + + optional< model::test_result > result; + try { + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *data.get()); + LD(F("Got %s from all_exec_data") % handle.original_pid()); + + test_data->exit_handle = handle; + + const model::test_case& test_case = test_data->test_program->find( + test_data->test_case_name); + + result = test_case.fake_result(); + + if (!result && handle.status() && handle.status().get().exited() && + handle.status().get().exitstatus() == exit_skipped) { + // If the test's process terminated with our magic "exit_skipped" + // status, there are two cases to handle. The first is the case + // where the "skipped cookie" exists, in which case we never got to + // actually invoke the test program; if that's the case, handle it + // here. The second case is where the test case actually decided to + // exit with the "exit_skipped" status; in that case, just fall back + // to the regular status handling. + const fs::path skipped_cookie_path = handle.control_directory() / + skipped_cookie; + std::ifstream input(skipped_cookie_path.c_str()); + if (input) { + result = model::test_result(model::test_result_skipped, + utils::read_stream(input)); + input.close(); + + // If we determined that the test needs to be skipped, we do not + // want to run the cleanup routine because doing so could result + // in errors. However, we still want to run the cleanup routine + // if the test's body reports a skip (because actions could have + // already been taken). + test_data->needs_cleanup = false; + } + } + if (!result) { + result = test_data->interface->compute_result( + handle.status(), + handle.control_directory(), + handle.stdout_file(), + handle.stderr_file()); + } + INV(result); + + if (!result.get().good()) { + append_files_listing(handle.work_directory(), + handle.stderr_file()); + } + + if (test_data->needs_cleanup) { + INV(test_case.get_metadata().has_cleanup()); + // The test body has completed and we have processed it. If there + // is a cleanup routine, trigger it now and wait for any other test + // completion. The caller never knows about cleanup routines. + _pimpl->spawn_cleanup(test_data->test_program, + test_data->test_case_name, + test_data->user_config, handle, result.get()); + test_data->needs_cleanup = false; + + // TODO(jmmv): Chaining this call is ugly. We'd be better off by + // looping over terminated processes until we got a result suitable + // for user consumption. For the time being this is good enough and + // not a problem because the call chain won't get big: the majority + // of test cases do not have cleanup routines. + return wait_any(); + } + } catch (const std::bad_cast& e) { + const cleanup_exec_data* cleanup_data = + &dynamic_cast< const cleanup_exec_data& >(*data.get()); + LD(F("Got %s from all_exec_data (cleanup)") % handle.original_pid()); + + // Handle the completion of cleanup subprocesses internally: the caller + // is not aware that these exist so, when we return, we must return the + // data for the original test that triggered this routine. For example, + // because the caller wants to see the exact same exec_handle that was + // returned by spawn_test. + + const model::test_result& body_result = cleanup_data->body_result; + if (body_result.good()) { + if (!handle.status()) { + result = model::test_result(model::test_result_broken, + "Test case cleanup timed out"); + } else { + if (!handle.status().get().exited() || + handle.status().get().exitstatus() != EXIT_SUCCESS) { + result = model::test_result( + model::test_result_broken, + "Test case cleanup did not terminate successfully"); + } else { + result = body_result; + } + } + } else { + result = body_result; + } + + // Untrack the cleanup process. This must be done explicitly because we + // do not create a result_handle object for the cleanup, and that is the + // one in charge of doing so in the regular (non-cleanup) case. + LD(F("Removing %s from all_exec_data (cleanup) in favor of %s") + % handle.original_pid() + % cleanup_data->body_exit_handle.original_pid()); + _pimpl->all_exec_data.erase(handle.original_pid()); + + handle = cleanup_data->body_exit_handle; + } + INV(result); + + std::shared_ptr< result_handle::bimpl > result_handle_bimpl( + new result_handle::bimpl(handle, _pimpl->all_exec_data)); + std::shared_ptr< test_result_handle::impl > test_result_handle_impl( + new test_result_handle::impl( + data->test_program, data->test_case_name, result.get())); + return result_handle_ptr(new test_result_handle(result_handle_bimpl, + test_result_handle_impl)); +} + + +/// Forks and executes a test case synchronously for debugging. +/// +/// \pre No other processes should be in execution by the scheduler. +/// +/// \param test_program The container test program. +/// \param test_case_name The name of the test case to run. +/// \param user_config User-provided configuration variables. +/// \param stdout_target File to which to write the stdout of the test case. +/// \param stderr_target File to which to write the stderr of the test case. +/// +/// \return The result of the execution of the test. +scheduler::result_handle_ptr +scheduler::scheduler_handle::debug_test( + const model::test_program_ptr test_program, + const std::string& test_case_name, + const config::tree& user_config, + const fs::path& stdout_target, + const fs::path& stderr_target) +{ + const exec_handle exec_handle = spawn_test( + test_program, test_case_name, user_config); + result_handle_ptr result_handle = wait_any(); + + // TODO(jmmv): We need to do this while the subprocess is alive. This is + // important for debugging purposes, as we should see the contents of stdout + // or stderr as they come in. + // + // Unfortunately, we cannot do so. We cannot just read and block from a + // file, waiting for further output to appear... as this only works on pipes + // or sockets. We need a better interface for this whole thing. + { + std::auto_ptr< std::ostream > output = utils::open_ostream( + stdout_target); + *output << utils::read_file(result_handle->stdout_file()); + } + { + std::auto_ptr< std::ostream > output = utils::open_ostream( + stderr_target); + *output << utils::read_file(result_handle->stderr_file()); + } + + INV(result_handle->original_pid() == exec_handle); + return result_handle; +} + + +/// Checks if an interrupt has fired. +/// +/// Calls to this function should be sprinkled in strategic places through the +/// code protected by an interrupts_handler object. +/// +/// This is just a wrapper over signals::check_interrupt() to avoid leaking this +/// dependency to the caller. +/// +/// \throw signals::interrupted_error If there has been an interrupt. +void +scheduler::scheduler_handle::check_interrupt(void) const +{ + _pimpl->generic.check_interrupt(); +} + + +/// Queries the current execution context. +/// +/// \return The queried context. +model::context +scheduler::current_context(void) +{ + return model::context(fs::current_path(), utils::getallenv()); +} + + +/// Generates the set of configuration variables for a test program. +/// +/// \param user_config The configuration variables provided by the user. +/// \param test_suite The name of the test suite. +/// +/// \return The mapping of configuration variables for the test program. +config::properties_map +scheduler::generate_config(const config::tree& user_config, + const std::string& test_suite) +{ + config::properties_map props; + + try { + props = user_config.all_properties(F("test_suites.%s") % test_suite, + true); + } catch (const config::unknown_key_error& unused_error) { + // Ignore: not all test suites have entries in the configuration. + } + + // TODO(jmmv): This is a hack that exists for the ATF interface only, so it + // should be moved there. + if (user_config.is_set("unprivileged_user")) { + const passwd::user& user = + user_config.lookup< engine::user_node >("unprivileged_user"); + props["unprivileged-user"] = user.name; + } + + return props; +} diff --git a/engine/scheduler.hpp b/engine/scheduler.hpp new file mode 100644 index 000000000000..24ff0b5a26fc --- /dev/null +++ b/engine/scheduler.hpp @@ -0,0 +1,282 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/scheduler.hpp +/// Multiprogrammed executor of test related operations. +/// +/// The scheduler's public interface exposes test cases as "black boxes". The +/// handling of cleanup routines is completely hidden from the caller and +/// happens in two cases: first, once a test case completes; and, second, in the +/// case of abrupt termination due to the reception of a signal. +/// +/// Hiding cleanup routines from the caller is an attempt to keep the logic of +/// execution and results handling in a single place. Otherwise, the various +/// drivers (say run_tests and debug_test) would need to replicate the handling +/// of this logic, which is tricky in itself (particularly due to signal +/// handling) and would lead to inconsistencies. +/// +/// Handling cleanup routines in the manner described above is *incredibly +/// complicated* (insane, actually) as you will see from the code. The +/// complexity will bite us in the future (today is 2015-06-26). Switching to a +/// threads-based implementation would probably simplify the code flow +/// significantly and allow parallelization of the test case listings in a +/// reasonable manner, though it depends on whether we can get clean handling of +/// signals and on whether we could use C++11's std::thread. (Is this a to-do? +/// Maybe. Maybe not.) +/// +/// See the documentation in utils/process/executor.hpp for details on +/// the expected workflow of these classes. + +#if !defined(ENGINE_SCHEDULER_HPP) +#define ENGINE_SCHEDULER_HPP + +#include "engine/scheduler_fwd.hpp" + +#include +#include +#include + +#include "model/context_fwd.hpp" +#include "model/metadata_fwd.hpp" +#include "model/test_case_fwd.hpp" +#include "model/test_program.hpp" +#include "model/test_result_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional.hpp" +#include "utils/process/executor_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace engine { +namespace scheduler { + + +/// Abstract interface of a test program scheduler interface. +/// +/// This interface defines the test program-specific operations that need to be +/// invoked at different points during the execution of a given test case. The +/// scheduler internally instantiates one of these for every test case. +class interface { +public: + /// Destructor. + virtual ~interface() {} + + /// Executes a test program's list operation. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param vars User-provided variables to pass to the test program. + virtual void exec_list(const model::test_program& test_program, + const utils::config::properties_map& vars) + const UTILS_NORETURN = 0; + + /// Computes the test cases list of a test program. + /// + /// \param status The termination status of the subprocess used to execute + /// the exec_test() method or none if the test timed out. + /// \param stdout_path Path to the file containing the stdout of the test. + /// \param stderr_path Path to the file containing the stderr of the test. + /// + /// \return A list of test cases. + virtual model::test_cases_map parse_list( + const utils::optional< utils::process::status >& status, + const utils::fs::path& stdout_path, + const utils::fs::path& stderr_path) const = 0; + + /// Executes a test case of the test program. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param test_case_name Name of the test case to invoke. + /// \param vars User-provided variables to pass to the test program. + /// \param control_directory Directory where the interface may place control + /// files. + virtual void exec_test(const model::test_program& test_program, + const std::string& test_case_name, + const utils::config::properties_map& vars, + const utils::fs::path& control_directory) + const UTILS_NORETURN = 0; + + /// Executes a test cleanup routine of the test program. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param test_case_name Name of the test case to invoke. + /// \param vars User-provided variables to pass to the test program. + /// \param control_directory Directory where the interface may place control + /// files. + virtual void exec_cleanup(const model::test_program& test_program, + const std::string& test_case_name, + const utils::config::properties_map& vars, + const utils::fs::path& control_directory) + const UTILS_NORETURN; + + /// Computes the result of a test case based on its termination status. + /// + /// \param status The termination status of the subprocess used to execute + /// the exec_test() method or none if the test timed out. + /// \param control_directory Directory where the interface may have placed + /// control files. + /// \param stdout_path Path to the file containing the stdout of the test. + /// \param stderr_path Path to the file containing the stderr of the test. + /// + /// \return A test result. + virtual model::test_result compute_result( + const utils::optional< utils::process::status >& status, + const utils::fs::path& control_directory, + const utils::fs::path& stdout_path, + const utils::fs::path& stderr_path) const = 0; +}; + + +/// Implementation of a test program with lazy loading of test cases. +class lazy_test_program : public model::test_program { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + +public: + lazy_test_program(const std::string&, const utils::fs::path&, + const utils::fs::path&, const std::string&, + const model::metadata&, + const utils::config::tree&, + scheduler_handle&); + + const model::test_cases_map& test_cases(void) const; +}; + + +/// Base type containing the results of the execution of a subprocess. +class result_handle { +protected: + struct bimpl; + +private: + /// Pointer to internal implementation of the base type. + std::shared_ptr< bimpl > _pbimpl; + +protected: + friend class scheduler_handle; + result_handle(std::shared_ptr< bimpl >); + +public: + virtual ~result_handle(void) = 0; + + void cleanup(void); + + int original_pid(void) const; + const utils::datetime::timestamp& start_time() const; + const utils::datetime::timestamp& end_time() const; + utils::fs::path work_directory(void) const; + const utils::fs::path& stdout_file(void) const; + const utils::fs::path& stderr_file(void) const; +}; + + +/// Container for all test termination data and accessor to cleanup operations. +class test_result_handle : public result_handle { + struct impl; + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class scheduler_handle; + test_result_handle(std::shared_ptr< bimpl >, std::shared_ptr< impl >); + +public: + ~test_result_handle(void); + + const model::test_program_ptr test_program(void) const; + const std::string& test_case_name(void) const; + const model::test_result& test_result(void) const; +}; + + +/// Stateful interface to the multiprogrammed execution of tests. +class scheduler_handle { + struct impl; + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend scheduler_handle setup(void); + scheduler_handle(void); + +public: + ~scheduler_handle(void); + + const utils::fs::path& root_work_directory(void) const; + + void cleanup(void); + + model::test_cases_map list_tests(const model::test_program*, + const utils::config::tree&); + exec_handle spawn_test(const model::test_program_ptr, + const std::string&, + const utils::config::tree&); + result_handle_ptr wait_any(void); + + result_handle_ptr debug_test(const model::test_program_ptr, + const std::string&, + const utils::config::tree&, + const utils::fs::path&, + const utils::fs::path&); + + void check_interrupt(void) const; +}; + + +extern utils::datetime::delta cleanup_timeout; +extern utils::datetime::delta list_timeout; + + +void ensure_valid_interface(const std::string&); +void register_interface(const std::string&, const std::shared_ptr< interface >); +std::set< std::string > registered_interface_names(void); +scheduler_handle setup(void); + +model::context current_context(void); +utils::config::properties_map generate_config(const utils::config::tree&, + const std::string&); + + +} // namespace scheduler +} // namespace engine + + +#endif // !defined(ENGINE_SCHEDULER_HPP) diff --git a/engine/scheduler_fwd.hpp b/engine/scheduler_fwd.hpp new file mode 100644 index 000000000000..f61b084e5a8d --- /dev/null +++ b/engine/scheduler_fwd.hpp @@ -0,0 +1,61 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/scheduler_fwd.hpp +/// Forward declarations for engine/scheduler.hpp + +#if !defined(ENGINE_SCHEDULER_FWD_HPP) +#define ENGINE_SCHEDULER_FWD_HPP + +#include + +namespace engine { +namespace scheduler { + + +/// Unique identifier for in-flight execution operations. +/// +/// TODO(jmmv): Might be worth to drop altogether and just use "int". The type +/// difference with executor::exec_handle is confusing. +typedef int exec_handle; + + +class scheduler_handle; +class interface; +class result_handle; +class test_result_handle; + + +/// Pointer to a dynamically-allocated result_handle. +typedef std::shared_ptr< result_handle > result_handle_ptr; + + +} // namespace scheduler +} // namespace engine + +#endif // !defined(ENGINE_SCHEDULER_FWD_HPP) diff --git a/engine/scheduler_test.cpp b/engine/scheduler_test.cpp new file mode 100644 index 000000000000..e144761d8f01 --- /dev/null +++ b/engine/scheduler_test.cpp @@ -0,0 +1,1242 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/scheduler.hpp" + +extern "C" { +#include + +#include +#include +} + +#include +#include +#include +#include + +#include + +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/stacktrace.hpp" +#include "utils/stream.hpp" +#include "utils/test_utils.ipp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace process = utils::process; +namespace scheduler = engine::scheduler; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Checks if a string starts with a prefix. +/// +/// \param str The string to be tested. +/// \param prefix The prefix to look for. +/// +/// \return True if the string is prefixed as specified. +static bool +starts_with(const std::string& str, const std::string& prefix) +{ + return (str.length() >= prefix.length() && + str.substr(0, prefix.length()) == prefix); +} + + +/// Strips a prefix from a string and converts the rest to an integer. +/// +/// \param str The string to be tested. +/// \param prefix The prefix to strip from the string. +/// +/// \return The part of the string after the prefix converted to an integer. +static int +suffix_to_int(const std::string& str, const std::string& prefix) +{ + PRE(starts_with(str, prefix)); + try { + return text::to_type< int >(str.substr(prefix.length())); + } catch (const text::value_error& error) { + std::cerr << F("Failed: %s\n") % error.what(); + std::abort(); + } +} + + +/// Mock interface definition for testing. +/// +/// This scheduler interface does not execute external binaries. It is designed +/// to simulate the scheduler of various programs with different exit statuses. +class mock_interface : public scheduler::interface { + /// Executes the subprocess simulating an exec. + /// + /// This is just a simple wrapper over _exit(2) because we cannot use + /// std::exit on exit from this mock interface. The reason is that we do + /// not want to invoke any destructors as otherwise we'd clear up the global + /// scheduler state by mistake. This wouldn't be a major problem if it + /// wasn't because doing so deletes on-disk files and we want to leave them + /// in place so that the parent process can test for them! + /// + /// \param exit_code Exit code. + void + do_exit(const int exit_code) const UTILS_NORETURN + { + std::cout.flush(); + std::cerr.flush(); + ::_exit(exit_code); + } + + /// Executes a test case that creates various files and then fails. + void + exec_create_files_and_fail(void) const UTILS_NORETURN + { + std::cerr << "This should not be clobbered\n"; + atf::utils::create_file("first file", ""); + atf::utils::create_file("second-file", ""); + fs::mkdir_p(fs::path("dir1/dir2"), 0755); + ::kill(::getpid(), SIGTERM); + std::abort(); + } + + /// Executes a test case that deletes all files in the current directory. + /// + /// This is intended to validate that the test runs in an empty directory, + /// separate from any control files that the scheduler may have created. + void + exec_delete_all(void) const UTILS_NORETURN + { + const int exit_code = ::system("rm *") == -1 + ? EXIT_FAILURE : EXIT_SUCCESS; + + // Recreate our own cookie. + atf::utils::create_file("exec_test_was_called", ""); + + do_exit(exit_code); + } + + /// Executes a test case that returns a specific exit code. + /// + /// \param exit_code Exit status to terminate the program with. + void + exec_exit(const int exit_code) const UTILS_NORETURN + { + do_exit(exit_code); + } + + /// Executes a test case that just fails. + void + exec_fail(void) const UTILS_NORETURN + { + std::cerr << "This should not be clobbered\n"; + ::kill(::getpid(), SIGTERM); + std::abort(); + } + + /// Executes a test case that prints all input parameters to the functor. + /// + /// \param test_program The test program to execute. + /// \param test_case_name Name of the test case to invoke, which must be a + /// number. + /// \param vars User-provided variables to pass to the test program. + void + exec_print_params(const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars) const + UTILS_NORETURN + { + std::cout << F("Test program: %s\n") % test_program.relative_path(); + std::cout << F("Test case: %s\n") % test_case_name; + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + std::cout << F("%s=%s\n") % (*iter).first % (*iter).second; + } + + std::cerr << F("stderr: %s\n") % test_case_name; + + do_exit(EXIT_SUCCESS); + } + +public: + /// Executes a test program's list operation. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param vars User-provided variables to pass to the test program. + void + exec_list(const model::test_program& test_program, + const config::properties_map& vars) + const UTILS_NORETURN + { + const std::string name = test_program.absolute_path().leaf_name(); + + std::cerr << name; + std::cerr.flush(); + if (name == "check_i_exist") { + if (fs::exists(test_program.absolute_path())) { + std::cout << "found\n"; + do_exit(EXIT_SUCCESS); + } else { + std::cout << "not_found\n"; + do_exit(EXIT_FAILURE); + } + } else if (name == "empty") { + do_exit(EXIT_SUCCESS); + } else if (name == "misbehave") { + utils::abort_without_coredump(); + } else if (name == "timeout") { + std::cout << "sleeping\n"; + std::cout.flush(); + ::sleep(100); + utils::abort_without_coredump(); + } else if (name == "vars") { + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + std::cout << F("%s_%s\n") % (*iter).first % (*iter).second; + } + do_exit(15); + } else { + std::abort(); + } + } + + /// Computes the test cases list of a test program. + /// + /// \param status The termination status of the subprocess used to execute + /// the exec_test() method or none if the test timed out. + /// \param stdout_path Path to the file containing the stdout of the test. + /// \param stderr_path Path to the file containing the stderr of the test. + /// + /// \return A list of test cases. + model::test_cases_map + parse_list(const optional< process::status >& status, + const fs::path& stdout_path, + const fs::path& stderr_path) const + { + const std::string name = utils::read_file(stderr_path); + if (name == "check_i_exist") { + ATF_REQUIRE(status.get().exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.get().exitstatus()); + } else if (name == "empty") { + ATF_REQUIRE(status.get().exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.get().exitstatus()); + } else if (name == "misbehave") { + throw std::runtime_error("misbehaved in parse_list"); + } else if (name == "timeout") { + ATF_REQUIRE(!status); + } else if (name == "vars") { + ATF_REQUIRE(status.get().exited()); + ATF_REQUIRE_EQ(15, status.get().exitstatus()); + } else { + ATF_FAIL("Invalid stderr contents; got " + name); + } + + model::test_cases_map_builder test_cases_builder; + + std::ifstream input(stdout_path.c_str()); + ATF_REQUIRE(input); + std::string line; + while (std::getline(input, line).good()) { + test_cases_builder.add(line); + } + + return test_cases_builder.build(); + } + + /// Executes a test case of the test program. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_program The test program to execute. + /// \param test_case_name Name of the test case to invoke. + /// \param vars User-provided variables to pass to the test program. + /// \param control_directory Directory where the interface may place control + /// files. + void + exec_test(const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& control_directory) const + { + const fs::path cookie = control_directory / "exec_test_was_called"; + std::ofstream control_file(cookie.c_str()); + if (!control_file) { + std::cerr << "Failed to create " << cookie << '\n'; + std::abort(); + } + control_file << test_case_name; + control_file.close(); + + if (test_case_name == "check_i_exist") { + do_exit(fs::exists(test_program.absolute_path()) ? 0 : 1); + } else if (starts_with(test_case_name, "cleanup_timeout")) { + exec_exit(EXIT_SUCCESS); + } else if (starts_with(test_case_name, "create_files_and_fail")) { + exec_create_files_and_fail(); + } else if (test_case_name == "delete_all") { + exec_delete_all(); + } else if (starts_with(test_case_name, "exit ")) { + exec_exit(suffix_to_int(test_case_name, "exit ")); + } else if (starts_with(test_case_name, "fail")) { + exec_fail(); + } else if (starts_with(test_case_name, "fail_body_fail_cleanup")) { + exec_fail(); + } else if (starts_with(test_case_name, "fail_body_pass_cleanup")) { + exec_fail(); + } else if (starts_with(test_case_name, "pass_body_fail_cleanup")) { + exec_exit(EXIT_SUCCESS); + } else if (starts_with(test_case_name, "print_params")) { + exec_print_params(test_program, test_case_name, vars); + } else if (starts_with(test_case_name, "skip_body_pass_cleanup")) { + exec_exit(EXIT_SUCCESS); + } else { + std::cerr << "Unknown test case " << test_case_name << '\n'; + std::abort(); + } + } + + /// Executes a test cleanup routine of the test program. + /// + /// This method is intended to be called within a subprocess and is expected + /// to terminate execution either by exec(2)ing the test program or by + /// exiting with a failure. + /// + /// \param test_case_name Name of the test case to invoke. + void + exec_cleanup(const model::test_program& /* test_program */, + const std::string& test_case_name, + const config::properties_map& /* vars */, + const fs::path& /* control_directory */) const + { + std::cout << "exec_cleanup was called\n"; + std::cout.flush(); + + if (starts_with(test_case_name, "cleanup_timeout")) { + ::sleep(100); + std::abort(); + } else if (starts_with(test_case_name, "fail_body_fail_cleanup")) { + exec_fail(); + } else if (starts_with(test_case_name, "fail_body_pass_cleanup")) { + exec_exit(EXIT_SUCCESS); + } else if (starts_with(test_case_name, "pass_body_fail_cleanup")) { + exec_fail(); + } else if (starts_with(test_case_name, "skip_body_pass_cleanup")) { + exec_exit(EXIT_SUCCESS); + } else { + std::cerr << "Should not have been called for a test without " + "a cleanup routine" << '\n'; + std::abort(); + } + } + + /// Computes the result of a test case based on its termination status. + /// + /// \param status The termination status of the subprocess used to execute + /// the exec_test() method or none if the test timed out. + /// \param control_directory Path to the directory where the interface may + /// have placed control files. + /// \param stdout_path Path to the file containing the stdout of the test. + /// \param stderr_path Path to the file containing the stderr of the test. + /// + /// \return A test result. + model::test_result + compute_result(const optional< process::status >& status, + const fs::path& control_directory, + const fs::path& stdout_path, + const fs::path& stderr_path) const + { + // Do not use any ATF_* macros here. Some of the tests below invoke + // this code in a subprocess, and terminating such subprocess due to a + // failed ATF_* macro yields mysterious failures that are incredibly + // hard to debug. (Case in point: the signal_handling test is racy by + // nature, and the test run by exec_test() above may not have created + // the cookie we expect below. We don't want to "silently" exit if the + // file is not there.) + + if (!status) { + return model::test_result(model::test_result_broken, + "Timed out"); + } + + if (status.get().exited()) { + // Only sanity-check the work directory-related parameters in case + // of a clean exit. In all other cases, there is no guarantee that + // these were ever created. + const fs::path cookie = control_directory / "exec_test_was_called"; + if (!atf::utils::file_exists(cookie.str())) { + return model::test_result( + model::test_result_broken, + "compute_result's control_directory does not seem to point " + "to the right location"); + } + const std::string test_case_name = utils::read_file(cookie); + + if (!atf::utils::file_exists(stdout_path.str())) { + return model::test_result( + model::test_result_broken, + "compute_result's stdout_path does not exist"); + } + if (!atf::utils::file_exists(stderr_path.str())) { + return model::test_result( + model::test_result_broken, + "compute_result's stderr_path does not exist"); + } + + if (test_case_name == "skip_body_pass_cleanup") { + return model::test_result( + model::test_result_skipped, + F("Exit %s") % status.get().exitstatus()); + } else { + return model::test_result( + model::test_result_passed, + F("Exit %s") % status.get().exitstatus()); + } + } else { + return model::test_result( + model::test_result_failed, + F("Signal %s") % status.get().termsig()); + } + } +}; + + +} // anonymous namespace + + +/// Runs list_tests on the scheduler and returns the results. +/// +/// \param test_name The name of the test supported by our exec_list function. +/// \param user_config Optional user settings for the test. +/// +/// \return The loaded list of test cases. +static model::test_cases_map +check_integration_list(const char* test_name, const fs::path root, + const config::tree& user_config = engine::empty_config()) +{ + const model::test_program program = model::test_program_builder( + "mock", fs::path(test_name), root, "the-suite") + .build(); + + scheduler::scheduler_handle handle = scheduler::setup(); + const model::test_cases_map test_cases = handle.list_tests(&program, + user_config); + handle.cleanup(); + + return test_cases; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_some); +ATF_TEST_CASE_BODY(integration__list_some) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.first", "test"); + user_config.set_string("test_suites.the-suite.second", "TEST"); + user_config.set_string("test_suites.abc.unused", "unused"); + + const model::test_cases_map test_cases = check_integration_list( + "vars", fs::path("."), user_config); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("first_test").add("second_TEST").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_check_paths); +ATF_TEST_CASE_BODY(integration__list_check_paths) +{ + fs::mkdir_p(fs::path("dir1/dir2/dir3"), 0755); + atf::utils::create_file("dir1/dir2/dir3/check_i_exist", ""); + + const model::test_cases_map test_cases = check_integration_list( + "dir2/dir3/check_i_exist", fs::path("dir1")); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("found").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_timeout); +ATF_TEST_CASE_BODY(integration__list_timeout) +{ + scheduler::list_timeout = datetime::delta(1, 0); + const model::test_cases_map test_cases = check_integration_list( + "timeout", fs::path(".")); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("sleeping").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_fail); +ATF_TEST_CASE_BODY(integration__list_fail) +{ + const model::test_cases_map test_cases = check_integration_list( + "misbehave", fs::path(".")); + + ATF_REQUIRE_EQ(1, test_cases.size()); + const model::test_case& test_case = test_cases.begin()->second; + ATF_REQUIRE_EQ("__test_cases_list__", test_case.name()); + ATF_REQUIRE(test_case.fake_result()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_broken, + "misbehaved in parse_list"), + test_case.fake_result().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_empty); +ATF_TEST_CASE_BODY(integration__list_empty) +{ + const model::test_cases_map test_cases = check_integration_list( + "empty", fs::path(".")); + + ATF_REQUIRE_EQ(1, test_cases.size()); + const model::test_case& test_case = test_cases.begin()->second; + ATF_REQUIRE_EQ("__test_cases_list__", test_case.name()); + ATF_REQUIRE(test_case.fake_result()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_broken, + "Empty test cases list"), + test_case.fake_result().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_one); +ATF_TEST_CASE_BODY(integration__run_one) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("exit 41").build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + const scheduler::exec_handle exec_handle = handle.spawn_test( + program, "exit 41", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(exec_handle, result_handle->original_pid()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 41"), + test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_many); +ATF_TEST_CASE_BODY(integration__run_many) +{ + static const std::size_t num_test_programs = 30; + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + // We mess around with the "current time" below, so make sure the tests do + // not spuriously exceed their deadline by bumping it to a large number. + const model::metadata infinite_timeout = model::metadata_builder() + .set_timeout(datetime::delta(1000000L, 0)).build(); + + std::size_t total_tests = 0; + std::map< scheduler::exec_handle, model::test_program_ptr > + exp_test_programs; + std::map< scheduler::exec_handle, std::string > exp_test_case_names; + std::map< scheduler::exec_handle, datetime::timestamp > exp_start_times; + std::map< scheduler::exec_handle, int > exp_exit_statuses; + for (std::size_t i = 0; i < num_test_programs; ++i) { + const std::string test_case_0 = F("exit %s") % (i * 3 + 0); + const std::string test_case_1 = F("exit %s") % (i * 3 + 1); + const std::string test_case_2 = F("exit %s") % (i * 3 + 2); + + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path(F("program-%s") % i), + fs::current_path(), "the-suite") + .set_metadata(infinite_timeout) + .add_test_case(test_case_0) + .add_test_case(test_case_1) + .add_test_case(test_case_2) + .build_ptr(); + + const datetime::timestamp start_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 40, 0, i); + + scheduler::exec_handle exec_handle; + + datetime::set_mock_now(start_time); + exec_handle = handle.spawn_test(program, test_case_0, user_config); + exp_test_programs.insert(std::make_pair(exec_handle, program)); + exp_test_case_names.insert(std::make_pair(exec_handle, test_case_0)); + exp_start_times.insert(std::make_pair(exec_handle, start_time)); + exp_exit_statuses.insert(std::make_pair(exec_handle, i * 3)); + ++total_tests; + + datetime::set_mock_now(start_time); + exec_handle = handle.spawn_test(program, test_case_1, user_config); + exp_test_programs.insert(std::make_pair(exec_handle, program)); + exp_test_case_names.insert(std::make_pair(exec_handle, test_case_1)); + exp_start_times.insert(std::make_pair(exec_handle, start_time)); + exp_exit_statuses.insert(std::make_pair(exec_handle, i * 3 + 1)); + ++total_tests; + + datetime::set_mock_now(start_time); + exec_handle = handle.spawn_test(program, test_case_2, user_config); + exp_test_programs.insert(std::make_pair(exec_handle, program)); + exp_test_case_names.insert(std::make_pair(exec_handle, test_case_2)); + exp_start_times.insert(std::make_pair(exec_handle, start_time)); + exp_exit_statuses.insert(std::make_pair(exec_handle, i * 3 + 2)); + ++total_tests; + } + + for (std::size_t i = 0; i < total_tests; ++i) { + const datetime::timestamp end_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 50, 10, i); + datetime::set_mock_now(end_time); + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + const scheduler::exec_handle exec_handle = + result_handle->original_pid(); + + const model::test_program_ptr test_program = exp_test_programs.find( + exec_handle)->second; + const std::string& test_case_name = exp_test_case_names.find( + exec_handle)->second; + const datetime::timestamp& start_time = exp_start_times.find( + exec_handle)->second; + const int exit_status = exp_exit_statuses.find(exec_handle)->second; + + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, + F("Exit %s") % exit_status), + test_result_handle->test_result()); + + ATF_REQUIRE_EQ(test_program, test_result_handle->test_program()); + ATF_REQUIRE_EQ(test_case_name, test_result_handle->test_case_name()); + + ATF_REQUIRE_EQ(start_time, result_handle->start_time()); + ATF_REQUIRE_EQ(end_time, result_handle->end_time()); + + result_handle->cleanup(); + + ATF_REQUIRE(!atf::utils::file_exists( + result_handle->stdout_file().str())); + ATF_REQUIRE(!atf::utils::file_exists( + result_handle->stderr_file().str())); + ATF_REQUIRE(!atf::utils::file_exists( + result_handle->work_directory().str())); + + result_handle.reset(); + } + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_check_paths); +ATF_TEST_CASE_BODY(integration__run_check_paths) +{ + fs::mkdir_p(fs::path("dir1/dir2/dir3"), 0755); + atf::utils::create_file("dir1/dir2/dir3/program", ""); + + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("dir2/dir3/program"), fs::path("dir1"), "the-suite") + .add_test_case("check_i_exist").build_ptr(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "check_i_exist", engine::default_config()); + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 0"), + test_result_handle->test_result()); + + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__parameters_and_output); +ATF_TEST_CASE_BODY(integration__parameters_and_output) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("print_params").build_ptr(); + + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.one", "first variable"); + user_config.set_string("test_suites.the-suite.two", "second variable"); + + scheduler::scheduler_handle handle = scheduler::setup(); + + const scheduler::exec_handle exec_handle = handle.spawn_test( + program, "print_params", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + ATF_REQUIRE_EQ(exec_handle, result_handle->original_pid()); + ATF_REQUIRE_EQ(program, test_result_handle->test_program()); + ATF_REQUIRE_EQ("print_params", test_result_handle->test_case_name()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 0"), + test_result_handle->test_result()); + + const fs::path stdout_file = result_handle->stdout_file(); + ATF_REQUIRE(atf::utils::compare_file( + stdout_file.str(), + "Test program: the-program\n" + "Test case: print_params\n" + "one=first variable\n" + "two=second variable\n")); + const fs::path stderr_file = result_handle->stderr_file(); + ATF_REQUIRE(atf::utils::compare_file( + stderr_file.str(), "stderr: print_params\n")); + + result_handle->cleanup(); + ATF_REQUIRE(!fs::exists(stdout_file)); + ATF_REQUIRE(!fs::exists(stderr_file)); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__fake_result); +ATF_TEST_CASE_BODY(integration__fake_result) +{ + const model::test_result fake_result(model::test_result_skipped, + "Some fake details"); + + model::test_cases_map test_cases; + test_cases.insert(model::test_cases_map::value_type( + "__fake__", model::test_case("__fake__", "ABC", fake_result))); + + const model::test_program_ptr program(new model::test_program( + "mock", fs::path("the-program"), fs::current_path(), "the-suite", + model::metadata_builder().build(), test_cases)); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "__fake__", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(fake_result, test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__head_skips); +ATF_TEST_CASE_BODY(integration__cleanup__head_skips) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("skip_me", + model::metadata_builder() + .add_required_config("variable-that-does-not-exist") + .set_has_cleanup(true) + .build()) + .build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "skip_me", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(model::test_result( + model::test_result_skipped, + "Required configuration property " + "'variable-that-does-not-exist' not defined"), + test_result_handle->test_result()); + ATF_REQUIRE(!atf::utils::grep_file("exec_cleanup was called", + result_handle->stdout_file().str())); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +/// Runs a test to verify the behavior of cleanup routines. +/// +/// \param test_case The name of the test case to invoke. +/// \param exp_result The expected test result of the execution. +static void +do_cleanup_test(const char* test_case, + const model::test_result& exp_result) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case(test_case) + .set_metadata(model::metadata_builder().set_has_cleanup(true).build()) + .build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, test_case, user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(exp_result, test_result_handle->test_result()); + ATF_REQUIRE(atf::utils::compare_file( + result_handle->stdout_file().str(), + "exec_cleanup was called\n")); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__body_skips); +ATF_TEST_CASE_BODY(integration__cleanup__body_skips) +{ + do_cleanup_test( + "skip_body_pass_cleanup", + model::test_result(model::test_result_skipped, "Exit 0")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__body_bad__cleanup_ok); +ATF_TEST_CASE_BODY(integration__cleanup__body_bad__cleanup_ok) +{ + do_cleanup_test( + "fail_body_pass_cleanup", + model::test_result(model::test_result_failed, "Signal 15")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__body_ok__cleanup_bad); +ATF_TEST_CASE_BODY(integration__cleanup__body_ok__cleanup_bad) +{ + do_cleanup_test( + "pass_body_fail_cleanup", + model::test_result(model::test_result_broken, "Test case cleanup " + "did not terminate successfully")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__body_bad__cleanup_bad); +ATF_TEST_CASE_BODY(integration__cleanup__body_bad__cleanup_bad) +{ + do_cleanup_test( + "fail_body_fail_cleanup", + model::test_result(model::test_result_failed, "Signal 15")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__cleanup__timeout); +ATF_TEST_CASE_BODY(integration__cleanup__timeout) +{ + scheduler::cleanup_timeout = datetime::delta(1, 0); + do_cleanup_test( + "cleanup_timeout", + model::test_result(model::test_result_broken, "Test case cleanup " + "timed out")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__check_requirements); +ATF_TEST_CASE_BODY(integration__check_requirements) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("exit 12") + .set_metadata(model::metadata_builder() + .add_required_config("abcde").build()) + .build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "exit 12", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(model::test_result( + model::test_result_skipped, + "Required configuration property 'abcde' not defined"), + test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__stacktrace); +ATF_TEST_CASE_BODY(integration__stacktrace) +{ + utils::prepare_coredump_test(this); + + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("unknown-dumps-core").build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "unknown-dumps-core", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_failed, + F("Signal %s") % SIGABRT), + test_result_handle->test_result()); + ATF_REQUIRE(!atf::utils::grep_file("attempting to gather stack trace", + result_handle->stdout_file().str())); + ATF_REQUIRE( atf::utils::grep_file("attempting to gather stack trace", + result_handle->stderr_file().str())); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +/// Runs a test to verify the dumping of the list of existing files on failure. +/// +/// \param test_case The name of the test case to invoke. +/// \param exp_stderr Expected contents of stderr. +static void +do_check_list_files_on_failure(const char* test_case, const char* exp_stderr) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case(test_case).build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, test_case, user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + atf::utils::cat_file(result_handle->stdout_file().str(), "child stdout: "); + ATF_REQUIRE(atf::utils::compare_file(result_handle->stdout_file().str(), + "")); + atf::utils::cat_file(result_handle->stderr_file().str(), "child stderr: "); + ATF_REQUIRE(atf::utils::compare_file(result_handle->stderr_file().str(), + exp_stderr)); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_files_on_failure__none); +ATF_TEST_CASE_BODY(integration__list_files_on_failure__none) +{ + do_check_list_files_on_failure("fail", "This should not be clobbered\n"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__list_files_on_failure__some); +ATF_TEST_CASE_BODY(integration__list_files_on_failure__some) +{ + do_check_list_files_on_failure( + "create_files_and_fail", + "This should not be clobbered\n" + "Files left in work directory after failure: " + "dir1, first file, second-file\n"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__prevent_clobbering_control_files); +ATF_TEST_CASE_BODY(integration__prevent_clobbering_control_files) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("delete_all").build_ptr(); + + const config::tree user_config = engine::empty_config(); + + scheduler::scheduler_handle handle = scheduler::setup(); + + (void)handle.spawn_test(program, "delete_all", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 0"), + test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(debug_test); +ATF_TEST_CASE_BODY(debug_test) +{ + const model::test_program_ptr program = model::test_program_builder( + "mock", fs::path("the-program"), fs::current_path(), "the-suite") + .add_test_case("print_params").build_ptr(); + + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.the-suite.one", "first variable"); + user_config.set_string("test_suites.the-suite.two", "second variable"); + + scheduler::scheduler_handle handle = scheduler::setup(); + + const fs::path stdout_file("custom-stdout.txt"); + const fs::path stderr_file("custom-stderr.txt"); + + scheduler::result_handle_ptr result_handle = handle.debug_test( + program, "print_params", user_config, stdout_file, stderr_file); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + + ATF_REQUIRE_EQ(program, test_result_handle->test_program()); + ATF_REQUIRE_EQ("print_params", test_result_handle->test_case_name()); + ATF_REQUIRE_EQ(model::test_result(model::test_result_passed, "Exit 0"), + test_result_handle->test_result()); + + // The original output went to a file. It's only an artifact of + // debug_test() that we later get a copy in our own files. + ATF_REQUIRE(stdout_file != result_handle->stdout_file()); + ATF_REQUIRE(stderr_file != result_handle->stderr_file()); + + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); + + ATF_REQUIRE(atf::utils::compare_file( + stdout_file.str(), + "Test program: the-program\n" + "Test case: print_params\n" + "one=first variable\n" + "two=second variable\n")); + ATF_REQUIRE(atf::utils::compare_file( + stderr_file.str(), "stderr: print_params\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ensure_valid_interface); +ATF_TEST_CASE_BODY(ensure_valid_interface) +{ + scheduler::ensure_valid_interface("mock"); + + ATF_REQUIRE_THROW_RE(engine::error, "Unsupported test interface 'mock2'", + scheduler::ensure_valid_interface("mock2")); + scheduler::register_interface( + "mock2", std::shared_ptr< scheduler::interface >(new mock_interface())); + scheduler::ensure_valid_interface("mock2"); + + // Standard interfaces should not be present unless registered. + ATF_REQUIRE_THROW_RE(engine::error, "Unsupported test interface 'plain'", + scheduler::ensure_valid_interface("plain")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(registered_interface_names); +ATF_TEST_CASE_BODY(registered_interface_names) +{ + std::set< std::string > exp_names; + + exp_names.insert("mock"); + ATF_REQUIRE_EQ(exp_names, scheduler::registered_interface_names()); + + scheduler::register_interface( + "mock2", std::shared_ptr< scheduler::interface >(new mock_interface())); + exp_names.insert("mock2"); + ATF_REQUIRE_EQ(exp_names, scheduler::registered_interface_names()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_context); +ATF_TEST_CASE_BODY(current_context) +{ + const model::context context = scheduler::current_context(); + ATF_REQUIRE_EQ(fs::current_path(), context.cwd()); + ATF_REQUIRE(utils::getallenv() == context.env()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_config__empty); +ATF_TEST_CASE_BODY(generate_config__empty) +{ + const config::tree user_config = engine::empty_config(); + + const config::properties_map exp_props; + + ATF_REQUIRE_EQ(exp_props, + scheduler::generate_config(user_config, "missing")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_config__no_matches); +ATF_TEST_CASE_BODY(generate_config__no_matches) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("architecture", "foo"); + user_config.set_string("test_suites.one.var1", "value 1"); + + const config::properties_map exp_props; + + ATF_REQUIRE_EQ(exp_props, + scheduler::generate_config(user_config, "two")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_config__some_matches); +ATF_TEST_CASE_BODY(generate_config__some_matches) +{ + std::vector< passwd::user > mock_users; + mock_users.push_back(passwd::user("nobody", 1234, 5678)); + passwd::set_mock_users_for_testing(mock_users); + + config::tree user_config = engine::empty_config(); + user_config.set_string("architecture", "foo"); + user_config.set_string("unprivileged_user", "nobody"); + user_config.set_string("test_suites.one.var1", "value 1"); + user_config.set_string("test_suites.two.var2", "value 2"); + + config::properties_map exp_props; + exp_props["unprivileged-user"] = "nobody"; + exp_props["var1"] = "value 1"; + + ATF_REQUIRE_EQ(exp_props, + scheduler::generate_config(user_config, "one")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "mock", std::shared_ptr< scheduler::interface >(new mock_interface())); + + ATF_ADD_TEST_CASE(tcs, integration__list_some); + ATF_ADD_TEST_CASE(tcs, integration__list_check_paths); + ATF_ADD_TEST_CASE(tcs, integration__list_timeout); + ATF_ADD_TEST_CASE(tcs, integration__list_fail); + ATF_ADD_TEST_CASE(tcs, integration__list_empty); + + ATF_ADD_TEST_CASE(tcs, integration__run_one); + ATF_ADD_TEST_CASE(tcs, integration__run_many); + + ATF_ADD_TEST_CASE(tcs, integration__run_check_paths); + ATF_ADD_TEST_CASE(tcs, integration__parameters_and_output); + + ATF_ADD_TEST_CASE(tcs, integration__fake_result); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__head_skips); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__body_skips); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__body_ok__cleanup_bad); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__body_bad__cleanup_ok); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__body_bad__cleanup_bad); + ATF_ADD_TEST_CASE(tcs, integration__cleanup__timeout); + ATF_ADD_TEST_CASE(tcs, integration__check_requirements); + ATF_ADD_TEST_CASE(tcs, integration__stacktrace); + ATF_ADD_TEST_CASE(tcs, integration__list_files_on_failure__none); + ATF_ADD_TEST_CASE(tcs, integration__list_files_on_failure__some); + ATF_ADD_TEST_CASE(tcs, integration__prevent_clobbering_control_files); + + ATF_ADD_TEST_CASE(tcs, debug_test); + + ATF_ADD_TEST_CASE(tcs, ensure_valid_interface); + ATF_ADD_TEST_CASE(tcs, registered_interface_names); + + ATF_ADD_TEST_CASE(tcs, current_context); + + ATF_ADD_TEST_CASE(tcs, generate_config__empty); + ATF_ADD_TEST_CASE(tcs, generate_config__no_matches); + ATF_ADD_TEST_CASE(tcs, generate_config__some_matches); +} diff --git a/engine/tap.cpp b/engine/tap.cpp new file mode 100644 index 000000000000..85e23857f5b7 --- /dev/null +++ b/engine/tap.cpp @@ -0,0 +1,191 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/tap.hpp" + +extern "C" { +#include +} + +#include + +#include "engine/exceptions.hpp" +#include "engine/tap_parser.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::optional; + + +namespace { + + +/// Computes the result of a TAP test program termination. +/// +/// Timeouts and bad TAP data must be handled by the caller. Here we assume +/// that we have been able to successfully parse the TAP output. +/// +/// \param summary Parsed TAP data for the test program. +/// \param status Exit status of the test program. +/// +/// \return A test result. +static model::test_result +tap_to_result(const engine::tap_summary& summary, + const process::status& status) +{ + if (summary.bailed_out()) { + return model::test_result(model::test_result_failed, "Bailed out"); + } + + if (summary.plan() == engine::all_skipped_plan) { + return model::test_result(model::test_result_skipped, + summary.all_skipped_reason()); + } + + if (summary.not_ok_count() == 0) { + if (status.exitstatus() == EXIT_SUCCESS) { + return model::test_result(model::test_result_passed); + } else { + return model::test_result( + model::test_result_broken, + F("Dubious test program: reported all tests as passed " + "but returned exit code %s") % status.exitstatus()); + } + } else { + const std::size_t total = summary.ok_count() + summary.not_ok_count(); + return model::test_result(model::test_result_failed, + F("%s of %s tests failed") % + summary.not_ok_count() % total); + } +} + + +} // anonymous namespace + + +/// Executes a test program's list operation. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +void +engine::tap_interface::exec_list( + const model::test_program& /* test_program */, + const config::properties_map& /* vars */) const +{ + ::_exit(EXIT_SUCCESS); +} + + +/// Computes the test cases list of a test program. +/// +/// \return A list of test cases. +model::test_cases_map +engine::tap_interface::parse_list( + const optional< process::status >& /* status */, + const fs::path& /* stdout_path */, + const fs::path& /* stderr_path */) const +{ + return model::test_cases_map_builder().add("main").build(); +} + + +/// Executes a test case of the test program. +/// +/// This method is intended to be called within a subprocess and is expected +/// to terminate execution either by exec(2)ing the test program or by +/// exiting with a failure. +/// +/// \param test_program The test program to execute. +/// \param test_case_name Name of the test case to invoke. +/// \param vars User-provided variables to pass to the test program. +void +engine::tap_interface::exec_test( + const model::test_program& test_program, + const std::string& test_case_name, + const config::properties_map& vars, + const fs::path& /* control_directory */) const +{ + PRE(test_case_name == "main"); + + for (config::properties_map::const_iterator iter = vars.begin(); + iter != vars.end(); ++iter) { + utils::setenv(F("TEST_ENV_%s") % (*iter).first, (*iter).second); + } + + process::args_vector args; + process::exec(test_program.absolute_path(), args); +} + + +/// Computes the result of a test case based on its termination status. +/// +/// \param status The termination status of the subprocess used to execute +/// the exec_test() method or none if the test timed out. +/// \param stdout_path Path to the file containing the stdout of the test. +/// +/// \return A test result. +model::test_result +engine::tap_interface::compute_result( + const optional< process::status >& status, + const fs::path& /* control_directory */, + const fs::path& stdout_path, + const fs::path& /* stderr_path */) const +{ + if (!status) { + return model::test_result(model::test_result_broken, + "Test case timed out"); + } else { + if (status.get().signaled()) { + return model::test_result( + model::test_result_broken, + F("Received signal %s") % status.get().termsig()); + } else { + try { + const tap_summary summary = parse_tap_output(stdout_path); + return tap_to_result(summary, status.get()); + } catch (const load_error& e) { + return model::test_result( + model::test_result_broken, + F("TAP test program yielded invalid data: %s") % e.what()); + } + } + } +} diff --git a/engine/tap.hpp b/engine/tap.hpp new file mode 100644 index 000000000000..b46bf28f0240 --- /dev/null +++ b/engine/tap.hpp @@ -0,0 +1,67 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/tap.hpp +/// Execution engine for test programs that output the TAP protocol. + +#if !defined(ENGINE_TAP_HPP) +#define ENGINE_TAP_HPP + +#include "engine/scheduler.hpp" + +namespace engine { + + +/// Implementation of the scheduler interface for tap test programs. +class tap_interface : public engine::scheduler::interface { +public: + void exec_list(const model::test_program&, + const utils::config::properties_map&) const UTILS_NORETURN; + + model::test_cases_map parse_list( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&) const; + + void exec_test(const model::test_program&, const std::string&, + const utils::config::properties_map&, + const utils::fs::path&) const + UTILS_NORETURN; + + model::test_result compute_result( + const utils::optional< utils::process::status >&, + const utils::fs::path&, + const utils::fs::path&, + const utils::fs::path&) const; +}; + + +} // namespace engine + + +#endif // !defined(ENGINE_TAP_HPP) diff --git a/engine/tap_helpers.cpp b/engine/tap_helpers.cpp new file mode 100644 index 000000000000..4f9505c78dec --- /dev/null +++ b/engine/tap_helpers.cpp @@ -0,0 +1,202 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +extern "C" { +#include + +#include + +extern char** environ; +} + +#include +#include +#include +#include + +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; + + +namespace { + + +/// Logs an error message and exits the test with an error code. +/// +/// \param str The error message to log. +static void +fail(const std::string& str) +{ + std::cerr << str << '\n'; + std::exit(EXIT_FAILURE); +} + + +/// A test scenario that validates the TEST_ENV_* variables. +static void +test_check_configuration_variables(void) +{ + std::set< std::string > vars; + char** iter; + for (iter = environ; *iter != NULL; ++iter) { + if (std::strstr(*iter, "TEST_ENV_") == *iter) { + vars.insert(*iter); + } + } + + std::set< std::string > exp_vars; + exp_vars.insert("TEST_ENV_first=some value"); + exp_vars.insert("TEST_ENV_second=some other value"); + if (vars == exp_vars) { + std::cout << "1..1\n" + << "ok 1\n"; + } else { + std::cout << "1..1\n" + << "not ok 1\n" + << F(" Expected: %s\nFound: %s\n") % exp_vars % vars; + } +} + + +/// A test scenario that crashes. +static void +test_crash(void) +{ + utils::abort_without_coredump(); +} + + +/// A test scenario that reports some tests as failed. +static void +test_fail(void) +{ + std::cout << "1..5\n" + << "ok 1 - This is good!\n" + << "not ok 2\n" + << "ok 3 - TODO Consider this as passed\n" + << "ok 4\n" + << "not ok 5\n"; +} + + +/// A test scenario that passes. +static void +test_pass(void) +{ + std::cout << "1..4\n" + << "ok 1 - This is good!\n" + << "non-result data\n" + << "ok 2 - SKIP Consider this as passed\n" + << "ok 3 - TODO Consider this as passed\n" + << "ok 4\n"; +} + + +/// A test scenario that passes but then exits with non-zero. +static void +test_pass_but_exit_failure(void) +{ + std::cout << "1..2\n" + << "ok 1\n" + << "ok 2\n"; + std::exit(70); +} + + +/// A test scenario that times out. +/// +/// Note that the timeout is defined in the Kyuafile, as the TAP interface has +/// no means for test programs to specify this by themselves. +static void +test_timeout(void) +{ + std::cout << "1..2\n" + << "ok 1\n"; + + ::sleep(10); + const fs::path control_dir = fs::path(utils::getenv("CONTROL_DIR").get()); + std::ofstream file((control_dir / "cookie").c_str()); + if (!file) + fail("Failed to create the control cookie"); + file.close(); +} + + +} // anonymous namespace + + +/// Entry point to the test program. +/// +/// The caller can select which test scenario to run by modifying the program's +/// basename on disk (either by a copy or by a hard link). +/// +/// \todo It may be worth to split this binary into separate, smaller binaries, +/// one for every "test scenario". We use this program as a dispatcher for +/// different "main"s, the only reason being to keep the amount of helper test +/// programs to a minimum. However, putting this each function in its own +/// binary could simplify many other things. +/// +/// \param argc The number of CLI arguments. +/// \param argv The CLI arguments themselves. These are not used because +/// Kyua will not pass any arguments to the plain test program. +int +main(int argc, char** argv) +{ + if (argc != 1) { + std::cerr << "No arguments allowed; select the test scenario with the " + "program's basename\n"; + return EXIT_FAILURE; + } + + const std::string& test_scenario = fs::path(argv[0]).leaf_name(); + + if (test_scenario == "check_configuration_variables") + test_check_configuration_variables(); + else if (test_scenario == "crash") + test_crash(); + else if (test_scenario == "fail") + test_fail(); + else if (test_scenario == "pass") + test_pass(); + else if (test_scenario == "pass_but_exit_failure") + test_pass_but_exit_failure(); + else if (test_scenario == "timeout") + test_timeout(); + else { + std::cerr << "Unknown test scenario\n"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/engine/tap_parser.cpp b/engine/tap_parser.cpp new file mode 100644 index 000000000000..d41328534fad --- /dev/null +++ b/engine/tap_parser.cpp @@ -0,0 +1,438 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/tap_parser.hpp" + +#include + +#include "engine/exceptions.hpp" +#include "utils/format/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" +#include "utils/text/regex.hpp" + +namespace fs = utils::fs; +namespace text = utils::text; + +using utils::optional; + + +/// TAP plan representing all tests being skipped. +const engine::tap_plan engine::all_skipped_plan(1, 0); + + +namespace { + + +/// Implementation of the TAP parser. +/// +/// This is a class only to simplify keeping global constant values around (like +/// prebuilt regular expressions). +class tap_parser : utils::noncopyable { + /// Regular expression to match plan lines. + text::regex _plan_regex; + + /// Regular expression to match a TODO and extract the reason. + text::regex _todo_regex; + + /// Regular expression to match a SKIP and extract the reason. + text::regex _skip_regex; + + /// Regular expression to match a single test result. + text::regex _result_regex; + + /// Checks if a line contains a TAP plan and extracts its data. + /// + /// \param line The line to try to parse. + /// \param [in,out] out_plan Used to store the found plan, if any. The same + /// output variable should be given to all calls to this function so + /// that duplicate plan entries can be discovered. + /// \param [out] out_all_skipped_reason Used to store the reason for all + /// tests being skipped, if any. If this is set to a non-empty value, + /// then the out_plan is set to 1..0. + /// + /// \return True if the line matched a plan; false otherwise. + /// + /// \throw engine::format_error If the input is invalid. + /// \throw text::error If the input is invalid. + bool + try_parse_plan(const std::string& line, + optional< engine::tap_plan >& out_plan, + std::string& out_all_skipped_reason) + { + const text::regex_matches plan_matches = _plan_regex.match(line); + if (!plan_matches) + return false; + const engine::tap_plan plan( + text::to_type< std::size_t >(plan_matches.get(1)), + text::to_type< std::size_t >(plan_matches.get(2))); + + if (out_plan) + throw engine::format_error( + F("Found duplicate plan %s..%s (saw %s..%s earlier)") % + plan.first % plan.second % + out_plan.get().first % out_plan.get().second); + + std::string all_skipped_reason; + const text::regex_matches skip_matches = _skip_regex.match(line); + if (skip_matches) { + if (plan != engine::all_skipped_plan) { + throw engine::format_error(F("Skipped plan must be %s..%s") % + engine::all_skipped_plan.first % + engine::all_skipped_plan.second); + } + all_skipped_reason = skip_matches.get(2); + if (all_skipped_reason.empty()) + all_skipped_reason = "No reason specified"; + } else { + if (plan.first > plan.second) + throw engine::format_error(F("Found reversed plan %s..%s") % + plan.first % plan.second); + } + + INV(!out_plan); + out_plan = plan; + out_all_skipped_reason = all_skipped_reason; + + POST(out_plan); + POST(out_all_skipped_reason.empty() || + out_plan.get() == engine::all_skipped_plan); + + return true; + } + + /// Checks if a line contains a TAP test result and extracts its data. + /// + /// \param line The line to try to parse. + /// \param [in,out] out_ok_count Accumulator for 'ok' results. + /// \param [in,out] out_not_ok_count Accumulator for 'not ok' results. + /// \param [out] out_bailed_out Set to true if the test bailed out. + /// + /// \return True if the line matched a result; false otherwise. + /// + /// \throw engine::format_error If the input is invalid. + /// \throw text::error If the input is invalid. + bool + try_parse_result(const std::string& line, std::size_t& out_ok_count, + std::size_t& out_not_ok_count, bool& out_bailed_out) + { + PRE(!out_bailed_out); + + const text::regex_matches result_matches = _result_regex.match(line); + if (result_matches) { + if (result_matches.get(1) == "ok") { + ++out_ok_count; + } else { + INV(result_matches.get(1) == "not ok"); + if (_todo_regex.match(line) || _skip_regex.match(line)) { + ++out_ok_count; + } else { + ++out_not_ok_count; + } + } + return true; + } else { + if (line.find("Bail out!") == 0) { + out_bailed_out = true; + return true; + } else { + return false; + } + } + } + +public: + /// Sets up the TAP parser state. + tap_parser(void) : + _plan_regex(text::regex::compile("^([0-9]+)\\.\\.([0-9]+)", 2)), + _todo_regex(text::regex::compile("TODO[ \t]*(.*)$", 2, true)), + _skip_regex(text::regex::compile("(SKIP|Skipped:?)[ \t]*(.*)$", 2, + true)), + _result_regex(text::regex::compile("^(not ok|ok)[ \t-]+[0-9]*", 1)) + { + } + + /// Parses an input file containing TAP output. + /// + /// \param input The stream to read from. + /// + /// \return The results of the parsing in the form of a tap_summary object. + /// + /// \throw engine::format_error If there are any syntax errors in the input. + /// \throw text::error If there are any syntax errors in the input. + engine::tap_summary + parse(std::ifstream& input) + { + optional< engine::tap_plan > plan; + std::string all_skipped_reason; + bool bailed_out = false; + std::size_t ok_count = 0, not_ok_count = 0; + + std::string line; + while (!bailed_out && std::getline(input, line)) { + if (try_parse_result(line, ok_count, not_ok_count, bailed_out)) + continue; + (void)try_parse_plan(line, plan, all_skipped_reason); + } + + if (bailed_out) { + return engine::tap_summary::new_bailed_out(); + } else { + if (!plan) + throw engine::format_error( + "Output did not contain any TAP plan and the program did " + "not bail out"); + + if (plan.get() == engine::all_skipped_plan) { + return engine::tap_summary::new_all_skipped(all_skipped_reason); + } else { + const std::size_t exp_count = plan.get().second - + plan.get().first + 1; + const std::size_t actual_count = ok_count + not_ok_count; + if (exp_count != actual_count) { + throw engine::format_error( + "Reported plan differs from actual executed tests"); + } + return engine::tap_summary::new_results(plan.get(), ok_count, + not_ok_count); + } + } + } +}; + + +} // anonymous namespace + + +/// Constructs a TAP summary with the results of parsing a TAP output. +/// +/// \param bailed_out_ Whether the test program bailed out early or not. +/// \param plan_ The TAP plan. +/// \param all_skipped_reason_ The reason for skipping all tests, if any. +/// \param ok_count_ Number of 'ok' test results. +/// \param not_ok_count_ Number of 'not ok' test results. +engine::tap_summary::tap_summary(const bool bailed_out_, + const tap_plan& plan_, + const std::string& all_skipped_reason_, + const std::size_t ok_count_, + const std::size_t not_ok_count_) : + _bailed_out(bailed_out_), _plan(plan_), + _all_skipped_reason(all_skipped_reason_), + _ok_count(ok_count_), _not_ok_count(not_ok_count_) +{ +} + + +/// Constructs a TAP summary for a bailed out test program. +/// +/// \return The new tap_summary object. +engine::tap_summary +engine::tap_summary::new_bailed_out(void) +{ + return tap_summary(true, tap_plan(0, 0), "", 0, 0); +} + + +/// Constructs a TAP summary for a test program that skipped all tests. +/// +/// \param reason Textual reason describing why the tests were skipped. +/// +/// \return The new tap_summary object. +engine::tap_summary +engine::tap_summary::new_all_skipped(const std::string& reason) +{ + return tap_summary(false, all_skipped_plan, reason, 0, 0); +} + + +/// Constructs a TAP summary for a test program that reported results. +/// +/// \param plan_ The TAP plan. +/// \param ok_count_ Total number of 'ok' results. +/// \param not_ok_count_ Total number of 'not ok' results. +/// +/// \return The new tap_summary object. +engine::tap_summary +engine::tap_summary::new_results(const tap_plan& plan_, + const std::size_t ok_count_, + const std::size_t not_ok_count_) +{ + PRE((plan_.second - plan_.first + 1) == (ok_count_ + not_ok_count_)); + return tap_summary(false, plan_, "", ok_count_, not_ok_count_); +} + + +/// Checks whether the test program bailed out early or not. +/// +/// \return True if the test program aborted execution before completing. +bool +engine::tap_summary::bailed_out(void) const +{ + return _bailed_out; +} + + +/// Gets the TAP plan of the test program. +/// +/// \pre bailed_out() must be false. +/// +/// \return The TAP plan. If 1..0, then all_skipped_reason() will have some +/// contents. +const engine::tap_plan& +engine::tap_summary::plan(void) const +{ + PRE(!_bailed_out); + return _plan; +} + + +/// Gets the reason for skipping all the tests, if any. +/// +/// \pre bailed_out() must be false. +/// \pre plan() returns 1..0. +/// +/// \return The reason for skipping all the tests. +const std::string& +engine::tap_summary::all_skipped_reason(void) const +{ + PRE(!_bailed_out); + PRE(_plan == all_skipped_plan); + return _all_skipped_reason; +} + + +/// Gets the number of 'ok' test results. +/// +/// \pre bailed_out() must be false. +/// +/// \return The number of test results that reported 'ok'. +std::size_t +engine::tap_summary::ok_count(void) const +{ + PRE(!bailed_out()); + PRE(_all_skipped_reason.empty()); + return _ok_count; +} + + +/// Gets the number of 'not ok' test results. +/// +/// \pre bailed_out() must be false. +/// +/// \return The number of test results that reported 'not ok'. +std::size_t +engine::tap_summary::not_ok_count(void) const +{ + PRE(!_bailed_out); + PRE(_all_skipped_reason.empty()); + return _not_ok_count; +} + + +/// Checks two tap_summary objects for equality. +/// +/// \param other The object to compare this one to. +/// +/// \return True if the two objects are equal; false otherwise. +bool +engine::tap_summary::operator==(const tap_summary& other) const +{ + return (_bailed_out == other._bailed_out && + _plan == other._plan && + _all_skipped_reason == other._all_skipped_reason && + _ok_count == other._ok_count && + _not_ok_count == other._not_ok_count); +} + + +/// Checks two tap_summary objects for inequality. +/// +/// \param other The object to compare this one to. +/// +/// \return True if the two objects are different; false otherwise. +bool +engine::tap_summary::operator!=(const tap_summary& other) const +{ + return !(*this == other); +} + + +/// Formats a tap_summary into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param summary The summary to format. +/// +/// \return The output stream. +std::ostream& +engine::operator<<(std::ostream& output, const tap_summary& summary) +{ + output << "tap_summary{"; + if (summary.bailed_out()) { + output << "bailed_out=true"; + } else { + const tap_plan& plan = summary.plan(); + output << "bailed_out=false" + << ", plan=" << plan.first << ".." << plan.second; + if (plan == all_skipped_plan) { + output << ", all_skipped_reason=" << summary.all_skipped_reason(); + } else { + output << ", ok_count=" << summary.ok_count() + << ", not_ok_count=" << summary.not_ok_count(); + } + } + output << "}"; + return output; +} + + +/// Parses an input file containing the TAP output of a test program. +/// +/// \param filename Path to the file to parse. +/// +/// \return The parsed data in the form of a tap_summary. +/// +/// \throw load_error If there are any problems parsing the file. Such problems +/// should be considered as test program breakage. +engine::tap_summary +engine::parse_tap_output(const utils::fs::path& filename) +{ + std::ifstream input(filename.str().c_str()); + if (!input) + throw engine::load_error(filename, "Failed to open TAP output file"); + + try { + return tap_summary(tap_parser().parse(input)); + } catch (const engine::format_error& e) { + throw engine::load_error(filename, e.what()); + } catch (const text::error& e) { + throw engine::load_error(filename, e.what()); + } +} diff --git a/engine/tap_parser.hpp b/engine/tap_parser.hpp new file mode 100644 index 000000000000..84cea908f5ba --- /dev/null +++ b/engine/tap_parser.hpp @@ -0,0 +1,99 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/tap_parser.hpp +/// Utilities to parse TAP test program output. + +#if !defined(ENGINE_TAP_PARSER_HPP) +#define ENGINE_TAP_PARSER_HPP + +#include "engine/tap_parser_fwd.hpp" + +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace engine { + + +/// TAP plan representing all tests being skipped. +extern const engine::tap_plan all_skipped_plan; + + +/// TAP output representation and parser. +class tap_summary { + /// Whether the test program bailed out early or not. + bool _bailed_out; + + /// The TAP plan. Only valid if not bailed out. + tap_plan _plan; + + /// If not empty, the reason why all tests were skipped. + std::string _all_skipped_reason; + + /// Total number of 'ok' tests. Only valid if not balied out. + std::size_t _ok_count; + + /// Total number of 'not ok' tests. Only valid if not balied out. + std::size_t _not_ok_count; + + tap_summary(const bool, const tap_plan&, const std::string&, + const std::size_t, const std::size_t); + +public: + // Yes, these three constructors indicate that we really ought to have three + // different classes and select between them at runtime. But doing so would + // be overly complex for our really simple needs here. + static tap_summary new_bailed_out(void); + static tap_summary new_all_skipped(const std::string&); + static tap_summary new_results(const tap_plan&, const std::size_t, + const std::size_t); + + bool bailed_out(void) const; + const tap_plan& plan(void) const; + const std::string& all_skipped_reason(void) const; + std::size_t ok_count(void) const; + std::size_t not_ok_count(void) const; + + bool operator==(const tap_summary&) const; + bool operator!=(const tap_summary&) const; +}; + + +std::ostream& operator<<(std::ostream&, const tap_summary&); + + +tap_summary parse_tap_output(const utils::fs::path&); + + +} // namespace engine + + +#endif // !defined(ENGINE_TAP_PARSER_HPP) diff --git a/engine/tap_parser_fwd.hpp b/engine/tap_parser_fwd.hpp new file mode 100644 index 000000000000..481ed2f42267 --- /dev/null +++ b/engine/tap_parser_fwd.hpp @@ -0,0 +1,50 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/tap_parser_fwd.hpp +/// Forward declarations for engine/tap_parser.hpp + +#if !defined(ENGINE_TAP_PARSER_FWD_HPP) +#define ENGINE_TAP_PARSER_FWD_HPP + +#include +#include + +namespace engine { + + +/// Representation of the TAP plan line. +typedef std::pair< std::size_t, std::size_t > tap_plan; + + +class tap_summary; + + +} // namespace engine + +#endif // !defined(ENGINE_TAP_PARSER_FWD_HPP) diff --git a/engine/tap_parser_test.cpp b/engine/tap_parser_test.cpp new file mode 100644 index 000000000000..af993bfab4ab --- /dev/null +++ b/engine/tap_parser_test.cpp @@ -0,0 +1,465 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/tap_parser.hpp" + +#include + +#include + +#include "engine/exceptions.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Helper to execute parse_tap_output() on inline text contents. +/// +/// \param contents The TAP output to parse. +/// +/// \return The tap_summary object resultingafter the parse. +/// +/// \throw engine::load_error If parse_tap_output() fails. +static engine::tap_summary +do_parse(const std::string& contents) +{ + std::ofstream output("tap.txt"); + ATF_REQUIRE(output); + output << contents; + output.close(); + return engine::parse_tap_output(fs::path("tap.txt")); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__bailed_out); +ATF_TEST_CASE_BODY(tap_summary__bailed_out) +{ + const engine::tap_summary summary = engine::tap_summary::new_bailed_out(); + ATF_REQUIRE(summary.bailed_out()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__some_results); +ATF_TEST_CASE_BODY(tap_summary__some_results) +{ + const engine::tap_summary summary = engine::tap_summary::new_results( + engine::tap_plan(1, 5), 3, 2); + ATF_REQUIRE(!summary.bailed_out()); + ATF_REQUIRE_EQ(engine::tap_plan(1, 5), summary.plan()); + ATF_REQUIRE_EQ(3, summary.ok_count()); + ATF_REQUIRE_EQ(2, summary.not_ok_count()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__all_skipped); +ATF_TEST_CASE_BODY(tap_summary__all_skipped) +{ + const engine::tap_summary summary = engine::tap_summary::new_all_skipped( + "Skipped"); + ATF_REQUIRE(!summary.bailed_out()); + ATF_REQUIRE_EQ(engine::tap_plan(1, 0), summary.plan()); + ATF_REQUIRE_EQ("Skipped", summary.all_skipped_reason()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__equality_operators); +ATF_TEST_CASE_BODY(tap_summary__equality_operators) +{ + const engine::tap_summary bailed_out = + engine::tap_summary::new_bailed_out(); + const engine::tap_summary all_skipped_1 = + engine::tap_summary::new_all_skipped("Reason 1"); + const engine::tap_summary results_1 = + engine::tap_summary::new_results(engine::tap_plan(1, 5), 3, 2); + + // Self-equality checks. + ATF_REQUIRE( bailed_out == bailed_out); + ATF_REQUIRE(!(bailed_out != bailed_out)); + ATF_REQUIRE( all_skipped_1 == all_skipped_1); + ATF_REQUIRE(!(all_skipped_1 != all_skipped_1)); + ATF_REQUIRE( results_1 == results_1); + ATF_REQUIRE(!(results_1 != results_1)); + + // Cross-equality checks. + ATF_REQUIRE(!(bailed_out == all_skipped_1)); + ATF_REQUIRE( bailed_out != all_skipped_1); + ATF_REQUIRE(!(bailed_out == results_1)); + ATF_REQUIRE( bailed_out != results_1); + ATF_REQUIRE(!(all_skipped_1 == results_1)); + ATF_REQUIRE( all_skipped_1 != results_1); + + // Checks for the all_skipped "type". + const engine::tap_summary all_skipped_2 = + engine::tap_summary::new_all_skipped("Reason 2"); + ATF_REQUIRE(!(all_skipped_1 == all_skipped_2)); + ATF_REQUIRE( all_skipped_1 != all_skipped_2); + + + // Checks for the results "type", different plan. + const engine::tap_summary results_2 = + engine::tap_summary::new_results(engine::tap_plan(2, 6), + results_1.ok_count(), + results_1.not_ok_count()); + ATF_REQUIRE(!(results_1 == results_2)); + ATF_REQUIRE( results_1 != results_2); + + + // Checks for the results "type", different counts. + const engine::tap_summary results_3 = + engine::tap_summary::new_results(results_1.plan(), + results_1.not_ok_count(), + results_1.ok_count()); + ATF_REQUIRE(!(results_1 == results_3)); + ATF_REQUIRE( results_1 != results_3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(tap_summary__output); +ATF_TEST_CASE_BODY(tap_summary__output) +{ + { + const engine::tap_summary summary = + engine::tap_summary::new_bailed_out(); + ATF_REQUIRE_EQ( + "tap_summary{bailed_out=true}", + (F("%s") % summary).str()); + } + + { + const engine::tap_summary summary = + engine::tap_summary::new_results(engine::tap_plan(5, 10), 2, 4); + ATF_REQUIRE_EQ( + "tap_summary{bailed_out=false, plan=5..10, ok_count=2, " + "not_ok_count=4}", + (F("%s") % summary).str()); + } + + { + const engine::tap_summary summary = + engine::tap_summary::new_all_skipped("Who knows"); + ATF_REQUIRE_EQ( + "tap_summary{bailed_out=false, plan=1..0, " + "all_skipped_reason=Who knows}", + (F("%s") % summary).str()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__only_one_result); +ATF_TEST_CASE_BODY(parse_tap_output__only_one_result) +{ + const engine::tap_summary summary = do_parse( + "1..1\n" + "ok - 1\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 1), 1, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__all_pass); +ATF_TEST_CASE_BODY(parse_tap_output__all_pass) +{ + const engine::tap_summary summary = do_parse( + "1..8\n" + "ok - 1\n" + " Some diagnostic message\n" + "ok - 2 This test also passed\n" + "garbage line\n" + "ok - 3 This test passed\n" + "not ok 4 # SKIP Some reason\n" + "not ok 5 # TODO Another reason\n" + "ok - 6 Doesn't make a difference SKIP\n" + "ok - 7 Doesn't make a difference either TODO\n" + "ok # Also works without a number\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 8), 8, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__some_fail); +ATF_TEST_CASE_BODY(parse_tap_output__some_fail) +{ + const engine::tap_summary summary = do_parse( + "garbage line\n" + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n" + "not ok - 3 This test failed\n" + "1..6\n" + "not ok - 4 This test failed\n" + "ok - 5 This test passed\n" + "not ok # Fails as well without a number\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 6), 2, 4); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__skip_and_todo_variants); +ATF_TEST_CASE_BODY(parse_tap_output__skip_and_todo_variants) +{ + const engine::tap_summary summary = do_parse( + "1..8\n" + "not ok - 1 # SKIP Some reason\n" + "not ok - 2 # skip Some reason\n" + "not ok - 3 # Skipped Some reason\n" + "not ok - 4 # skipped Some reason\n" + "not ok - 5 # Skipped: Some reason\n" + "not ok - 6 # skipped: Some reason\n" + "not ok - 7 # TODO Some reason\n" + "not ok - 8 # todo Some reason\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 8), 8, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__skip_all_with_reason); +ATF_TEST_CASE_BODY(parse_tap_output__skip_all_with_reason) +{ + const engine::tap_summary summary = do_parse( + "1..0 SKIP Some reason for skipping\n" + "ok - 1\n" + " Some diagnostic message\n" + "ok - 6 Doesn't make a difference SKIP\n" + "ok - 7 Doesn't make a difference either TODO\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_all_skipped("Some reason for skipping"); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__skip_all_without_reason); +ATF_TEST_CASE_BODY(parse_tap_output__skip_all_without_reason) +{ + const engine::tap_summary summary = do_parse( + "1..0 unrecognized # garbage skip\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_all_skipped("No reason specified"); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__skip_all_invalid); +ATF_TEST_CASE_BODY(parse_tap_output__skip_all_invalid) +{ + ATF_REQUIRE_THROW_RE(engine::load_error, + "Skipped plan must be 1\\.\\.0", + do_parse("1..3 # skip\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__plan_at_end); +ATF_TEST_CASE_BODY(parse_tap_output__plan_at_end) +{ + const engine::tap_summary summary = do_parse( + "ok - 1\n" + " Some diagnostic message\n" + "ok - 2 This test also passed\n" + "garbage line\n" + "ok - 3 This test passed\n" + "not ok 4 # SKIP Some reason\n" + "not ok 5 # TODO Another reason\n" + "ok - 6 Doesn't make a difference SKIP\n" + "ok - 7 Doesn't make a difference either TODO\n" + "1..7\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 7), 7, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__stray_oks); +ATF_TEST_CASE_BODY(parse_tap_output__stray_oks) +{ + const engine::tap_summary summary = do_parse( + "1..3\n" + "ok - 1\n" + "ok\n" + "ok - 2 This test also passed\n" + "not ok\n" + "ok - 3 This test passed\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_results(engine::tap_plan(1, 3), 3, 0); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__no_plan); +ATF_TEST_CASE_BODY(parse_tap_output__no_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, + "Output did not contain any TAP plan", + do_parse( + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__double_plan); +ATF_TEST_CASE_BODY(parse_tap_output__double_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, + "Found duplicate plan", + do_parse( + "garbage line\n" + "1..5\n" + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n" + "1..8\n" + "ok\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__inconsistent_plan); +ATF_TEST_CASE_BODY(parse_tap_output__inconsistent_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, + "Reported plan differs from actual executed tests", + do_parse( + "1..3\n" + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__inconsistent_trailing_plan); +ATF_TEST_CASE_BODY(parse_tap_output__inconsistent_trailing_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, + "Reported plan differs from actual executed tests", + do_parse( + "not ok - 1 This test failed\n" + "ok - 2 This test passed\n" + "1..3\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__insane_plan); +ATF_TEST_CASE_BODY(parse_tap_output__insane_plan) +{ + ATF_REQUIRE_THROW_RE( + engine::load_error, "Invalid value", + do_parse("120830981209831..234891793874080981092803981092312\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__reversed_plan); +ATF_TEST_CASE_BODY(parse_tap_output__reversed_plan) +{ + ATF_REQUIRE_THROW_RE(engine::load_error, + "Found reversed plan 8\\.\\.5", + do_parse("8..5\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__bail_out); +ATF_TEST_CASE_BODY(parse_tap_output__bail_out) +{ + const engine::tap_summary summary = do_parse( + "1..3\n" + "not ok - 1 This test failed\n" + "Bail out! There is some unknown problem\n" + "ok - 2 This test passed\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_bailed_out(); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__bail_out_wins_over_no_plan); +ATF_TEST_CASE_BODY(parse_tap_output__bail_out_wins_over_no_plan) +{ + const engine::tap_summary summary = do_parse( + "not ok - 1 This test failed\n" + "Bail out! There is some unknown problem\n" + "ok - 2 This test passed\n"); + + const engine::tap_summary exp_summary = + engine::tap_summary::new_bailed_out(); + ATF_REQUIRE_EQ(exp_summary, summary); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_tap_output__open_failure); +ATF_TEST_CASE_BODY(parse_tap_output__open_failure) +{ + ATF_REQUIRE_THROW_RE(engine::load_error, "hello.txt.*Failed to open", + engine::parse_tap_output(fs::path("hello.txt"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, tap_summary__bailed_out); + ATF_ADD_TEST_CASE(tcs, tap_summary__some_results); + ATF_ADD_TEST_CASE(tcs, tap_summary__all_skipped); + ATF_ADD_TEST_CASE(tcs, tap_summary__equality_operators); + ATF_ADD_TEST_CASE(tcs, tap_summary__output); + + ATF_ADD_TEST_CASE(tcs, parse_tap_output__only_one_result); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__all_pass); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__some_fail); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__skip_and_todo_variants); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__skip_all_without_reason); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__skip_all_with_reason); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__skip_all_invalid); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__plan_at_end); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__stray_oks); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__no_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__double_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__inconsistent_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__inconsistent_trailing_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__insane_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__reversed_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__bail_out); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__bail_out_wins_over_no_plan); + ATF_ADD_TEST_CASE(tcs, parse_tap_output__open_failure); +} diff --git a/engine/tap_test.cpp b/engine/tap_test.cpp new file mode 100644 index 000000000000..f4253a68e727 --- /dev/null +++ b/engine/tap_test.cpp @@ -0,0 +1,218 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/tap.hpp" + +extern "C" { +#include +} + +#include + +#include "engine/config.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; + + +namespace { + + +/// Copies the tap helper to the work directory, selecting a specific helper. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param name Name of the new binary to create. Must match the name of a +/// valid helper, as the binary name is used to select it. +static void +copy_tap_helper(const atf::tests::tc* tc, const char* name) +{ + const fs::path srcdir(tc->get_config_var("srcdir")); + atf::utils::copy_file((srcdir / "tap_helpers").str(), name); +} + + +/// Runs one tap test program and checks its result. +/// +/// \param tc Pointer to the calling test case, to obtain srcdir. +/// \param test_case_name Name of the "test case" to select from the helper +/// program. +/// \param exp_result The expected result. +/// \param metadata The test case metadata. +/// \param user_config User-provided configuration variables. +static void +run_one(const atf::tests::tc* tc, const char* test_case_name, + const model::test_result& exp_result, + const model::metadata& metadata = model::metadata_builder().build(), + const config::tree& user_config = engine::empty_config()) +{ + copy_tap_helper(tc, test_case_name); + const model::test_program_ptr program = model::test_program_builder( + "tap", fs::path(test_case_name), fs::current_path(), "the-suite") + .add_test_case("main", metadata).build_ptr(); + + scheduler::scheduler_handle handle = scheduler::setup(); + (void)handle.spawn_test(program, "main", user_config); + + scheduler::result_handle_ptr result_handle = handle.wait_any(); + const scheduler::test_result_handle* test_result_handle = + dynamic_cast< const scheduler::test_result_handle* >( + result_handle.get()); + atf::utils::cat_file(result_handle->stdout_file().str(), "stdout: "); + atf::utils::cat_file(result_handle->stderr_file().str(), "stderr: "); + ATF_REQUIRE_EQ(exp_result, test_result_handle->test_result()); + result_handle->cleanup(); + result_handle.reset(); + + handle.cleanup(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(list); +ATF_TEST_CASE_BODY(list) +{ + const model::test_program program = model::test_program_builder( + "tap", fs::path("non-existent"), fs::path("."), "unused-suite") + .build(); + + scheduler::scheduler_handle handle = scheduler::setup(); + const model::test_cases_map test_cases = handle.list_tests( + &program, engine::empty_config()); + handle.cleanup(); + + const model::test_cases_map exp_test_cases = model::test_cases_map_builder() + .add("main").build(); + ATF_REQUIRE_EQ(exp_test_cases, test_cases); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__all_tests_pass); +ATF_TEST_CASE_BODY(test__all_tests_pass) +{ + const model::test_result exp_result(model::test_result_passed); + run_one(this, "pass", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__some_tests_fail); +ATF_TEST_CASE_BODY(test__some_tests_fail) +{ + const model::test_result exp_result(model::test_result_failed, + "2 of 5 tests failed"); + run_one(this, "fail", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__all_tests_pass_but_exit_failure); +ATF_TEST_CASE_BODY(test__all_tests_pass_but_exit_failure) +{ + const model::test_result exp_result( + model::test_result_broken, + "Dubious test program: reported all tests as passed but returned exit " + "code 70"); + run_one(this, "pass_but_exit_failure", exp_result); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__signal_is_broken); +ATF_TEST_CASE_BODY(test__signal_is_broken) +{ + const model::test_result exp_result(model::test_result_broken, + F("Received signal %s") % SIGABRT); + run_one(this, "crash", exp_result); +} + + +ATF_TEST_CASE(test__timeout_is_broken); +ATF_TEST_CASE_HEAD(test__timeout_is_broken) +{ + set_md_var("timeout", "60"); +} +ATF_TEST_CASE_BODY(test__timeout_is_broken) +{ + utils::setenv("CONTROL_DIR", fs::current_path().str()); + + const model::metadata metadata = model::metadata_builder() + .set_timeout(datetime::delta(1, 0)).build(); + const model::test_result exp_result(model::test_result_broken, + "Test case timed out"); + run_one(this, "timeout", exp_result, metadata); + + ATF_REQUIRE(!atf::utils::file_exists("cookie")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test__configuration_variables); +ATF_TEST_CASE_BODY(test__configuration_variables) +{ + config::tree user_config = engine::empty_config(); + user_config.set_string("test_suites.a-suite.first", "unused"); + user_config.set_string("test_suites.the-suite.first", "some value"); + user_config.set_string("test_suites.the-suite.second", "some other value"); + user_config.set_string("test_suites.other-suite.first", "unused"); + + const model::test_result exp_result(model::test_result_passed); + run_one(this, "check_configuration_variables", exp_result, + model::metadata_builder().build(), user_config); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "tap", std::shared_ptr< scheduler::interface >( + new engine::tap_interface())); + + ATF_ADD_TEST_CASE(tcs, list); + + ATF_ADD_TEST_CASE(tcs, test__all_tests_pass); + ATF_ADD_TEST_CASE(tcs, test__all_tests_pass_but_exit_failure); + ATF_ADD_TEST_CASE(tcs, test__some_tests_fail); + ATF_ADD_TEST_CASE(tcs, test__signal_is_broken); + ATF_ADD_TEST_CASE(tcs, test__timeout_is_broken); + ATF_ADD_TEST_CASE(tcs, test__configuration_variables); +} diff --git a/examples/Kyuafile b/examples/Kyuafile new file mode 100644 index 000000000000..2c8f39baad58 --- /dev/null +++ b/examples/Kyuafile @@ -0,0 +1,5 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="syntax_test"} diff --git a/examples/Kyuafile.top b/examples/Kyuafile.top new file mode 100644 index 000000000000..3c28945f0cf5 --- /dev/null +++ b/examples/Kyuafile.top @@ -0,0 +1,52 @@ +-- Copyright 2011 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- Example top-level Kyuafile. +-- +-- This sample top-level Kyuafile looks for any */Kyuafile files and includes +-- them in order to process all the test cases within a test suite. +-- +-- This file is supposed to be installed in the root directory of the tests +-- hierarchy; typically, this is /usr/tests/Kyuafile (note that the .top +-- extension has been dropped). Third-party packages install tests as +-- subdirectories of /usr/tests. When doing so, they should not have to update +-- the contents of the top-level Kyuafile; in other words, Kyua needs to +-- discover tests in such subdirectories automatically. + +syntax(2) + +for file in fs.files(".") do + if file == "." or file == ".." then + -- Skip these special entries. + else + local kyuafile = fs.join(file, "Kyuafile") + if fs.exists(kyuafile) then + include(kyuafile) + end + end +end diff --git a/examples/Makefile.am.inc b/examples/Makefile.am.inc new file mode 100644 index 000000000000..3c9b27dae6a9 --- /dev/null +++ b/examples/Makefile.am.inc @@ -0,0 +1,45 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dist_examples_DATA = examples/Kyuafile.top +dist_examples_DATA += examples/kyua.conf + +if WITH_ATF +tests_examplesdir = $(pkgtestsdir)/examples + +tests_examples_DATA = examples/Kyuafile +EXTRA_DIST += $(tests_examples_DATA) + +tests_examples_PROGRAMS = examples/syntax_test +examples_syntax_test_SOURCES = examples/syntax_test.cpp +examples_syntax_test_CPPFLAGS = -DKYUA_EXAMPLESDIR="\"$(examplesdir)\"" +examples_syntax_test_CXXFLAGS = $(ENGINE_CFLAGS) $(UTILS_CFLAGS) \ + $(ATF_CXX_CFLAGS) +examples_syntax_test_LDADD = $(ENGINE_LIBS) $(UTILS_LIBS) \ + $(ATF_CXX_LIBS) +endif diff --git a/examples/kyua.conf b/examples/kyua.conf new file mode 100644 index 000000000000..83418a320dc4 --- /dev/null +++ b/examples/kyua.conf @@ -0,0 +1,69 @@ +-- Copyright 2011 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- Example file for the configuration of Kyua. +-- +-- All the values shown here do not reflect the default values that Kyua +-- is using on this installation: these are just fictitious settings that +-- may or may not work. +-- +-- To write your own configuration file, it is recommended that you start +-- from a blank file and then define only those settings that you want to +-- override. If you want to use this file as a template, you will have +-- to comment out all the settings first to prevent any side-effects. + +-- The file must start by declaring the name and version of its format. +syntax(2) + +-- Name of the system architecture (aka processor type). +architecture = "x86_64" + +-- Maximum number of jobs (such as test case runs) to execute concurrently. +parallelism = 16 + +-- Name of the system platform (aka machine type). +platform = "amd64" + +-- The name or UID of the unprivileged user. +-- +-- If set, this user must exist in the system and his privileges will be +-- used to run test cases that need regular privileges when Kyua is +-- executed as root. +unprivileged_user = "nobody" + +-- Set actual configuration properties for the test suite named 'kyua'. +test_suites.kyua.run_coredump_tests = "false" + +-- Set fictitious configuration properties for the test suite named 'FreeBSD'. +test_suites.FreeBSD.iterations = "1000" +test_suites.FreeBSD.run_old_tests = "false" + +-- Set fictitious configuration properties for the test suite named 'NetBSD'. +test_suites.NetBSD.file_systems = "ffs lfs ext2fs" +test_suites.NetBSD.iterations = "100" +test_suites.NetBSD.run_broken_tests = "true" diff --git a/examples/syntax_test.cpp b/examples/syntax_test.cpp new file mode 100644 index 000000000000..a90acb810d4f --- /dev/null +++ b/examples/syntax_test.cpp @@ -0,0 +1,210 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +extern "C" { +#include +} + +#include + +#include "engine/config.hpp" +#include "engine/kyuafile.hpp" +#include "engine/plain.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace scheduler = engine::scheduler; + +using utils::none; + + +namespace { + + +/// Gets the path to an example file. +/// +/// \param name The name of the example file. +/// +/// \return A path to the desired example file. This can either be inside the +/// source tree before installing Kyua or in the target installation directory +/// after installation. +static fs::path +example_file(const char* name) +{ + const fs::path examplesdir(utils::getenv_with_default( + "KYUA_EXAMPLESDIR", KYUA_EXAMPLESDIR)); + return examplesdir / name; +} + + +} // anonymous namespace + + +ATF_TEST_CASE(kyua_conf); +ATF_TEST_CASE_HEAD(kyua_conf) +{ + utils::logging::set_inmemory(); + set_md_var("require.files", example_file("kyua.conf").str()); +} +ATF_TEST_CASE_BODY(kyua_conf) +{ + std::vector< passwd::user > users; + users.push_back(passwd::user("nobody", 1, 2)); + passwd::set_mock_users_for_testing(users); + + const config::tree user_config = engine::load_config( + example_file("kyua.conf")); + + ATF_REQUIRE_EQ( + "x86_64", + user_config.lookup< config::string_node >("architecture")); + ATF_REQUIRE_EQ( + 16, + user_config.lookup< config::positive_int_node >("parallelism")); + ATF_REQUIRE_EQ( + "amd64", + user_config.lookup< config::string_node >("platform")); + + ATF_REQUIRE_EQ( + "nobody", + user_config.lookup< engine::user_node >("unprivileged_user").name); + + config::properties_map exp_test_suites; + exp_test_suites["test_suites.kyua.run_coredump_tests"] = "false"; + exp_test_suites["test_suites.FreeBSD.iterations"] = "1000"; + exp_test_suites["test_suites.FreeBSD.run_old_tests"] = "false"; + exp_test_suites["test_suites.NetBSD.file_systems"] = "ffs lfs ext2fs"; + exp_test_suites["test_suites.NetBSD.iterations"] = "100"; + exp_test_suites["test_suites.NetBSD.run_broken_tests"] = "true"; + ATF_REQUIRE(exp_test_suites == user_config.all_properties("test_suites")); +} + + +ATF_TEST_CASE(kyuafile_top__no_matches); +ATF_TEST_CASE_HEAD(kyuafile_top__no_matches) +{ + utils::logging::set_inmemory(); + set_md_var("require.files", example_file("Kyuafile.top").str()); +} +ATF_TEST_CASE_BODY(kyuafile_top__no_matches) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + fs::mkdir(fs::path("root"), 0755); + const fs::path source_path = example_file("Kyuafile.top"); + ATF_REQUIRE(::symlink(source_path.c_str(), "root/Kyuafile") != -1); + + atf::utils::create_file("root/file", ""); + fs::mkdir(fs::path("root/subdir"), 0755); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + fs::path("root/Kyuafile"), none, engine::default_config(), handle); + ATF_REQUIRE_EQ(fs::path("root"), kyuafile.source_root()); + ATF_REQUIRE_EQ(fs::path("root"), kyuafile.build_root()); + ATF_REQUIRE(kyuafile.test_programs().empty()); + + handle.cleanup(); +} + + +ATF_TEST_CASE(kyuafile_top__some_matches); +ATF_TEST_CASE_HEAD(kyuafile_top__some_matches) +{ + utils::logging::set_inmemory(); + set_md_var("require.files", example_file("Kyuafile.top").str()); +} +ATF_TEST_CASE_BODY(kyuafile_top__some_matches) +{ + scheduler::scheduler_handle handle = scheduler::setup(); + + fs::mkdir(fs::path("root"), 0755); + const fs::path source_path = example_file("Kyuafile.top"); + ATF_REQUIRE(::symlink(source_path.c_str(), "root/Kyuafile") != -1); + + atf::utils::create_file("root/file", ""); + + fs::mkdir(fs::path("root/subdir1"), 0755); + atf::utils::create_file("root/subdir1/Kyuafile", + "syntax(2)\n" + "plain_test_program{name='a', test_suite='b'}\n"); + atf::utils::create_file("root/subdir1/a", ""); + + fs::mkdir(fs::path("root/subdir2"), 0755); + atf::utils::create_file("root/subdir2/Kyuafile", + "syntax(2)\n" + "plain_test_program{name='c', test_suite='d'}\n"); + atf::utils::create_file("root/subdir2/c", ""); + atf::utils::create_file("root/subdir2/Kyuafile.etc", "invalid"); + + const engine::kyuafile kyuafile = engine::kyuafile::load( + fs::path("root/Kyuafile"), none, engine::default_config(), handle); + ATF_REQUIRE_EQ(fs::path("root"), kyuafile.source_root()); + ATF_REQUIRE_EQ(fs::path("root"), kyuafile.build_root()); + + const model::test_program exp_test_program_a = model::test_program_builder( + "plain", fs::path("subdir1/a"), fs::path("root").to_absolute(), "b") + .add_test_case("main") + .build(); + const model::test_program exp_test_program_c = model::test_program_builder( + "plain", fs::path("subdir2/c"), fs::path("root").to_absolute(), "d") + .add_test_case("main") + .build(); + + ATF_REQUIRE_EQ(2, kyuafile.test_programs().size()); + ATF_REQUIRE((exp_test_program_a == *kyuafile.test_programs()[0] && + exp_test_program_c == *kyuafile.test_programs()[1]) + || + (exp_test_program_a == *kyuafile.test_programs()[1] && + exp_test_program_c == *kyuafile.test_programs()[0])); + + handle.cleanup(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + scheduler::register_interface( + "plain", std::shared_ptr< scheduler::interface >( + new engine::plain_interface())); + + ATF_ADD_TEST_CASE(tcs, kyua_conf); + + ATF_ADD_TEST_CASE(tcs, kyuafile_top__no_matches); + ATF_ADD_TEST_CASE(tcs, kyuafile_top__some_matches); +} diff --git a/integration/Kyuafile b/integration/Kyuafile new file mode 100644 index 000000000000..2ebb4ec8acca --- /dev/null +++ b/integration/Kyuafile @@ -0,0 +1,16 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="cmd_about_test"} +atf_test_program{name="cmd_config_test"} +atf_test_program{name="cmd_db_exec_test"} +atf_test_program{name="cmd_db_migrate_test"} +atf_test_program{name="cmd_debug_test"} +atf_test_program{name="cmd_help_test"} +atf_test_program{name="cmd_list_test"} +atf_test_program{name="cmd_report_html_test"} +atf_test_program{name="cmd_report_junit_test"} +atf_test_program{name="cmd_report_test"} +atf_test_program{name="cmd_test_test"} +atf_test_program{name="global_test"} diff --git a/integration/Makefile.am.inc b/integration/Makefile.am.inc new file mode 100644 index 000000000000..cf9ad86d5730 --- /dev/null +++ b/integration/Makefile.am.inc @@ -0,0 +1,150 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +if WITH_ATF +tests_integrationdir = $(pkgtestsdir)/integration + +tests_integration_DATA = integration/Kyuafile +EXTRA_DIST += $(tests_integration_DATA) + +ATF_SH_BUILD = \ + $(MKDIR_P) integration; \ + echo "\#! $(ATF_SH)" >integration/$${name}; \ + echo "\# AUTOMATICALLY GENERATED FROM Makefile" >>integration/$${name}; \ + if [ -n "$${substs}" ]; then \ + cat $(srcdir)/integration/utils.sh $(srcdir)/integration/$${name}.sh \ + | sed "$${substs}" >>integration/$${name}; \ + else \ + cat $(srcdir)/integration/utils.sh $(srcdir)/integration/$${name}.sh \ + >>integration/$${name}; \ + fi; \ + chmod +x integration/$${name} + +ATF_SH_DEPS = \ + $(srcdir)/integration/utils.sh \ + Makefile + +EXTRA_DIST += integration/utils.sh + +tests_integration_SCRIPTS = integration/cmd_about_test +CLEANFILES += integration/cmd_about_test +EXTRA_DIST += integration/cmd_about_test.sh +integration/cmd_about_test: $(srcdir)/integration/cmd_about_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_about_test"; \ + substs='s,__KYUA_DOCDIR__,$(docdir),g'; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_config_test +CLEANFILES += integration/cmd_config_test +EXTRA_DIST += integration/cmd_config_test.sh +integration/cmd_config_test: $(srcdir)/integration/cmd_config_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_config_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_db_exec_test +CLEANFILES += integration/cmd_db_exec_test +EXTRA_DIST += integration/cmd_db_exec_test.sh +integration/cmd_db_exec_test: $(srcdir)/integration/cmd_db_exec_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_db_exec_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_db_migrate_test +CLEANFILES += integration/cmd_db_migrate_test +EXTRA_DIST += integration/cmd_db_migrate_test.sh +integration/cmd_db_migrate_test: $(srcdir)/integration/cmd_db_migrate_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_db_migrate_test"; \ + substs='s,__KYUA_STOREDIR__,$(storedir),g'; \ + substs="$${substs};s,__KYUA_STORETESTDATADIR__,$(tests_storedir),g"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_debug_test +CLEANFILES += integration/cmd_debug_test +EXTRA_DIST += integration/cmd_debug_test.sh +integration/cmd_debug_test: $(srcdir)/integration/cmd_debug_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_debug_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_help_test +CLEANFILES += integration/cmd_help_test +EXTRA_DIST += integration/cmd_help_test.sh +integration/cmd_help_test: $(srcdir)/integration/cmd_help_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_help_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_list_test +CLEANFILES += integration/cmd_list_test +EXTRA_DIST += integration/cmd_list_test.sh +integration/cmd_list_test: $(srcdir)/integration/cmd_list_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_list_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_report_test +CLEANFILES += integration/cmd_report_test +EXTRA_DIST += integration/cmd_report_test.sh +integration/cmd_report_test: $(srcdir)/integration/cmd_report_test.sh \ + $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_report_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_report_html_test +CLEANFILES += integration/cmd_report_html_test +EXTRA_DIST += integration/cmd_report_html_test.sh +integration/cmd_report_html_test: \ + $(srcdir)/integration/cmd_report_html_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_report_html_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_report_junit_test +CLEANFILES += integration/cmd_report_junit_test +EXTRA_DIST += integration/cmd_report_junit_test.sh +integration/cmd_report_junit_test: \ + $(srcdir)/integration/cmd_report_junit_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_report_junit_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/cmd_test_test +CLEANFILES += integration/cmd_test_test +EXTRA_DIST += integration/cmd_test_test.sh +integration/cmd_test_test: $(srcdir)/integration/cmd_test_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="cmd_test_test"; \ + $(ATF_SH_BUILD) + +tests_integration_SCRIPTS += integration/global_test +CLEANFILES += integration/global_test +EXTRA_DIST += integration/global_test.sh +integration/global_test: $(srcdir)/integration/global_test.sh $(ATF_SH_DEPS) + $(AM_V_GEN)name="global_test"; \ + $(ATF_SH_BUILD) +endif + +include integration/helpers/Makefile.am.inc diff --git a/integration/cmd_about_test.sh b/integration/cmd_about_test.sh new file mode 100755 index 000000000000..06d5da5ac4c2 --- /dev/null +++ b/integration/cmd_about_test.sh @@ -0,0 +1,158 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# Location of installed documents. Used to validate the output of the about +# messages against the golden files. +: "${KYUA_DOCDIR:=__KYUA_DOCDIR__}" + + +# Common code to validate the output of all about information. +# +# \param file The name of the file with the output. +check_all() { + local file="${1}"; shift + + grep -E 'kyua .*[0-9]+\.[0-9]+' "${file}" || \ + atf_fail 'No version reported' + grep 'Copyright' "${file}" || atf_fail 'No license reported' + grep '^\*[^<>]*$' "${file}" || atf_fail 'No authors reported' + grep '^\*.*<.*@.*>$' "${file}" || atf_fail 'No contributors reported' + grep 'Homepage' "${file}" || atf_fail 'No homepage reported' +} + + +utils_test_case all_topics__installed +all_topics__installed_head() { + atf_set "require.files" "${KYUA_DOCDIR}/AUTHORS" \ + "${KYUA_DOCDIR}/CONTRIBUTORS" "${KYUA_DOCDIR}/LICENSE" +} +all_topics__installed_body() { + atf_check -s exit:0 -o save:stdout -e empty kyua about + check_all stdout +} + + +utils_test_case all_topics__override +all_topics__override_body() { + mkdir docs + echo "* Author (no email)" >docs/AUTHORS + echo "* Contributor " >docs/CONTRIBUTORS + echo "Copyright text" >docs/LICENSE + export KYUA_DOCDIR=docs + atf_check -s exit:0 -o save:stdout -e empty kyua about + check_all stdout +} + + +utils_test_case topic__authors__installed +topic__authors__installed_head() { + atf_set "require.files" "${KYUA_DOCDIR}/AUTHORS" \ + "${KYUA_DOCDIR}/CONTRIBUTORS" +} +topic__authors__installed_body() { + grep -h '^\* ' "${KYUA_DOCDIR}/AUTHORS" "${KYUA_DOCDIR}/CONTRIBUTORS" \ + >expout + atf_check -s exit:0 -o file:expout -e empty kyua about authors +} + + +utils_test_case topic__authors__override +topic__authors__override_body() { + mkdir docs + echo "* Author (no email)" >docs/AUTHORS + echo "* Contributor " >docs/CONTRIBUTORS + export KYUA_DOCDIR=docs + cat docs/AUTHORS docs/CONTRIBUTORS >expout + atf_check -s exit:0 -o file:expout -e empty kyua about authors +} + + +utils_test_case topic__license__installed +topic__license__installed_head() { + atf_set "require.files" "${KYUA_DOCDIR}/LICENSE" +} +topic__license__installed_body() { + atf_check -s exit:0 -o file:"${KYUA_DOCDIR}/LICENSE" -e empty \ + kyua about license +} + + +utils_test_case topic__license__override +topic__license__override_body() { + mkdir docs + echo "Copyright text" >docs/LICENSE + export KYUA_DOCDIR=docs + atf_check -s exit:0 -o file:docs/LICENSE -e empty kyua about license +} + + +utils_test_case topic__version +topic__version_body() { + atf_check -s exit:0 -o save:stdout -e empty kyua about version + + local lines="$(wc -l stdout | awk '{ print $1 }')" + [ "${lines}" -eq 1 ] || atf_fail "Version query returned more than one line" + + grep -E '^kyua (.*) [0-9]+\.[0-9]+$' stdout || \ + atf_fail "Invalid version message" +} + + +utils_test_case topic__invalid +topic__invalid_body() { + cat >experr <stderr <"${HOME}/.kyua/kyua.conf" <expout <"${HOME}/.kyua/kyua.conf" <expout <experr <kyua.conf <kyua.conf <.kyua/kyua.conf <kyua.conf <kyua.conf <experr <"${HOME}/.kyua/kyua.conf" <config <experr <experr <Kyuafile <expout <expout2 + atf_check -s exit:0 -o file:expout2 -e empty \ + kyua db-exec --no-headers "SELECT * FROM data ORDER BY a" +} + + +atf_init_test_cases() { + atf_add_test_case one_arg + atf_add_test_case many_args + atf_add_test_case no_args + atf_add_test_case invalid_statement + atf_add_test_case no_create_store + + atf_add_test_case results_file__default_home + atf_add_test_case results_file__explicit__ok + atf_add_test_case results_file__explicit__fail + + atf_add_test_case no_headers_flag +} diff --git a/integration/cmd_db_migrate_test.sh b/integration/cmd_db_migrate_test.sh new file mode 100755 index 000000000000..404a4e774019 --- /dev/null +++ b/integration/cmd_db_migrate_test.sh @@ -0,0 +1,167 @@ +# Copyright 2013 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# Location of installed schema files. +: "${KYUA_STOREDIR:=__KYUA_STOREDIR__}" + + +# Location of installed test data files. +: "${KYUA_STORETESTDATADIR:=__KYUA_STORETESTDATADIR__}" + + +# Creates an empty old-style action database. +# +# \param ... Files that contain SQL commands to be run. +create_historical_db() { + mkdir -p "${HOME}/.kyua" + cat "${@}" | sqlite3 "${HOME}/.kyua/store.db" +} + + +# Creates an empty results file. +# +# \param ... Files that contain SQL commands to be run. +create_results_file() { + mkdir -p "${HOME}/.kyua/store" + local dbname="results.$(utils_test_suite_id)-20140718-173200-123456.db" + cat "${@}" | sqlite3 "${HOME}/.kyua/store/${dbname}" +} + + +utils_test_case upgrade__from_v1 +upgrade__from_v1_head() { + atf_set require.files \ + "${KYUA_STORETESTDATADIR}/schema_v1.sql" \ + "${KYUA_STORETESTDATADIR}/testdata_v1.sql" \ + "${KYUA_STOREDIR}/migrate_v1_v2.sql" \ + "${KYUA_STOREDIR}/migrate_v2_v3.sql" + atf_set require.progs "sqlite3" +} +upgrade__from_v1_body() { + create_historical_db "${KYUA_STORETESTDATADIR}/schema_v1.sql" \ + "${KYUA_STORETESTDATADIR}/testdata_v1.sql" + atf_check -s exit:0 -o empty -e empty kyua db-migrate + for f in \ + "results.test_suite_root.20130108-111331-000000.db" \ + "results.usr_tests.20130108-123832-000000.db" \ + "results.usr_tests.20130108-112635-000000.db" + do + [ -f "${HOME}/.kyua/store/${f}" ] || atf_fail "Expected file ${f}" \ + "was not created" + done + [ ! -f "${HOME}/.kyua/store.db" ] || atf_fail "Historical database not" \ + "deleted" +} + + +utils_test_case upgrade__from_v2 +upgrade__from_v2_head() { + atf_set require.files \ + "${KYUA_STORETESTDATADIR}/schema_v2.sql" \ + "${KYUA_STORETESTDATADIR}/testdata_v2.sql" \ + "${KYUA_STOREDIR}/migrate_v2_v3.sql" + atf_set require.progs "sqlite3" +} +upgrade__from_v2_body() { + create_historical_db "${KYUA_STORETESTDATADIR}/schema_v2.sql" \ + "${KYUA_STORETESTDATADIR}/testdata_v2.sql" + atf_check -s exit:0 -o empty -e empty kyua db-migrate + for f in \ + "results.test_suite_root.20130108-111331-000000.db" \ + "results.usr_tests.20130108-123832-000000.db" \ + "results.usr_tests.20130108-112635-000000.db" + do + [ -f "${HOME}/.kyua/store/${f}" ] || atf_fail "Expected file ${f}" \ + "was not created" + done + [ ! -f "${HOME}/.kyua/store.db" ] || atf_fail "Historical database not" \ + "deleted" +} + + +utils_test_case already_up_to_date +already_up_to_date_head() { + atf_set require.files "${KYUA_STOREDIR}/schema_v3.sql" + atf_set require.progs "sqlite3" +} +already_up_to_date_body() { + create_results_file "${KYUA_STOREDIR}/schema_v3.sql" + atf_check -s exit:1 -o empty -e match:"already at schema version" \ + kyua db-migrate +} + + +utils_test_case need_upgrade +need_upgrade_head() { + atf_set require.files "${KYUA_STORETESTDATADIR}/schema_v1.sql" + atf_set require.progs "sqlite3" +} +need_upgrade_body() { + create_results_file "${KYUA_STORETESTDATADIR}/schema_v1.sql" + atf_check -s exit:2 -o empty \ + -e match:"database has schema version 1.*use db-migrate" kyua report +} + + +utils_test_case results_file__ok +results_file__ok_body() { + echo "This is not a valid database" >test.db + atf_check -s exit:1 -o empty -e match:"Migration failed" \ + kyua db-migrate --results-file ./test.db +} + + +utils_test_case results_file__fail +results_file__fail_body() { + atf_check -s exit:1 -o empty -e match:"No previous results.*test.db" \ + kyua db-migrate --results-file ./test.db +} + + +utils_test_case too_many_arguments +too_many_arguments_body() { + cat >stderr <Kyuafile <experr <Kyuafile <experr <Kyuafile <expout < passed +EOF +cat >experr <Kyuafile <expout < failed: This fails on purpose +EOF + cat >experr <Kyuafile <experr <experr <experr <Kyuafile <expout < passed +EOF + atf_check -s exit:0 -o file:expout -e empty kyua debug \ + --stdout=saved.out --stderr=saved.err single:with_cleanup + + cat >expout <experr <Kyuafile <expout < passed +EOF + atf_check -s exit:0 -o file:expout -e empty kyua debug \ + --stdout=saved.out --stderr=saved.err second:pass + + cat >expout <experr <root/Kyuafile <root/subdir/Kyuafile <expout < failed: This fails on purpose +EOF + cat >experr <Kyuafile <expout < passed +EOF + cat >experr <"my-config" <Kyuafile <Kyuafile <expout < passed +EOF +cat >experr <Kyuafile <myfile <experr <Kyuafile <Kyuafile <non_executable + + cat >experr <experr <expout < broken: Invalid header for test case list; expecting Content-Type for application/X-atf-tp version 1, got '' +EOF + # CHECK_STYLE_ENABLE + atf_check -s exit:1 -o file:expout -e empty kyua debug \ + crash_on_list:__test_cases_list__ + + # CHECK_STYLE_DISABLE + cat >expout < broken: Permission denied to run test program +EOF + # CHECK_STYLE_ENABLE + atf_check -s exit:1 -o file:expout -e empty kyua debug \ + non_executable:__test_cases_list__ +} + + +atf_init_test_cases() { + atf_add_test_case no_args + atf_add_test_case many_args + atf_add_test_case one_arg__ok_pass + atf_add_test_case one_arg__ok_fail + atf_add_test_case one_arg__no_match + atf_add_test_case one_arg__no_test_case + atf_add_test_case one_arg__bad_filter + + atf_add_test_case body_and_cleanup + + atf_add_test_case stdout_stderr_flags + + atf_add_test_case args_are_relative + + atf_add_test_case only_load_used_test_programs + + atf_add_test_case config_behavior + + atf_add_test_case build_root_flag + atf_add_test_case kyuafile_flag__ok + atf_add_test_case missing_kyuafile + atf_add_test_case bogus_kyuafile + atf_add_test_case bogus_test_program +} diff --git a/integration/cmd_help_test.sh b/integration/cmd_help_test.sh new file mode 100755 index 000000000000..d8afbd0e6aba --- /dev/null +++ b/integration/cmd_help_test.sh @@ -0,0 +1,93 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +utils_test_case global +global_body() { + atf_check -s exit:0 -o save:stdout -e empty kyua help + grep -E 'kyua .*[0-9]+\.[0-9]+' stdout || atf_fail 'No version reported' + grep '^Usage: kyua' stdout || atf_fail 'No usage line printed' + grep -- '--loglevel' stdout || atf_fail 'Generic options not printed' + if grep -- '--show' stdout; then + atf_fail 'One option of the about subcommand appeared in the output' + fi + grep 'about *Shows detailed' stdout || atf_fail 'Commands not printed' +} + + +utils_test_case one_command +one_command_body() { + atf_check -s exit:0 -o save:stdout -e empty kyua help test + grep -E 'kyua .*[0-9]+\.[0-9]+' stdout || atf_fail 'No version reported' + grep '^Usage: kyua' stdout || atf_fail 'No usage line printed' + grep '^Run tests' stdout || atf_fail 'No description printed' + grep -- '--loglevel' stdout || atf_fail 'Generic options not printed' + grep -- '--kyuafile' stdout || atf_fail 'Command options not printed' + if grep 'about: Shows detailed' stdout; then + atf_fail 'Printed table of commands, but should not have done so' + fi +} + + +utils_test_case ignore_bad_config +ignore_bad_config_body() { + echo 'this is an invalid configuration file' >bad-config + atf_check -s exit:0 -o save:stdout -e empty kyua -c bad-config help + grep '^Usage: kyua' stdout || atf_fail 'No usage line printed' + grep -- '--loglevel' stdout || atf_fail 'Generic options not printed' +} + + +utils_test_case unknown_command +unknown_command_body() { + cat >stderr <stderr <Kyuafile <subdir/Kyuafile <expout <Kyuafile <subdir/Kyuafile <expout <Kyuafile <expout <Kyuafile <expout <experr <experr <Kyuafile <subdir/Kyuafile <expout <experr <experr <Kyuafile <experr <Kyuafile <expout <experr <root/Kyuafile <root/subdir/Kyuafile <expout <Kyuafile <expout <"my-config" <Kyuafile <Kyuafile <first + utils_cp_helper simple_all_pass build/first + + cat >subdir/Kyuafile <subdir/second + utils_cp_helper simple_some_fail build/subdir/second + + cat >expout <Kyuafile <myfile <expout <Kyuafile <myfile <expout <Kyuafile <subdir/Kyuafile <expout <Kyuafile <experr <Kyuafile <experr <experr <subdir/Kyuafile <experr <subdir/Kyuafile <experr <Kyuafile <Kyuafile <non_executable + + cat >expout <Kyuafile <subdir/Kyuafile <subdir/ok + +# CHECK_STYLE_DISABLE + cat >experr <Kyuafile <"${dbfile_name}" + rm stdout + + # Ensure the results of 'report-html' come from the database. + rm Kyuafile simple_all_pass simple_some_fail metadata +} + + +# Ensure a file has a set of strings. +# +# \param file The name of the file to check. +# \param ... List of strings to check. +check_in_file() { + local file="${1}"; shift + + while [ ${#} -gt 0 ]; do + echo "Checking for presence of '${1}' in ${file}" + if grep "${1}" "${file}" >/dev/null; then + : + else + atf_fail "Test case output not found in HTML page ${file}" + fi + shift + done +} + + +# Ensure a file does not have a set of strings. +# +# \param file The name of the file to check. +# \param ... List of strings to check. +check_not_in_file() { + local file="${1}"; shift + + while [ ${#} -gt 0 ]; do + echo "Checking for lack of '${1}' in ${file}" + if grep "${1}" "${file}" >/dev/null; then + atf_fail "Spurious test case output found in HTML page" + fi + shift + done +} + + +utils_test_case default_behavior__ok +default_behavior__ok_body() { + run_tests "mock1" unused_dbfile_name + + atf_check -s exit:0 -o ignore -e empty kyua report-html + for f in \ + html/index.html \ + html/context.html \ + html/simple_all_pass_skip.html \ + html/simple_some_fail_fail.html + do + test -f "${f}" || atf_fail "Missing ${f}" + done + + atf_check -o match:"2 TESTS FAILING" cat html/index.html + + check_in_file html/simple_all_pass_skip.html \ + "This is the stdout of skip" "This is the stderr of skip" + check_not_in_file html/simple_all_pass_skip.html \ + "This is the stdout of pass" "This is the stderr of pass" \ + "This is the stdout of fail" "This is the stderr of fail" \ + "Test case did not write anything to" + + check_in_file html/simple_some_fail_fail.html \ + "This is the stdout of fail" "This is the stderr of fail" + check_not_in_file html/simple_some_fail_fail.html \ + "This is the stdout of pass" "This is the stderr of pass" \ + "This is the stdout of skip" "This is the stderr of skip" \ + "Test case did not write anything to" + + check_in_file html/metadata_one_property.html \ + "description = Does nothing but has one metadata property" + check_not_in_file html/metadata_one_property.html \ + "allowed_architectures = some-architecture" + + check_in_file html/metadata_many_properties.html \ + "allowed_architectures = some-architecture" + check_not_in_file html/metadata_many_properties.html \ + "description = Does nothing but has one metadata property" +} + + +utils_test_case default_behavior__no_store +default_behavior__no_store_body() { + echo 'kyua: E: No previous results file found for test suite' \ + "$(utils_test_suite_id)." >experr + atf_check -s exit:2 -o empty -e file:experr kyua report-html +} + + +utils_test_case results_file__explicit +results_file__explicit_body() { + run_tests "mock1" dbfile_name1 + run_tests "mock2" dbfile_name2 + + atf_check -s exit:0 -o ignore -e empty kyua report-html \ + --results-file="$(cat dbfile_name1)" + grep "MOCK.*mock1" html/context.html || atf_fail "Invalid context in report" + + rm -rf html + atf_check -s exit:0 -o ignore -e empty kyua report-html \ + --results-file="$(cat dbfile_name2)" + grep "MOCK.*mock2" html/context.html || atf_fail "Invalid context in report" +} + + +utils_test_case results_file__not_found +results_file__not_found_body() { + atf_check -s exit:2 -o empty -e match:"kyua: E: No previous results.*foo" \ + kyua report-html --results-file=foo +} + + +utils_test_case force__yes +force__yes_body() { + run_tests "mock1" unused_dbfile_name + + atf_check -s exit:0 -o ignore -e empty kyua report-html + test -f html/index.html || atf_fail "Expected file not created" + rm html/index.html + atf_check -s exit:0 -o ignore -e empty kyua report-html --force + test -f html/index.html || atf_fail "Expected file not created" +} + + +utils_test_case force__no +force__no_body() { + run_tests "mock1" unused_dbfile_name + + atf_check -s exit:0 -o ignore -e empty kyua report-html + test -f html/index.html || atf_fail "Expected file not created" + rm html/index.html + +cat >experr <experr + atf_check -s exit:2 -o empty -e file:experr kyua report-html \ + --results-filter=passed,foo-bar +} + + +atf_init_test_cases() { + atf_add_test_case default_behavior__ok + atf_add_test_case default_behavior__no_store + + atf_add_test_case results_file__explicit + atf_add_test_case results_file__not_found + + atf_add_test_case force__yes + atf_add_test_case force__no + + atf_add_test_case output__explicit + + atf_add_test_case results_filter__ok + atf_add_test_case results_filter__invalid +} diff --git a/integration/cmd_report_junit_test.sh b/integration/cmd_report_junit_test.sh new file mode 100755 index 000000000000..af1a464f6004 --- /dev/null +++ b/integration/cmd_report_junit_test.sh @@ -0,0 +1,300 @@ +# Copyright 2014 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# Executes a mock test suite to generate data in the database. +# +# \param mock_env The value to store in a MOCK variable in the environment. +# Use this to be able to differentiate executions by inspecting the +# context of the output. +# \param dbfile_name File to which to write the path to the generated database +# file. +run_tests() { + local mock_env="${1}"; shift + local dbfile_name="${1}"; shift + + cat >Kyuafile <"${dbfile_name}" + rm stdout + + # Ensure the results of 'report-junit' come from the database. + rm Kyuafile simple_all_pass +} + + +# Removes the contents of a properties tag from stdout. +strip_properties='awk " +BEGIN { skip = 0; } + +/<\/properties>/ { + print \"\"; + skip = 0; + next; +} + +// { + print \"\"; + print \"CONTENTS STRIPPED BY TEST\"; + skip = 1; + next; +} + +{ if (!skip) print; }"' + + +utils_test_case default_behavior__ok +default_behavior__ok_body() { + utils_install_times_wrapper + + run_tests "mock1 +this should not be seen +mock1 new line" unused_dbfile_name + + cat >expout < + + +CONTENTS STRIPPED BY TEST + + +This is the stdout of pass + +Test case metadata +------------------ + +allowed_architectures is empty +allowed_platforms is empty +description is empty +has_cleanup = false +is_exclusive = false +required_configs is empty +required_disk_space = 0 +required_files is empty +required_memory = 0 +required_programs is empty +required_user is empty +timeout = 300 + +Timing information +------------------ + +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Original stderr +--------------- + +This is the stderr of pass + + + + +This is the stdout of skip + +Skipped result details +---------------------- + +The reason for skipping is this + +Test case metadata +------------------ + +allowed_architectures is empty +allowed_platforms is empty +description is empty +has_cleanup = false +is_exclusive = false +required_configs is empty +required_disk_space = 0 +required_files is empty +required_memory = 0 +required_programs is empty +required_user is empty +timeout = 300 + +Timing information +------------------ + +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Original stderr +--------------- + +This is the stderr of skip + + + +EOF + atf_check -s exit:0 -o file:expout -e empty -x "kyua report-junit" \ + "| ${strip_properties}" +} + + +utils_test_case default_behavior__no_store +default_behavior__no_store_body() { + echo 'kyua: E: No previous results file found for test suite' \ + "$(utils_test_suite_id)." >experr + atf_check -s exit:2 -o empty -e file:experr kyua report-junit +} + + +utils_test_case results_file__explicit +results_file__explicit_body() { + run_tests "mock1" dbfile_name1 + run_tests "mock2" dbfile_name2 + + atf_check -s exit:0 -o match:"MOCK.*mock1" -o not-match:"MOCK.*mock2" \ + -e empty kyua report-junit --results-file="$(cat dbfile_name1)" + atf_check -s exit:0 -o not-match:"MOCK.*mock1" -o match:"MOCK.*mock2" \ + -e empty kyua report-junit --results-file="$(cat dbfile_name2)" +} + + +utils_test_case results_file__not_found +results_file__not_found_body() { + atf_check -s exit:2 -o empty -e match:"kyua: E: No previous results.*foo" \ + kyua report-junit --results-file=foo +} + + +utils_test_case output__explicit +output__explicit_body() { + run_tests unused_mock unused_dbfile_name + + cat >report < + + +CONTENTS STRIPPED BY TEST + + +This is the stdout of pass + +Test case metadata +------------------ + +allowed_architectures is empty +allowed_platforms is empty +description is empty +has_cleanup = false +is_exclusive = false +required_configs is empty +required_disk_space = 0 +required_files is empty +required_memory = 0 +required_programs is empty +required_user is empty +timeout = 300 + +Timing information +------------------ + +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Original stderr +--------------- + +This is the stderr of pass + + + + +This is the stdout of skip + +Skipped result details +---------------------- + +The reason for skipping is this + +Test case metadata +------------------ + +allowed_architectures is empty +allowed_platforms is empty +description is empty +has_cleanup = false +is_exclusive = false +required_configs is empty +required_disk_space = 0 +required_files is empty +required_memory = 0 +required_programs is empty +required_user is empty +timeout = 300 + +Timing information +------------------ + +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Original stderr +--------------- + +This is the stderr of skip + + + +EOF + + atf_check -s exit:0 -o file:report -e empty -x kyua report-junit \ + --output=/dev/stdout "| ${strip_properties} | ${utils_strip_times}" + atf_check -s exit:0 -o empty -e save:stderr kyua report-junit \ + --output=/dev/stderr + atf_check -s exit:0 -o file:report -x cat stderr \ + "| ${strip_properties} | ${utils_strip_times}" + + atf_check -s exit:0 -o empty -e empty kyua report-junit \ + --output=my-file + atf_check -s exit:0 -o file:report -x cat my-file \ + "| ${strip_properties} | ${utils_strip_times}" +} + + +atf_init_test_cases() { + atf_add_test_case default_behavior__ok + atf_add_test_case default_behavior__no_store + + atf_add_test_case results_file__explicit + atf_add_test_case results_file__not_found + + atf_add_test_case output__explicit +} diff --git a/integration/cmd_report_test.sh b/integration/cmd_report_test.sh new file mode 100755 index 000000000000..18a5db386dfd --- /dev/null +++ b/integration/cmd_report_test.sh @@ -0,0 +1,381 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# Executes a mock test suite to generate data in the database. +# +# \param mock_env The value to store in a MOCK variable in the environment. +# Use this to be able to differentiate executions by inspecting the +# context of the output. +# \param dbfile_name File to which to write the path to the generated database +# file. +run_tests() { + local mock_env="${1}"; shift + local dbfile_name="${1}"; shift + + cat >Kyuafile <"${dbfile_name}" + rm stdout + + # Ensure the results of 'report' come from the database. + rm Kyuafile simple_all_pass +} + + +utils_test_case default_behavior__ok +default_behavior__ok_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report + + run_tests "mock2" dbfile_name2 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name2) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report +} + + +utils_test_case default_behavior__no_store +default_behavior__no_store_body() { + echo 'kyua: E: No previous results file found for test suite' \ + "$(utils_test_suite_id)." >experr + atf_check -s exit:2 -o empty -e file:experr kyua report +} + + +utils_test_case results_file__explicit +results_file__explicit_body() { + run_tests "mock1" dbfile_name1 + run_tests "mock2" dbfile_name2 + + atf_check -s exit:0 -o match:"MOCK=mock1" -o not-match:"MOCK=mock2" \ + -e empty kyua report --results-file="$(cat dbfile_name1)" \ + --verbose + atf_check -s exit:0 -o not-match:"MOCK=mock1" -o match:"MOCK=mock2" \ + -e empty kyua report --results-file="$(cat dbfile_name2)" \ + --verbose +} + + +utils_test_case results_file__not_found +results_file__not_found_body() { + atf_check -s exit:2 -o empty -e match:"kyua: E: No previous results.*foo" \ + kyua report --results-file=foo +} + + +utils_test_case output__explicit +output__explicit_body() { + run_tests unused_mock dbfile_name + + cat >report < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + + atf_check -s exit:0 -o file:report -e empty -x kyua report \ + --output=/dev/stdout "| ${utils_strip_times_but_not_ids}" + atf_check -s exit:0 -o empty -e save:stderr kyua report \ + --output=/dev/stderr + atf_check -s exit:0 -o file:report -x cat stderr \ + "| ${utils_strip_times_but_not_ids}" + + atf_check -s exit:0 -o empty -e empty kyua report \ + --output=my-file + atf_check -s exit:0 -o file:report -x cat my-file \ + "| ${utils_strip_times_but_not_ids}" +} + + +utils_test_case filter__ok +filter__ok_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 1 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + simple_all_pass:skip +} + + +utils_test_case filter__ok_passed_excluded_by_default +filter__ok_passed_excluded_by_default_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + # Passed results are excluded by default so they are not displayed even if + # requested with a test case filter. This might be somewhat confusing... + cat >expout < Summary +Results read from $(cat dbfile_name1) +Test cases: 1 total, 0 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + simple_all_pass:pass + cat >expout < Passed tests +simple_all_pass:pass -> passed [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 1 total, 0 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + --results-filter= simple_all_pass:pass +} + + +utils_test_case filter__no_match +filter__no_match_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 1 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + cat >experr <expout < Execution context +Current directory: ${real_cwd} +Environment variables: +EOF + # $_ is a bash variable. To keep our tests stable, we override its value + # below to match the hardcoded value in run_tests. + env \ + HOME="${real_cwd}" \ + MOCK="mock1 +has multiple lines +and terminates here" \ + _='fake-value' \ + "$(atf_get_srcdir)/helpers/dump_env" ' ' ' ' >>expout + cat >>expout < simple_all_pass:skip +Result: skipped: The reason for skipping is this +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Duration: S.UUUs + +Metadata: + allowed_architectures is empty + allowed_platforms is empty + description is empty + has_cleanup = false + is_exclusive = false + required_configs is empty + required_disk_space = 0 + required_files is empty + required_memory = 0 + required_programs is empty + required_user is empty + timeout = 300 + +Standard output: +This is the stdout of skip + +Standard error: +This is the stderr of skip +===> Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Start time: YYYY-MM-DDTHH:MM:SS.ssssssZ +End time: YYYY-MM-DDTHH:MM:SS.ssssssZ +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty -x kyua report --verbose \ + "| ${utils_strip_times_but_not_ids}" +} + + +utils_test_case results_filter__empty +results_filter__empty_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Passed tests +simple_all_pass:pass -> passed [S.UUUs] +===> Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report --results-filter= +} + + +utils_test_case results_filter__one +results_filter__one_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Passed tests +simple_all_pass:pass -> passed [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + --results-filter=passed +} + + +utils_test_case results_filter__multiple_all_match +results_filter__multiple_all_match_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Passed tests +simple_all_pass:pass -> passed [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + --results-filter=skipped,passed +} + + +utils_test_case results_filter__multiple_some_match +results_filter__multiple_some_match_body() { + utils_install_times_wrapper + + run_tests "mock1" dbfile_name1 + + cat >expout < Skipped tests +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +===> Summary +Results read from $(cat dbfile_name1) +Test cases: 2 total, 1 skipped, 0 expected failures, 0 broken, 0 failed +Total time: S.UUUs +EOF + atf_check -s exit:0 -o file:expout -e empty kyua report \ + --results-filter=skipped,xfail,broken,failed +} + + +atf_init_test_cases() { + atf_add_test_case default_behavior__ok + atf_add_test_case default_behavior__no_store + + atf_add_test_case results_file__explicit + atf_add_test_case results_file__not_found + + atf_add_test_case filter__ok + atf_add_test_case filter__ok_passed_excluded_by_default + atf_add_test_case filter__no_match + + atf_add_test_case verbose + + atf_add_test_case output__explicit + + atf_add_test_case results_filter__empty + atf_add_test_case results_filter__one + atf_add_test_case results_filter__multiple_all_match + atf_add_test_case results_filter__multiple_some_match +} diff --git a/integration/cmd_test_test.sh b/integration/cmd_test_test.sh new file mode 100755 index 000000000000..bc8c62daf223 --- /dev/null +++ b/integration/cmd_test_test.sh @@ -0,0 +1,1071 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +utils_test_case one_test_program__all_pass +one_test_program__all_pass_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < passed [S.UUUs] +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + + utils_cp_helper simple_all_pass . + atf_check -s exit:0 -o file:expout -e empty kyua test +} + + +utils_test_case one_test_program__some_fail +one_test_program__some_fail_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < failed: This fails on purpose [S.UUUs] +simple_some_fail:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/2 passed (1 failed) +EOF + + utils_cp_helper simple_some_fail . + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case many_test_programs__all_pass +many_test_programs__all_pass_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] +fourth:main -> skipped: Required file '/non-existent/foo' not found [S.UUUs] +second:pass -> passed [S.UUUs] +second:skip -> skipped: The reason for skipping is this [S.UUUs] +third:pass -> passed [S.UUUs] +third:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +7/7 passed (0 failed) +EOF + + utils_cp_helper simple_all_pass first + utils_cp_helper simple_all_pass second + utils_cp_helper simple_all_pass third + echo "not executed" >fourth; chmod +x fourth + atf_check -s exit:0 -o file:expout -e empty kyua test +} + + +utils_test_case many_test_programs__some_fail +many_test_programs__some_fail_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < failed: This fails on purpose [S.UUUs] +first:pass -> passed [S.UUUs] +fourth:main -> failed: Returned non-success exit status 76 [S.UUUs] +second:fail -> failed: This fails on purpose [S.UUUs] +second:pass -> passed [S.UUUs] +third:pass -> passed [S.UUUs] +third:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +4/7 passed (3 failed) +EOF + + utils_cp_helper simple_some_fail first + utils_cp_helper simple_some_fail second + utils_cp_helper simple_all_pass third + echo '#! /bin/sh' >fourth + echo 'exit 76' >>fourth + chmod +x fourth + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case expect__all_pass +expect__all_pass_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < expected_failure: This is the reason for death [S.UUUs] +expect_all_pass:exit -> expected_failure: Exiting with correct code [S.UUUs] +expect_all_pass:failure -> expected_failure: Oh no: Forced failure [S.UUUs] +expect_all_pass:signal -> expected_failure: Exiting with correct signal [S.UUUs] +expect_all_pass:timeout -> expected_failure: This times out [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +5/5 passed (0 failed) +EOF +# CHECK_STYLE_ENABLE + + utils_cp_helper expect_all_pass . + atf_check -s exit:0 -o file:expout -e empty kyua test +} + + +utils_test_case expect__some_fail +expect__some_fail_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < failed: Test case was expected to terminate abruptly but it continued execution [S.UUUs] +expect_some_fail:exit -> failed: Test case expected to exit with code 12 but got code 34 [S.UUUs] +expect_some_fail:failure -> failed: Test case was expecting a failure but none were raised [S.UUUs] +expect_some_fail:pass -> passed [S.UUUs] +expect_some_fail:signal -> failed: Test case expected to receive signal 15 but got 9 [S.UUUs] +expect_some_fail:timeout -> failed: Test case was expected to hang but it continued execution [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/6 passed (5 failed) +EOF +# CHECK_STYLE_ENABLE + + utils_cp_helper expect_some_fail . + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case premature_exit +premature_exit_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < broken: Premature exit; test case received signal 9 [S.UUUs] +bogus_test_cases:exit -> broken: Premature exit; test case exited with code 0 [S.UUUs] +bogus_test_cases:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/3 passed (2 failed) +EOF +# CHECK_STYLE_ENABLE + + utils_cp_helper bogus_test_cases . + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case no_args +no_args_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <subdir/Kyuafile <expout < passed [S.UUUs] +simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] +subdir/simple_some_fail:fail -> failed: This fails on purpose [S.UUUs] +subdir/simple_some_fail:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +3/4 passed (1 failed) +EOF + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case one_arg__subdir +one_arg__subdir_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <subdir/Kyuafile <expout < passed [S.UUUs] +subdir/simple_all_pass:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF +# CHECK_STYLE_ENABLE + atf_check -s exit:0 -o file:expout -e empty kyua test subdir +} + + +utils_test_case one_arg__test_case +one_arg__test_case_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/1 passed (0 failed) +EOF + atf_check -s exit:0 -o file:expout -e empty kyua test first:skip +} + + +utils_test_case one_arg__test_program +one_arg__test_program_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < failed: This fails on purpose [S.UUUs] +second:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/2 passed (1 failed) +EOF + atf_check -s exit:1 -o file:expout -e empty kyua test second +} + + +utils_test_case one_arg__invalid +one_arg__invalid_body() { +cat >experr <experr <Kyuafile <subdir/Kyuafile <expout < passed [S.UUUs] +subdir/second:fail -> failed: This fails on purpose [S.UUUs] +subdir/second:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/3 passed (1 failed) +EOF + atf_check -s exit:1 -o file:expout -e empty kyua test subdir first:pass +} + + +utils_test_case many_args__invalid +many_args__invalid_body() { +cat >experr <experr <Kyuafile <expout <experr <Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] +third:fail -> failed: This fails on purpose [S.UUUs] +third:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +3/4 passed (1 failed) +EOF + + cat >experr <root/Kyuafile <root/subdir/Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] +subdir/fourth:fail -> failed: This fails on purpose [S.UUUs] + +Results file id is $(utils_results_id root) +Results saved to $(utils_results_file root) + +2/3 passed (1 failed) +EOF + atf_check -s exit:1 -o file:expout -e empty kyua test \ + -k "$(pwd)/root/Kyuafile" first subdir/fourth:fail +} + + +utils_test_case only_load_used_test_programs +only_load_used_test_programs_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + CREATE_COOKIE="$(pwd)/cookie"; export CREATE_COOKIE + atf_check -s exit:0 -o file:expout -e empty kyua test first + if [ -f "${CREATE_COOKIE}" ]; then + atf_fail "An unmatched test case has been executed, which harms" \ + "performance" + fi +} + + +utils_test_case config_behavior +config_behavior_body() { + cat >"my-config" <Kyuafile <Kyuafile <expout < failed: This fails on purpose [S.UUUs] +some-program:pass -> passed [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +1/2 passed (1 failed) +EOF + + atf_check -s exit:1 -o file:expout -e empty kyua test + +cat >expout <Kyuafile <Kyuafile <Kyuafile <Kyuafile <subdir/Kyuafile <expout < passed [S.UUUs] +first:skip -> skipped: The reason for skipping is this [S.UUUs] +subdir/second:pass -> passed [S.UUUs] +subdir/second:skip -> skipped: The reason for skipping is this [S.UUUs] +subdir/third:pass -> passed [S.UUUs] +subdir/third:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +6/6 passed (0 failed) +EOF + + mkdir build + mkdir build/subdir + utils_cp_helper simple_all_pass build/first + utils_cp_helper simple_all_pass build/subdir/second + utils_cp_helper simple_all_pass build/subdir/third + + atf_check -s exit:0 -o file:expout -e empty kyua test --build-root=build +} + + +utils_test_case kyuafile_flag__no_args +kyuafile_flag__no_args_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <myfile <expout < passed [S.UUUs] +sometest:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + atf_check -s exit:0 -o file:expout -e empty kyua test -k myfile + atf_check -s exit:0 -o file:expout -e empty kyua test --kyuafile=myfile +} + + +utils_test_case kyuafile_flag__some_args +kyuafile_flag__some_args_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <myfile <expout < passed [S.UUUs] +sometest:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + atf_check -s exit:0 -o file:expout -e empty kyua test -k myfile sometest + cat >expout < passed [S.UUUs] +sometest:skip -> skipped: The reason for skipping is this [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +2/2 passed (0 failed) +EOF + atf_check -s exit:0 -o file:expout -e empty kyua test --kyuafile=myfile \ + sometest +} + + +utils_test_case interrupt +interrupt_body() { + cat >Kyuafile <stdout 2>stderr & + pid=${!} + echo "Kyua subprocess is PID ${pid}" + + while [ ! -f body ]; do + echo "Waiting for body to start" + sleep 1 + done + echo "Body started" + sleep 1 + + echo "Sending INT signal to ${pid}" + kill -INT ${pid} + echo "Waiting for process ${pid} to exit" + wait ${pid} + ret=${?} + sed -e 's,^,kyua stdout:,' stdout + sed -e 's,^,kyua stderr:,' stderr + echo "Process ${pid} exited" + [ ${ret} -ne 0 ] || atf_fail 'No error code reported' + + [ -f cleanup ] || atf_fail 'Cleanup part not executed after signal' + atf_expect_pass + + atf_check -s exit:0 -o ignore -e empty grep 'Signal caught' stderr + atf_check -s exit:0 -o ignore -e empty \ + grep 'kyua: E: Interrupted by signal' stderr +} + + +utils_test_case exclusive_tests +exclusive_tests_body() { + cat >Kyuafile <>Kyuafile + done + utils_cp_helper race . + + atf_check \ + -s exit:0 \ + -o match:"100/100 passed" \ + kyua \ + -v parallelism=20 \ + -v test_suites.integration.shared_file="$(pwd)/shared_file" \ + test +} + + +utils_test_case no_test_program_match +no_test_program_match_body() { + utils_install_stable_test_wrapper + + cat >Kyuafile <expout <experr <Kyuafile <expout <experr <experr <subdir/Kyuafile <experr <subdir/Kyuafile <experr <"${HOME}/.kyua/kyua.conf" <Kyuafile <Kyuafile <non_executable + +# CHECK_STYLE_DISABLE + cat >expout < broken: Invalid header for test case list; expecting Content-Type for application/X-atf-tp version 1, got '' [S.UUUs] +non_executable:__test_cases_list__ -> broken: Permission denied to run test program [S.UUUs] + +Results file id is $(utils_results_id) +Results saved to $(utils_results_file) + +0/2 passed (2 failed) +EOF +# CHECK_STYLE_ENABLE + atf_check -s exit:1 -o file:expout -e empty kyua test +} + + +utils_test_case missing_test_program +missing_test_program_body() { + cat >Kyuafile <subdir/Kyuafile <subdir/ok + +# CHECK_STYLE_DISABLE + cat >experr <experr <experr <experr < +#include +#include +#include + + +int +main(void) +{ + std::cerr << "This is not a valid test program!\n"; + + const char* cookie = std::getenv("CREATE_COOKIE"); + if (cookie != NULL && std::strlen(cookie) > 0) { + std::ofstream file(cookie); + if (!file) + std::abort(); + file << "Cookie file\n"; + file.close(); + } + + return EXIT_SUCCESS; +} diff --git a/integration/helpers/bogus_test_cases.cpp b/integration/helpers/bogus_test_cases.cpp new file mode 100644 index 000000000000..1a7c27031e1b --- /dev/null +++ b/integration/helpers/bogus_test_cases.cpp @@ -0,0 +1,64 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +extern "C" { +#include +#include +} + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(die); +ATF_TEST_CASE_BODY(die) +{ + ::kill(::getpid(), SIGKILL); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exit); +ATF_TEST_CASE_BODY(exit) +{ + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pass); +ATF_TEST_CASE_BODY(pass) +{ +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, die); + ATF_ADD_TEST_CASE(tcs, exit); + ATF_ADD_TEST_CASE(tcs, pass); +} diff --git a/integration/helpers/config.cpp b/integration/helpers/config.cpp new file mode 100644 index 000000000000..aa4fef291725 --- /dev/null +++ b/integration/helpers/config.cpp @@ -0,0 +1,58 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include + +#include + + +ATF_TEST_CASE(get_variable); +ATF_TEST_CASE_HEAD(get_variable) +{ + const char* output = ::getenv("CONFIG_VAR_FILE"); + if (output == NULL) { + set_md_var("require.config", "the-variable"); + } else { + if (has_config_var("the-variable")) { + atf::utils::create_file(output, get_config_var("the-variable") + + std::string("\n")); + } else { + atf::utils::create_file(output, "NOT DEFINED\n"); + } + } +} +ATF_TEST_CASE_BODY(get_variable) +{ + ATF_REQUIRE_EQ("value2", get_config_var("the-variable")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, get_variable); +} diff --git a/integration/helpers/dump_env.cpp b/integration/helpers/dump_env.cpp new file mode 100644 index 000000000000..a2e8313a0062 --- /dev/null +++ b/integration/helpers/dump_env.cpp @@ -0,0 +1,74 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Dumps all environment variables. +// +// This helper program allows comparing the printed environment variables +// to what 'kyua report --verbose' may output. It does so by sorting the +// variables and allowing the caller to customize how the output looks +// like (indentation for each line and for continuation lines). + +#include +#include + +#include "utils/env.hpp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +int +main(const int argc, const char* const* const argv) +{ + if (argc != 3) { + std::cerr << "Usage: dump_env \n"; + return EXIT_FAILURE; + } + const char* prefix = argv[1]; + const char* continuation_prefix = argv[2]; + + const std::map< std::string, std::string > env = utils::getallenv(); + for (std::map< std::string, std::string >::const_iterator + iter = env.begin(); iter != env.end(); ++iter) { + const std::string& name = (*iter).first; + const std::vector< std::string > value = text::split( + (*iter).second, '\n'); + + if (value.empty()) { + std::cout << prefix << name << "=\n"; + } else { + std::cout << prefix << name << '=' << value[0] << '\n'; + for (std::vector< std::string >::size_type i = 1; + i < value.size(); ++i) { + std::cout << continuation_prefix << value[i] << '\n'; + } + } + } + + return EXIT_SUCCESS; +} diff --git a/integration/helpers/expect_all_pass.cpp b/integration/helpers/expect_all_pass.cpp new file mode 100644 index 000000000000..a7df16e3a783 --- /dev/null +++ b/integration/helpers/expect_all_pass.cpp @@ -0,0 +1,92 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +extern "C" { +#include +#include +} + +#include + +#include + +#include "utils/test_utils.ipp" + + +ATF_TEST_CASE_WITHOUT_HEAD(die); +ATF_TEST_CASE_BODY(die) +{ + expect_death("This is the reason for death"); + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exit); +ATF_TEST_CASE_BODY(exit) +{ + expect_exit(12, "Exiting with correct code"); + std::exit(12); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(failure); +ATF_TEST_CASE_BODY(failure) +{ + expect_fail("Oh no"); + fail("Forced failure"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(signal); +ATF_TEST_CASE_BODY(signal) +{ + expect_signal(SIGTERM, "Exiting with correct signal"); + ::kill(::getpid(), SIGTERM); +} + + +ATF_TEST_CASE(timeout); +ATF_TEST_CASE_HEAD(timeout) +{ + set_md_var("timeout", "1"); +} +ATF_TEST_CASE_BODY(timeout) +{ + expect_timeout("This times out"); + ::sleep(10); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, die); + ATF_ADD_TEST_CASE(tcs, exit); + ATF_ADD_TEST_CASE(tcs, failure); + ATF_ADD_TEST_CASE(tcs, signal); + ATF_ADD_TEST_CASE(tcs, timeout); +} diff --git a/integration/helpers/expect_some_fail.cpp b/integration/helpers/expect_some_fail.cpp new file mode 100644 index 000000000000..da1a9f0ceb39 --- /dev/null +++ b/integration/helpers/expect_some_fail.cpp @@ -0,0 +1,94 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +extern "C" { +#include +#include +} + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(die); +ATF_TEST_CASE_BODY(die) +{ + expect_death("Won't die"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exit); +ATF_TEST_CASE_BODY(exit) +{ + expect_exit(12, "Invalid exit code"); + std::exit(34); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(failure); +ATF_TEST_CASE_BODY(failure) +{ + expect_fail("Does not fail"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pass); +ATF_TEST_CASE_BODY(pass) +{ +} + + +ATF_TEST_CASE_WITHOUT_HEAD(signal); +ATF_TEST_CASE_BODY(signal) +{ + expect_signal(SIGTERM, "Invalid signal"); + ::kill(::getpid(), SIGKILL); +} + + +ATF_TEST_CASE(timeout); +ATF_TEST_CASE_HEAD(timeout) +{ + set_md_var("timeout", "1"); +} +ATF_TEST_CASE_BODY(timeout) +{ + expect_timeout("Does not time out"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, die); + ATF_ADD_TEST_CASE(tcs, exit); + ATF_ADD_TEST_CASE(tcs, failure); + ATF_ADD_TEST_CASE(tcs, pass); + ATF_ADD_TEST_CASE(tcs, signal); + ATF_ADD_TEST_CASE(tcs, timeout); +} diff --git a/integration/helpers/interrupts.cpp b/integration/helpers/interrupts.cpp new file mode 100644 index 000000000000..b6c5a948098c --- /dev/null +++ b/integration/helpers/interrupts.cpp @@ -0,0 +1,62 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +extern "C" { +#include +} + +#include + +#include + + +ATF_TEST_CASE_WITH_CLEANUP(block_body); +ATF_TEST_CASE_HEAD(block_body) +{ + set_md_var("require.config", "body-cookie cleanup-cookie"); +} +ATF_TEST_CASE_BODY(block_body) +{ + const std::string cookie(get_config_var("body-cookie")); + std::ofstream output(cookie.c_str()); + output.close(); + for (;;) + ::pause(); +} +ATF_TEST_CASE_CLEANUP(block_body) +{ + const std::string cookie(get_config_var("cleanup-cookie")); + std::ofstream output(cookie.c_str()); + output.close(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, block_body); +} diff --git a/integration/helpers/metadata.cpp b/integration/helpers/metadata.cpp new file mode 100644 index 000000000000..8005d7d9b68d --- /dev/null +++ b/integration/helpers/metadata.cpp @@ -0,0 +1,95 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include + +#include + +#include "utils/test_utils.ipp" + + +ATF_TEST_CASE_WITHOUT_HEAD(no_properties); +ATF_TEST_CASE_BODY(no_properties) +{ +} + + +ATF_TEST_CASE(one_property); +ATF_TEST_CASE_HEAD(one_property) +{ + set_md_var("descr", "Does nothing but has one metadata property"); +} +ATF_TEST_CASE_BODY(one_property) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE(many_properties); +ATF_TEST_CASE_HEAD(many_properties) +{ + set_md_var("descr", " A description with some padding"); + set_md_var("require.arch", "some-architecture"); + set_md_var("require.config", "var1 var2 var3"); + set_md_var("require.files", "/my/file1 /some/other/file"); + set_md_var("require.machine", "some-platform"); + set_md_var("require.progs", "bin1 bin2 /nonexistent/bin3"); + set_md_var("require.user", "root"); + set_md_var("X-no-meaning", "I am a custom variable"); +} +ATF_TEST_CASE_BODY(many_properties) +{ + utils::abort_without_coredump(); +} + + +ATF_TEST_CASE_WITH_CLEANUP(with_cleanup); +ATF_TEST_CASE_HEAD(with_cleanup) +{ + set_md_var("timeout", "250"); +} +ATF_TEST_CASE_BODY(with_cleanup) +{ + std::cout << "Body message to stdout\n"; + std::cerr << "Body message to stderr\n"; +} +ATF_TEST_CASE_CLEANUP(with_cleanup) +{ + std::cout << "Cleanup message to stdout\n"; + std::cerr << "Cleanup message to stderr\n"; +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, no_properties); + ATF_ADD_TEST_CASE(tcs, one_property); + ATF_ADD_TEST_CASE(tcs, many_properties); + ATF_ADD_TEST_CASE(tcs, with_cleanup); +} diff --git a/integration/helpers/race.cpp b/integration/helpers/race.cpp new file mode 100644 index 000000000000..39d4b04f3923 --- /dev/null +++ b/integration/helpers/race.cpp @@ -0,0 +1,99 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file integration/helpers/race.cpp +/// Creates a file and reads it back, looking for races. +/// +/// This program should fail with high chances if it is called multiple times at +/// once with TEST_ENV_shared_file pointing to the same file. + +extern "C" { +#include + +#include +} + +#include +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/env.hpp" +#include "utils/optional.ipp" +#include "utils/stream.hpp" + +namespace fs = utils::fs; + +using utils::optional; + + +/// Entry point to the helper test program. +/// +/// \return EXIT_SUCCESS if no race is detected; EXIT_FAILURE otherwise. +int +main(void) +{ + const optional< std::string > shared_file = utils::getenv( + "TEST_ENV_shared_file"); + if (!shared_file) { + std::cerr << "Environment variable TEST_ENV_shared_file not defined\n"; + std::exit(EXIT_FAILURE); + } + const fs::path shared_path(shared_file.get()); + + if (fs::exists(shared_path)) { + std::cerr << "Shared file already exists; created by a concurrent " + "test?"; + std::exit(EXIT_FAILURE); + } + + const std::string contents = F("%s") % ::getpid(); + + std::ofstream output(shared_path.c_str()); + if (!output) { + std::cerr << "Failed to create shared file; conflict with a concurrent " + "test?"; + std::exit(EXIT_FAILURE); + } + output << contents; + output.close(); + + ::usleep(10000); + + const std::string read_contents = utils::read_file(shared_path); + if (read_contents != contents) { + std::cerr << "Shared file contains unexpected contents; modified by a " + "concurrent test?"; + std::exit(EXIT_FAILURE); + } + + fs::unlink(shared_path); + std::exit(EXIT_SUCCESS); +} diff --git a/integration/helpers/simple_all_pass.cpp b/integration/helpers/simple_all_pass.cpp new file mode 100644 index 000000000000..4e168b4cca5f --- /dev/null +++ b/integration/helpers/simple_all_pass.cpp @@ -0,0 +1,55 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(pass); +ATF_TEST_CASE_BODY(pass) +{ + std::cout << "This is the stdout of pass\n"; + std::cerr << "This is the stderr of pass\n"; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(skip); +ATF_TEST_CASE_BODY(skip) +{ + std::cout << "This is the stdout of skip\n"; + std::cerr << "This is the stderr of skip\n"; + skip("The reason for skipping is this"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, pass); + ATF_ADD_TEST_CASE(tcs, skip); +} diff --git a/integration/helpers/simple_some_fail.cpp b/integration/helpers/simple_some_fail.cpp new file mode 100644 index 000000000000..909ffb6e2ee1 --- /dev/null +++ b/integration/helpers/simple_some_fail.cpp @@ -0,0 +1,53 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(fail); +ATF_TEST_CASE_BODY(fail) +{ + std::cout << "This is the stdout of fail\n"; + std::cerr << "This is the stderr of fail\n"; + fail("This fails on purpose"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pass); +ATF_TEST_CASE_BODY(pass) +{ +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, fail); + ATF_ADD_TEST_CASE(tcs, pass); +} diff --git a/integration/utils.sh b/integration/utils.sh new file mode 100755 index 000000000000..99565a1c9857 --- /dev/null +++ b/integration/utils.sh @@ -0,0 +1,177 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# Subcommand to strip out the durations and timestamps in a report. +# +# This is to make the reports deterministic and thus easily testable. The +# time deltas are replaced by the fixed string S.UUU and the timestamps are +# replaced by the fixed strings YYYYMMDD.HHMMSS.ssssss and +# YYYY-MM-DDTHH:MM:SS.ssssssZ depending on their original format. +# +# This variable should be used as shown here: +# +# atf_check ... -x kyua report "| ${utils_strip_times}" +# +# Use the utils_install_times_wrapper function to create a 'kyua' wrapper +# script that automatically does this. +# CHECK_STYLE_DISABLE +utils_strip_times='sed -E \ + -e "s,( |\[|\")[0-9][0-9]*.[0-9][0-9][0-9](s]|s|\"),\1S.UUU\2,g" \ + -e "s,[0-9]{8}-[0-9]{6}-[0-9]{6},YYYYMMDD-HHMMSS-ssssss,g" \ + -e "s,[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}Z,YYYY-MM-DDTHH:MM:SS.ssssssZ,g"' +# CHECK_STYLE_ENABLE + + +# Same as utils_strip_times but avoids stripping timestamp-based report IDs. +# +# This is to make the reports deterministic and thus easily testable. The +# time deltas are replaced by the fixed string S.UUU and the timestamps are +# replaced by the fixed string YYYY-MM-DDTHH:MM:SS.ssssssZ. +# CHECK_STYLE_DISABLE +utils_strip_times_but_not_ids='sed -E \ + -e "s,( |\[|\")[0-9][0-9]*.[0-9][0-9][0-9](s]|s|\"),\1S.UUU\2,g" \ + -e "s,[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}Z,YYYY-MM-DDTHH:MM:SS.ssssssZ,g"' +# CHECK_STYLE_ENABLE + + +# Computes the results id for a test suite run. +# +# The computed path is "generic" in the sense that it does not include a +# real timestamp: it only includes a placeholder. This function should be +# used along the utils_strip_times function so that the timestamps of +# the real results files are stripped out. +# +# \param path Optional path to use; if not given, use the cwd. +utils_results_id() { + local test_suite_id="$(utils_test_suite_id "${@}")" + echo "${test_suite_id}.YYYYMMDD-HHMMSS-ssssss" +} + + +# Computes the results file for a test suite run. +# +# The computed path is "generic" in the sense that it does not include a +# real timestamp: it only includes a placeholder. This function should be +# used along the utils_strip_times function so that the timestampts of the +# real results files are stripped out. +# +# \param path Optional path to use; if not given, use the cwd. +utils_results_file() { + echo "${HOME}/.kyua/store/results.$(utils_results_id "${@}").db" +} + + +# Copies a helper binary from the source directory to the work directory. +# +# \param name The name of the binary to copy. +# \param destination The target location for the binary; can be either +# a directory name or a file name. +utils_cp_helper() { + local name="${1}"; shift + local destination="${1}"; shift + + ln -s "$(atf_get_srcdir)"/helpers/"${name}" "${destination}" +} + + +# Creates a 'kyua' binary in the path that strips timing data off the output. +# +# Call this on test cases that wish to replace timing data in the *stdout* of +# Kyua with the deterministic strings. This is to be used by tests that +# validate the 'test' and 'report' subcommands. +utils_install_times_wrapper() { + [ ! -x kyua ] || return + cat >kyua <kyua.tmpout +result=\${?} +cat kyua.tmpout | ${utils_strip_times} +exit \${result} +EOF + chmod +x kyua + PATH="$(pwd):${PATH}" +} + + +# Creates a 'kyua' binary in the path that makes the output of 'test' stable. +# +# Call this on test cases that wish to replace timing data with deterministic +# strings and that need the result lines in the output to be sorted +# lexicographically. The latter hides the indeterminism caused by parallel +# execution so that the output can be verified. For these reasons, this is to +# be used exclusively by tests that validate the 'test' subcommand. +utils_install_stable_test_wrapper() { + [ ! -x kyua ] || return + cat >kyua <kyua.tmpout +result=\${?} +cat kyua.tmpout | ${utils_strip_times} >kyua.tmpout2 + +# Sort the test result lines but keep the rest intact. +grep '[^ ]*:[^ ]*' kyua.tmpout2 | sort >kyua.tmpout3 +grep -v '[^ ]*:[^ ]*' kyua.tmpout2 >kyua.tmpout4 +cat kyua.tmpout3 kyua.tmpout4 + +exit \${result} +EOF + chmod +x kyua + PATH="$(pwd):${PATH}" +} + + +# Defines a test case with a default head. +utils_test_case() { + local name="${1}"; shift + + atf_test_case "${name}" + eval "${name}_head() { + atf_set require.progs kyua + }" +} + + +# Computes the test suite identifier for results files files. +# +# \param path Optional path to use; if not given, use the cwd. +utils_test_suite_id() { + local path= + if [ ${#} -gt 0 ]; then + path="$(cd ${1} && pwd)"; shift + else + path="$(pwd)" + fi + echo "${path}" | sed -e 's,^/,,' -e 's,/,_,g' +} diff --git a/m4/ax_cxx_compile_stdcxx.m4 b/m4/ax_cxx_compile_stdcxx.m4 new file mode 100644 index 000000000000..43087b2e6889 --- /dev/null +++ b/m4/ax_cxx_compile_stdcxx.m4 @@ -0,0 +1,951 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CXX_COMPILE_STDCXX(VERSION, [ext|noext], [mandatory|optional]) +# +# DESCRIPTION +# +# Check for baseline language coverage in the compiler for the specified +# version of the C++ standard. If necessary, add switches to CXX and +# CXXCPP to enable support. VERSION may be '11' (for the C++11 standard) +# or '14' (for the C++14 standard). +# +# The second argument, if specified, indicates whether you insist on an +# extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g. +# -std=c++11). If neither is specified, you get whatever works, with +# preference for an extended mode. +# +# The third argument, if specified 'mandatory' or if left unspecified, +# indicates that baseline support for the specified C++ standard is +# required and that the macro should error out if no mode with that +# support is found. If specified 'optional', then configuration proceeds +# regardless, after defining HAVE_CXX${VERSION} if and only if a +# supporting mode is found. +# +# LICENSE +# +# Copyright (c) 2008 Benjamin Kosnik +# Copyright (c) 2012 Zack Weinberg +# Copyright (c) 2013 Roy Stogner +# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov +# Copyright (c) 2015 Paul Norman +# Copyright (c) 2015 Moritz Klammler +# Copyright (c) 2016, 2018 Krzesimir Nowak +# Copyright (c) 2019 Enji Cooper +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 11 + +dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro +dnl (serial version number 13). + +AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl + m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"], + [$1], [14], [ax_cxx_compile_alternatives="14 1y"], + [$1], [17], [ax_cxx_compile_alternatives="17 1z"], + [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl + m4_if([$2], [], [], + [$2], [ext], [], + [$2], [noext], [], + [m4_fatal([invalid second argument `$2' to AX_CXX_COMPILE_STDCXX])])dnl + m4_if([$3], [], [ax_cxx_compile_cxx$1_required=true], + [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true], + [$3], [optional], [ax_cxx_compile_cxx$1_required=false], + [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])]) + AC_LANG_PUSH([C++])dnl + ac_success=no + + m4_if([$2], [noext], [], [dnl + if test x$ac_success = xno; then + for alternative in ${ax_cxx_compile_alternatives}; do + switch="-std=gnu++${alternative}" + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, + $cachevar, + [ac_save_CXX="$CXX" + CXX="$CXX $switch" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [eval $cachevar=yes], + [eval $cachevar=no]) + CXX="$ac_save_CXX"]) + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + fi]) + + m4_if([$2], [ext], [], [dnl + if test x$ac_success = xno; then + dnl HP's aCC needs +std=c++11 according to: + dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf + dnl Cray's crayCC needs "-h std=c++11" + for alternative in ${ax_cxx_compile_alternatives}; do + for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, + $cachevar, + [ac_save_CXX="$CXX" + CXX="$CXX $switch" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [eval $cachevar=yes], + [eval $cachevar=no]) + CXX="$ac_save_CXX"]) + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + if test x$ac_success = xyes; then + break + fi + done + fi]) + AC_LANG_POP([C++]) + if test x$ax_cxx_compile_cxx$1_required = xtrue; then + if test x$ac_success = xno; then + AC_MSG_ERROR([*** A compiler with support for C++$1 language features is required.]) + fi + fi + if test x$ac_success = xno; then + HAVE_CXX$1=0 + AC_MSG_NOTICE([No compiler with C++$1 support was found]) + else + HAVE_CXX$1=1 + AC_DEFINE(HAVE_CXX$1,1, + [define if the compiler supports basic C++$1 syntax]) + fi + AC_SUBST(HAVE_CXX$1) +]) + + +dnl Test body for checking C++11 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 +) + + +dnl Test body for checking C++14 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 +) + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17 +) + +dnl Tests for new features in C++11 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[ + +// If the compiler admits that it is not ready for C++11, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201103L + +#error "This is not a C++11 compiler" + +#else + +namespace cxx11 +{ + + namespace test_static_assert + { + + template + struct check + { + static_assert(sizeof(int) <= sizeof(T), "not big enough"); + }; + + } + + namespace test_final_override + { + + struct Base + { + virtual ~Base() {} + virtual void f() {} + }; + + struct Derived : public Base + { + virtual ~Derived() override {} + virtual void f() override {} + }; + + } + + namespace test_double_right_angle_brackets + { + + template < typename T > + struct check {}; + + typedef check single_type; + typedef check> double_type; + typedef check>> triple_type; + typedef check>>> quadruple_type; + + } + + namespace test_decltype + { + + int + f() + { + int a = 1; + decltype(a) b = 2; + return a + b; + } + + } + + namespace test_type_deduction + { + + template < typename T1, typename T2 > + struct is_same + { + static const bool value = false; + }; + + template < typename T > + struct is_same + { + static const bool value = true; + }; + + template < typename T1, typename T2 > + auto + add(T1 a1, T2 a2) -> decltype(a1 + a2) + { + return a1 + a2; + } + + int + test(const int c, volatile int v) + { + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == false, ""); + auto ac = c; + auto av = v; + auto sumi = ac + av + 'x'; + auto sumf = ac + av + 1.0; + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == true, ""); + return (sumf > 0.0) ? sumi : add(c, v); + } + + } + + namespace test_noexcept + { + + int f() { return 0; } + int g() noexcept { return 0; } + + static_assert(noexcept(f()) == false, ""); + static_assert(noexcept(g()) == true, ""); + + } + + namespace test_constexpr + { + + template < typename CharT > + unsigned long constexpr + strlen_c_r(const CharT *const s, const unsigned long acc) noexcept + { + return *s ? strlen_c_r(s + 1, acc + 1) : acc; + } + + template < typename CharT > + unsigned long constexpr + strlen_c(const CharT *const s) noexcept + { + return strlen_c_r(s, 0UL); + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("1") == 1UL, ""); + static_assert(strlen_c("example") == 7UL, ""); + static_assert(strlen_c("another\0example") == 7UL, ""); + + } + + namespace test_rvalue_references + { + + template < int N > + struct answer + { + static constexpr int value = N; + }; + + answer<1> f(int&) { return answer<1>(); } + answer<2> f(const int&) { return answer<2>(); } + answer<3> f(int&&) { return answer<3>(); } + + void + test() + { + int i = 0; + const int c = 0; + static_assert(decltype(f(i))::value == 1, ""); + static_assert(decltype(f(c))::value == 2, ""); + static_assert(decltype(f(0))::value == 3, ""); + } + + } + + namespace test_uniform_initialization + { + + struct test + { + static const int zero {}; + static const int one {1}; + }; + + static_assert(test::zero == 0, ""); + static_assert(test::one == 1, ""); + + } + + namespace test_lambdas + { + + void + test1() + { + auto lambda1 = [](){}; + auto lambda2 = lambda1; + lambda1(); + lambda2(); + } + + int + test2() + { + auto a = [](int i, int j){ return i + j; }(1, 2); + auto b = []() -> int { return '0'; }(); + auto c = [=](){ return a + b; }(); + auto d = [&](){ return c; }(); + auto e = [a, &b](int x) mutable { + const auto identity = [](int y){ return y; }; + for (auto i = 0; i < a; ++i) + a += b--; + return x + identity(a + b); + }(0); + return a + b + c + d + e; + } + + int + test3() + { + const auto nullary = [](){ return 0; }; + const auto unary = [](int x){ return x; }; + using nullary_t = decltype(nullary); + using unary_t = decltype(unary); + const auto higher1st = [](nullary_t f){ return f(); }; + const auto higher2nd = [unary](nullary_t f1){ + return [unary, f1](unary_t f2){ return f2(unary(f1())); }; + }; + return higher1st(nullary) + higher2nd(nullary)(unary); + } + + } + + namespace test_variadic_templates + { + + template + struct sum; + + template + struct sum + { + static constexpr auto value = N0 + sum::value; + }; + + template <> + struct sum<> + { + static constexpr auto value = 0; + }; + + static_assert(sum<>::value == 0, ""); + static_assert(sum<1>::value == 1, ""); + static_assert(sum<23>::value == 23, ""); + static_assert(sum<1, 2>::value == 3, ""); + static_assert(sum<5, 5, 11>::value == 21, ""); + static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, ""); + + } + + // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae + // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function + // because of this. + namespace test_template_alias_sfinae + { + + struct foo {}; + + template + using member = typename T::member_type; + + template + void func(...) {} + + template + void func(member*) {} + + void test(); + + void test() { func(0); } + + } + +} // namespace cxx11 + +#endif // __cplusplus >= 201103L + +]]) + + +dnl Tests for new features in C++14 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[ + +// If the compiler admits that it is not ready for C++14, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201402L + +#error "This is not a C++14 compiler" + +#else + +namespace cxx14 +{ + + namespace test_polymorphic_lambdas + { + + int + test() + { + const auto lambda = [](auto&&... args){ + const auto istiny = [](auto x){ + return (sizeof(x) == 1UL) ? 1 : 0; + }; + const int aretiny[] = { istiny(args)... }; + return aretiny[0]; + }; + return lambda(1, 1L, 1.0f, '1'); + } + + } + + namespace test_binary_literals + { + + constexpr auto ivii = 0b0000000000101010; + static_assert(ivii == 42, "wrong value"); + + } + + namespace test_generalized_constexpr + { + + template < typename CharT > + constexpr unsigned long + strlen_c(const CharT *const s) noexcept + { + auto length = 0UL; + for (auto p = s; *p; ++p) + ++length; + return length; + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("x") == 1UL, ""); + static_assert(strlen_c("test") == 4UL, ""); + static_assert(strlen_c("another\0test") == 7UL, ""); + + } + + namespace test_lambda_init_capture + { + + int + test() + { + auto x = 0; + const auto lambda1 = [a = x](int b){ return a + b; }; + const auto lambda2 = [a = lambda1(x)](){ return a; }; + return lambda2(); + } + + } + + namespace test_digit_separators + { + + constexpr auto ten_million = 100'000'000; + static_assert(ten_million == 100000000, ""); + + } + + namespace test_return_type_deduction + { + + auto f(int& x) { return x; } + decltype(auto) g(int& x) { return x; } + + template < typename T1, typename T2 > + struct is_same + { + static constexpr auto value = false; + }; + + template < typename T > + struct is_same + { + static constexpr auto value = true; + }; + + int + test() + { + auto x = 0; + static_assert(is_same::value, ""); + static_assert(is_same::value, ""); + return x; + } + + } + +} // namespace cxx14 + +#endif // __cplusplus >= 201402L + +]]) + + +dnl Tests for new features in C++17 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[ + +// If the compiler admits that it is not ready for C++17, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201703L + +#error "This is not a C++17 compiler" + +#else + +#include +#include +#include + +namespace cxx17 +{ + + namespace test_constexpr_lambdas + { + + constexpr int foo = [](){return 42;}(); + + } + + namespace test::nested_namespace::definitions + { + + } + + namespace test_fold_expression + { + + template + int multiply(Args... args) + { + return (args * ... * 1); + } + + template + bool all(Args... args) + { + return (args && ...); + } + + } + + namespace test_extended_static_assert + { + + static_assert (true); + + } + + namespace test_auto_brace_init_list + { + + auto foo = {5}; + auto bar {5}; + + static_assert(std::is_same, decltype(foo)>::value); + static_assert(std::is_same::value); + } + + namespace test_typename_in_template_template_parameter + { + + template typename X> struct D; + + } + + namespace test_fallthrough_nodiscard_maybe_unused_attributes + { + + int f1() + { + return 42; + } + + [[nodiscard]] int f2() + { + [[maybe_unused]] auto unused = f1(); + + switch (f1()) + { + case 17: + f1(); + [[fallthrough]]; + case 42: + f1(); + } + return f1(); + } + + } + + namespace test_extended_aggregate_initialization + { + + struct base1 + { + int b1, b2 = 42; + }; + + struct base2 + { + base2() { + b3 = 42; + } + int b3; + }; + + struct derived : base1, base2 + { + int d; + }; + + derived d1 {{1, 2}, {}, 4}; // full initialization + derived d2 {{}, {}, 4}; // value-initialized bases + + } + + namespace test_general_range_based_for_loop + { + + struct iter + { + int i; + + int& operator* () + { + return i; + } + + const int& operator* () const + { + return i; + } + + iter& operator++() + { + ++i; + return *this; + } + }; + + struct sentinel + { + int i; + }; + + bool operator== (const iter& i, const sentinel& s) + { + return i.i == s.i; + } + + bool operator!= (const iter& i, const sentinel& s) + { + return !(i == s); + } + + struct range + { + iter begin() const + { + return {0}; + } + + sentinel end() const + { + return {5}; + } + }; + + void f() + { + range r {}; + + for (auto i : r) + { + [[maybe_unused]] auto v = i; + } + } + + } + + namespace test_lambda_capture_asterisk_this_by_value + { + + struct t + { + int i; + int foo() + { + return [*this]() + { + return i; + }(); + } + }; + + } + + namespace test_enum_class_construction + { + + enum class byte : unsigned char + {}; + + byte foo {42}; + + } + + namespace test_constexpr_if + { + + template + int f () + { + if constexpr(cond) + { + return 13; + } + else + { + return 42; + } + } + + } + + namespace test_selection_statement_with_initializer + { + + int f() + { + return 13; + } + + int f2() + { + if (auto i = f(); i > 0) + { + return 3; + } + + switch (auto i = f(); i + 4) + { + case 17: + return 2; + + default: + return 1; + } + } + + } + + namespace test_template_argument_deduction_for_class_templates + { + + template + struct pair + { + pair (T1 p1, T2 p2) + : m1 {p1}, + m2 {p2} + {} + + T1 m1; + T2 m2; + }; + + void f() + { + [[maybe_unused]] auto p = pair{13, 42u}; + } + + } + + namespace test_non_type_auto_template_parameters + { + + template + struct B + {}; + + B<5> b1; + B<'a'> b2; + + } + + namespace test_structured_bindings + { + + int arr[2] = { 1, 2 }; + std::pair pr = { 1, 2 }; + + auto f1() -> int(&)[2] + { + return arr; + } + + auto f2() -> std::pair& + { + return pr; + } + + struct S + { + int x1 : 2; + volatile double y1; + }; + + S f3() + { + return {}; + } + + auto [ x1, y1 ] = f1(); + auto& [ xr1, yr1 ] = f1(); + auto [ x2, y2 ] = f2(); + auto& [ xr2, yr2 ] = f2(); + const auto [ x3, y3 ] = f3(); + + } + + namespace test_exception_spec_type_system + { + + struct Good {}; + struct Bad {}; + + void g1() noexcept; + void g2(); + + template + Bad + f(T*, T*); + + template + Good + f(T1*, T2*); + + static_assert (std::is_same_v); + + } + + namespace test_inline_variables + { + + template void f(T) + {} + + template inline T g(T) + { + return T{}; + } + + template<> inline void f<>(int) + {} + + template<> int g<>(int) + { + return 5; + } + + } + +} // namespace cxx17 + +#endif // __cplusplus < 201703L + +]]) diff --git a/m4/compiler-features.m4 b/m4/compiler-features.m4 new file mode 100644 index 000000000000..840f292383d5 --- /dev/null +++ b/m4/compiler-features.m4 @@ -0,0 +1,122 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl +dnl KYUA_ATTRIBUTE_NORETURN +dnl +dnl Checks if the current compiler has a way to mark functions that do not +dnl return and defines ATTRIBUTE_NORETURN to the appropriate string. +dnl +AC_DEFUN([KYUA_ATTRIBUTE_NORETURN], [ + dnl This check is overly simple and should be fixed. For example, + dnl Sun's cc does support the noreturn attribute but CC (the C++ + dnl compiler) does not. And in that case, CC just raises a warning + dnl during compilation, not an error. + AC_CACHE_CHECK( + [whether __attribute__((noreturn)) is supported], + [kyua_cv_attribute_noreturn], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([], [ +#if ((__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || __GNUC__ > 2) + return 0; +#else + return 1; +#endif + ])], + [kyua_cv_attribute_noreturn=yes], + [kyua_cv_attribute_noreturn=no]) + ]) + if test "${kyua_cv_attribute_noreturn}" = yes; then + attribute_value="__attribute__((noreturn))" + else + attribute_value="" + fi + AC_SUBST([ATTRIBUTE_NORETURN], [${attribute_value}]) +]) + + +dnl +dnl KYUA_ATTRIBUTE_PURE +dnl +dnl Checks if the current compiler has a way to mark functions as pure. +dnl +AC_DEFUN([KYUA_ATTRIBUTE_PURE], [ + AC_CACHE_CHECK( + [whether __attribute__((__pure__)) is supported], + [kyua_cv_attribute_pure], [ + AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([ +static int function(int, int) __attribute__((__pure__)); + +static int +function(int a, int b) +{ + return a + b; +}], [ + return function(3, 4); +])], + [kyua_cv_attribute_pure=yes], + [kyua_cv_attribute_pure=no]) + ]) + if test "${kyua_cv_attribute_pure}" = yes; then + attribute_value="__attribute__((__pure__))" + else + attribute_value="" + fi + AC_SUBST([ATTRIBUTE_PURE], [${attribute_value}]) +]) + + +dnl +dnl KYUA_ATTRIBUTE_UNUSED +dnl +dnl Checks if the current compiler has a way to mark parameters as unused +dnl so that the -Wunused-parameter warning can be avoided. +dnl +AC_DEFUN([KYUA_ATTRIBUTE_UNUSED], [ + AC_CACHE_CHECK( + [whether __attribute__((__unused__)) is supported], + [kyua_cv_attribute_unused], [ + AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([ +static void +function(int a __attribute__((__unused__))) +{ +}], [ + function(3); + return 0; +])], + [kyua_cv_attribute_unused=yes], + [kyua_cv_attribute_unused=no]) + ]) + if test "${kyua_cv_attribute_unused}" = yes; then + attribute_value="__attribute__((__unused__))" + else + attribute_value="" + fi + AC_SUBST([ATTRIBUTE_UNUSED], [${attribute_value}]) +]) diff --git a/m4/compiler-flags.m4 b/m4/compiler-flags.m4 new file mode 100644 index 000000000000..f8dd555118d4 --- /dev/null +++ b/m4/compiler-flags.m4 @@ -0,0 +1,169 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl \file compiler-flags.m4 +dnl +dnl Macros to check for the existence of compiler flags. The macros in this +dnl file support both C and C++. +dnl +dnl Be aware that, in order to detect a flag accurately, we may need to enable +dnl strict warning checking in the compiler (i.e. enable -Werror). Some +dnl compilers, e.g. Clang, report unknown -W flags as warnings unless -Werror is +dnl selected. This fact would confuse the flag checks below because we would +dnl conclude that a flag is valid while in reality it is not. To resolve this, +dnl the macros below will pass -Werror to the compiler along with any other flag +dnl being checked. + + +dnl Checks for a compiler flag and sets a result variable. +dnl +dnl This is an auxiliary macro for the implementation of _KYUA_FLAG. +dnl +dnl \param 1 The shell variable containing the compiler name. Used for +dnl reporting purposes only. C or CXX. +dnl \param 2 The shell variable containing the flags for the compiler. +dnl CFLAGS or CXXFLAGS. +dnl \param 3 The name of the compiler flag to check for. +dnl \param 4 The shell variable to set with the result of the test. Will +dnl be set to 'yes' if the flag is valid, 'no' otherwise. +dnl \param 5 Additional, optional flags to pass to the C compiler while +dnl looking for the flag in $3. We use this here to pass -Werror to the +dnl flag checks (unless we are checking for -Werror already). +AC_DEFUN([_KYUA_FLAG_AUX], [ + if test x"${$4-unset}" = xunset; then + AC_MSG_CHECKING(whether ${$1} supports $3) + saved_flags="${$2}" + $4=no + $2="${$2} $5 $3" + # The inclusion of a header file in the test program below is needed + # because some compiler flags that we test for may actually not be + # compatible with other flags, and such compatibility checks are + # performed within the system header files. + # + # As an example, if we are testing for -D_FORTIFY_SOURCE=2 and the + # compilation is being done with -O2, Linux's /usr/include/features.h + # will abort the compilation of our code later on. By including a + # generic header file here that pulls in features.h we ensure that + # this test is accurate for the build stage. + AC_LINK_IFELSE([AC_LANG_PROGRAM([#include ], [return 0;])], + AC_MSG_RESULT(yes) + $4=yes, + AC_MSG_RESULT(no)) + $2="${saved_flags}" + fi +]) + + +dnl Checks for a compiler flag and appends it to a result variable. +dnl +dnl \param 1 The shell variable containing the compiler name. Used for +dnl reporting purposes only. CC or CXX. +dnl \param 2 The shell variable containing the flags for the compiler. +dnl CFLAGS or CXXFLAGS. +dnl \param 3 The name of the compiler flag to check for. +dnl \param 4 The shell variable to which to append $3 if the flag is valid. +AC_DEFUN([_KYUA_FLAG], [ + _KYUA_FLAG_AUX([$1], [$2], [-Werror], [kyua_$1_has_werror]) + if test "$3" = "-Werror"; then + found=${kyua_$1_has_werror} + else + found=unset + if test ${kyua_$1_has_werror} = yes; then + _KYUA_FLAG_AUX([$1], [$2], [$3], [found], [-Werror]) + else + _KYUA_FLAG_AUX([$1], [$2], [$3], [found], []) + fi + fi + if test ${found} = yes; then + $4="${$4} $3" + fi +]) + + +dnl Checks for a C compiler flag and appends it to a variable. +dnl +dnl \pre The current language is C. +dnl +dnl \param 1 The name of the compiler flag to check for. +dnl \param 2 The shell variable to which to append $1 if the flag is valid. +AC_DEFUN([KYUA_CC_FLAG], [ + AC_LANG_ASSERT([C]) + _KYUA_FLAG([CC], [CFLAGS], [$1], [$2]) +]) + + +dnl Checks for a C++ compiler flag and appends it to a variable. +dnl +dnl \pre The current language is C++. +dnl +dnl \param 1 The name of the compiler flag to check for. +dnl \param 2 The shell variable to which to append $1 if the flag is valid. +AC_DEFUN([KYUA_CXX_FLAG], [ + AC_LANG_ASSERT([C++]) + _KYUA_FLAG([CXX], [CXXFLAGS], [$1], [$2]) +]) + + +dnl Checks for a set of C compiler flags and appends them to CFLAGS. +dnl +dnl The checks are performed independently and only when all the checks are +dnl done, the output variable is modified. +dnl +dnl \param 1 Whitespace-separated list of C flags to check. +AC_DEFUN([KYUA_CC_FLAGS], [ + AC_LANG_PUSH([C]) + valid_cflags= + for f in $1; do + KYUA_CC_FLAG(${f}, valid_cflags) + done + if test -n "${valid_cflags}"; then + CFLAGS="${CFLAGS} ${valid_cflags}" + fi + AC_LANG_POP([C]) +]) + + +dnl Checks for a set of C++ compiler flags and appends them to CXXFLAGS. +dnl +dnl The checks are performed independently and only when all the checks are +dnl done, the output variable is modified. +dnl +dnl \pre The current language is C++. +dnl +dnl \param 1 Whitespace-separated list of C flags to check. +AC_DEFUN([KYUA_CXX_FLAGS], [ + AC_LANG_PUSH([C++]) + valid_cxxflags= + for f in $1; do + KYUA_CXX_FLAG(${f}, valid_cxxflags) + done + if test -n "${valid_cxxflags}"; then + CXXFLAGS="${CXXFLAGS} ${valid_cxxflags}" + fi + AC_LANG_POP([C++]) +]) diff --git a/m4/developer-mode.m4 b/m4/developer-mode.m4 new file mode 100644 index 000000000000..ad946056f63c --- /dev/null +++ b/m4/developer-mode.m4 @@ -0,0 +1,112 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl \file developer-mode.m4 +dnl +dnl "Developer mode" is a mode in which the build system reports any +dnl build-time warnings as fatal errors. This helps in minimizing the +dnl amount of trivial coding problems introduced in the code. +dnl Unfortunately, this is not bullet-proof due to the wide variety of +dnl compilers available and their different warning diagnostics. +dnl +dnl When developer mode support is added to a package, the compilation will +dnl gain a bunch of extra warning diagnostics. These will NOT be enforced +dnl unless developer mode is enabled. +dnl +dnl Developer mode is enabled when the user requests it through the +dnl configure command line, or when building from the repository. The +dnl latter is to minimize the risk of committing new code with warnings +dnl into the tree. + + +dnl Adds "developer mode" support to the package. +dnl +dnl This macro performs the actual definition of the --enable-developer +dnl flag and implements all of its logic. See the file-level comment for +dnl details as to what this implies. +AC_DEFUN([KYUA_DEVELOPER_MODE], [ + m4_foreach([language], [$1], [m4_set_add([languages], language)]) + + AC_ARG_ENABLE( + [developer], + AS_HELP_STRING([--enable-developer], [enable developer features]),, + [if test -d "${srcdir}/.git"; then + AC_MSG_NOTICE([building from HEAD; developer mode autoenabled]) + enable_developer=yes + else + enable_developer=no + fi]) + + # + # The following warning flags should also be enabled but cannot be. + # Reasons given below. + # + # -Wold-style-cast: Raises errors when using TIOCGWINSZ, at least under + # Mac OS X. This is due to the way _IOR is defined. + # + + try_c_cxx_flags="-D_FORTIFY_SOURCE=2 \ + -Wall \ + -Wcast-qual \ + -Wextra \ + -Wpointer-arith \ + -Wredundant-decls \ + -Wreturn-type \ + -Wshadow \ + -Wsign-compare \ + -Wswitch \ + -Wwrite-strings" + + try_c_flags="-Wmissing-prototypes \ + -Wno-traditional \ + -Wstrict-prototypes" + + try_cxx_flags="-Wabi \ + -Wctor-dtor-privacy \ + -Wno-deprecated \ + -Wno-non-template-friend \ + -Wno-pmf-conversions \ + -Wnon-virtual-dtor \ + -Woverloaded-virtual \ + -Wreorder \ + -Wsign-promo \ + -Wsynth" + + if test ${enable_developer} = yes; then + try_werror=yes + try_c_cxx_flags="${try_c_cxx_flags} -g -Werror" + else + try_werror=no + try_c_cxx_flags="${try_c_cxx_flags} -DNDEBUG" + fi + + m4_set_contains([languages], [C], + [KYUA_CC_FLAGS(${try_c_cxx_flags} ${try_c_flags})]) + m4_set_contains([languages], [C++], + [KYUA_CXX_FLAGS(${try_c_cxx_flags} ${try_cxx_flags})]) +]) diff --git a/m4/doxygen.m4 b/m4/doxygen.m4 new file mode 100644 index 000000000000..24fd2a408f88 --- /dev/null +++ b/m4/doxygen.m4 @@ -0,0 +1,62 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl +dnl KYUA_DOXYGEN +dnl +dnl Adds a --with-doxygen flag to the configure script and, when Doxygen support +dnl is requested by the user, sets DOXYGEN to the path of the Doxygen binary and +dnl enables the WITH_DOXYGEN Automake conditional. +dnl +AC_DEFUN([KYUA_DOXYGEN], [ + AC_ARG_WITH([doxygen], + AS_HELP_STRING([--with-doxygen], + [build documentation for internal APIs]), + [], + [with_doxygen=auto]) + + if test "${with_doxygen}" = yes; then + AC_PATH_PROG([DOXYGEN], [doxygen], []) + if test -z "${DOXYGEN}"; then + AC_MSG_ERROR([Doxygen explicitly requested but not found]) + fi + elif test "${with_doxygen}" = auto; then + AC_PATH_PROG([DOXYGEN], [doxygen], []) + elif test "${with_doxygen}" = no; then + DOXYGEN= + else + AC_MSG_CHECKING([for doxygen]) + DOXYGEN="${with_doxygen}" + AC_MSG_RESULT([${DOXYGEN}]) + if test ! -x "${DOXYGEN}"; then + AC_MSG_ERROR([Doxygen binary ${DOXYGEN} is not executable]) + fi + fi + AM_CONDITIONAL([WITH_DOXYGEN], [test -n "${DOXYGEN}"]) + AC_SUBST([DOXYGEN]) +]) diff --git a/m4/fs.m4 b/m4/fs.m4 new file mode 100644 index 000000000000..7cb103eb1370 --- /dev/null +++ b/m4/fs.m4 @@ -0,0 +1,125 @@ +dnl Copyright 2011 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl \file m4/fs.m4 +dnl File system related checks. +dnl +dnl The macros in this file check for features required in the utils/fs +dnl module. The global KYUA_FS_MODULE macro will call all checks required +dnl for the library. + + +dnl KYUA_FS_GETCWD_DYN +dnl +dnl Checks whether getcwd(NULL, 0) works; i.e. if getcwd(3) can dynamically +dnl allocate the output buffer to fit the whole current path. +AC_DEFUN([KYUA_FS_GETCWD_DYN], [ + AC_CACHE_CHECK( + [whether getcwd(NULL, 0) works], + [kyua_cv_getcwd_dyn], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([#include +#include +], [ + char *cwd = getcwd(NULL, 0); + return (cwd != NULL) ? EXIT_SUCCESS : EXIT_FAILURE; +])], + [kyua_cv_getcwd_dyn=yes], + [kyua_cv_getcwd_dyn=no]) + ]) + if test "${kyua_cv_getcwd_dyn}" = yes; then + AC_DEFINE_UNQUOTED([HAVE_GETCWD_DYN], [1], + [Define to 1 if getcwd(NULL, 0) works]) + fi +]) + + +dnl KYUA_FS_LCHMOD +dnl +dnl Checks whether lchmod(3) exists and if it works. Some systems, such as +dnl Ubuntu 10.04.1 LTS, provide a lchmod(3) stub that is not implemented yet +dnl allows programs to compile cleanly (albeit for a warning). It would be +dnl nice to detect if lchmod(3) works at run time to prevent side-effects of +dnl this test but doing so means we will keep receiving a noisy compiler +dnl warning. +AC_DEFUN([KYUA_FS_LCHMOD], [ + AC_CACHE_CHECK( + [for a working lchmod], + [kyua_cv_lchmod_works], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([#include +#include +#include +#include +#include +], [ + int fd = open("conftest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd == -1) { + perror("creation of conftest.txt failed"); + return EXIT_FAILURE; + } + + return lchmod("conftest.txt", 0640) != -1 ? EXIT_SUCCESS : EXIT_FAILURE; +])], + [kyua_cv_lchmod_works=yes], + [kyua_cv_lchmod_works=no]) + ]) + rm -f conftest.txt + if test "${kyua_cv_lchmod_works}" = yes; then + AC_DEFINE_UNQUOTED([HAVE_WORKING_LCHMOD], [1], + [Define to 1 if your lchmod works]) + fi +]) + + +dnl KYUA_FS_UNMOUNT +dnl +dnl Detect the correct method to unmount a file system. +AC_DEFUN([KYUA_FS_UNMOUNT], [ + AC_CHECK_FUNCS([unmount], [have_unmount2=yes], [have_unmount2=no]) + if test "${have_unmount2}" = no; then + have_umount8=yes + AC_PATH_PROG([UMOUNT], [umount], [have_umount8=no]) + if test "${have_umount8}" = yes; then + AC_DEFINE_UNQUOTED([UMOUNT], ["${UMOUNT}"], + [Set to the path of umount(8)]) + else + AC_MSG_ERROR([Don't know how to unmount a file system]) + fi + fi +]) + + +dnl KYUA_FS_MODULE +dnl +dnl Performs all checks needed by the utils/fs library. +AC_DEFUN([KYUA_FS_MODULE], [ + AC_CHECK_HEADERS([sys/mount.h sys/statvfs.h sys/vfs.h]) + AC_CHECK_FUNCS([statfs statvfs]) + KYUA_FS_GETCWD_DYN + KYUA_FS_LCHMOD + KYUA_FS_UNMOUNT +]) diff --git a/m4/getopt.m4 b/m4/getopt.m4 new file mode 100644 index 000000000000..f58635330704 --- /dev/null +++ b/m4/getopt.m4 @@ -0,0 +1,213 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +dnl Checks if getopt(3) supports a + sign to enforce POSIX correctness. +dnl +dnl In the GNU implementation of getopt(3), we need to pass a + sign at +dnl the beginning of the options string to request POSIX behavior. +dnl +dnl Defines HAVE_GETOPT_GNU if a + sign is supported. +AC_DEFUN([_KYUA_GETOPT_GNU], [ + AC_CACHE_CHECK( + [whether getopt allows a + sign for POSIX behavior optreset], + [kyua_cv_getopt_gnu], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([#include +#include +#include ], [ + int argc = 4; + char* argv@<:@5@:>@ = { + strdup("conftest"), + strdup("-+"), + strdup("-a"), + strdup("bar"), + NULL + }; + int ch; + int seen_a = 0, seen_plus = 0; + + while ((ch = getopt(argc, argv, "+a:")) != -1) { + switch (ch) { + case 'a': + seen_a = 1; + break; + + case '+': + seen_plus = 1; + break; + + case '?': + default: + ; + } + } + + return (seen_a && !seen_plus) ? EXIT_SUCCESS : EXIT_FAILURE; +])], + [kyua_cv_getopt_gnu=yes], + [kyua_cv_getopt_gnu=no]) + ]) + if test "${kyua_cv_getopt_gnu}" = yes; then + AC_DEFINE([HAVE_GETOPT_GNU], [1], + [Define to 1 if getopt allows a + sign for POSIX behavior]) + fi +]) + +dnl Checks if optreset exists to reset the processing of getopt(3) options. +dnl +dnl getopt(3) has an optreset global variable to reset internal state +dnl before calling getopt(3) again. However, optreset is not standard and +dnl is only present in the BSD versions of getopt(3). +dnl +dnl Defines HAVE_GETOPT_WITH_OPTRESET if optreset exists. +AC_DEFUN([_KYUA_GETOPT_WITH_OPTRESET], [ + AC_CACHE_CHECK( + [whether getopt has optreset], + [kyua_cv_getopt_optreset], [ + AC_COMPILE_IFELSE([AC_LANG_SOURCE([ +#include +#include + +int +main(void) +{ + optreset = 1; + return EXIT_SUCCESS; +} +])], + [kyua_cv_getopt_optreset=yes], + [kyua_cv_getopt_optreset=no]) + ]) + if test "${kyua_cv_getopt_optreset}" = yes; then + AC_DEFINE([HAVE_GETOPT_WITH_OPTRESET], [1], + [Define to 1 if getopt has optreset]) + fi +]) + + +dnl Checks the value to pass to optind to reset getopt(3) processing. +dnl +dnl The standard value to pass to optind to reset the processing of command +dnl lines with getopt(3) is 1. However, the GNU extensions to getopt_long(3) +dnl are not properly reset unless optind is set to 0, causing crashes later +dnl on and incorrect option processing behavior. +dnl +dnl Sets the GETOPT_OPTIND_RESET_VALUE macro to the integer value that has to +dnl be passed to optind to reset option processing. +AC_DEFUN([_KYUA_GETOPT_OPTIND_RESET_VALUE], [ + AC_CACHE_CHECK( + [for the optind value to reset getopt processing], + [kyua_cv_getopt_optind_reset_value], [ + AC_RUN_IFELSE([AC_LANG_SOURCE([ +#include +#include +#include + +static void +first_pass(void) +{ + int argc, ch, flag; + char* argv@<:@5@:>@; + + argc = 4; + argv@<:@0@:>@ = strdup("progname"); + argv@<:@1@:>@ = strdup("-a"); + argv@<:@2@:>@ = strdup("foo"); + argv@<:@3@:>@ = strdup("bar"); + argv@<:@4@:>@ = NULL; + + flag = 0; + while ((ch = getopt(argc, argv, "+:a")) != -1) { + switch (ch) { + case 'a': + flag = 1; + break; + default: + break; + } + } + if (!flag) { + exit(EXIT_FAILURE); + } +} + +static void +second_pass(void) +{ + int argc, ch, flag; + char* argv@<:@5@:>@; + + argc = 4; + argv@<:@0@:>@ = strdup("progname"); + argv@<:@1@:>@ = strdup("-b"); + argv@<:@2@:>@ = strdup("foo"); + argv@<:@3@:>@ = strdup("bar"); + argv@<:@4@:>@ = NULL; + + flag = 0; + while ((ch = getopt(argc, argv, "b")) != -1) { + switch (ch) { + case 'b': + flag = 1; + break; + default: + break; + } + } + if (!flag) { + exit(EXIT_FAILURE); + } +} + +int +main(void) +{ + /* We do two passes in two different functions to prevent the reuse of + * variables and, specially, to force the use of two different argument + * vectors. */ + first_pass(); + optind = 0; + second_pass(); + return EXIT_SUCCESS; +} +])], + [kyua_cv_getopt_optind_reset_value=0], + [kyua_cv_getopt_optind_reset_value=1]) + ]) + AC_DEFINE_UNQUOTED([GETOPT_OPTIND_RESET_VALUE], + [${kyua_cv_getopt_optind_reset_value}], + [Define to the optind value to reset getopt processing]) +]) + + +dnl Wrapper macro to detect all getopt(3) necessary features. +AC_DEFUN([KYUA_GETOPT], [ + _KYUA_GETOPT_GNU + _KYUA_GETOPT_OPTIND_RESET_VALUE + _KYUA_GETOPT_WITH_OPTRESET +]) diff --git a/m4/memory.m4 b/m4/memory.m4 new file mode 100644 index 000000000000..3d9a83a20ab5 --- /dev/null +++ b/m4/memory.m4 @@ -0,0 +1,122 @@ +dnl Copyright 2012 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl \file m4/memory.m4 +dnl +dnl Macros to configure the utils::memory module. + + +dnl Entry point to detect all features needed by utils::memory. +dnl +dnl This looks for a mechanism to check the available physical memory in the +dnl system. +AC_DEFUN([KYUA_MEMORY], [ + memory_query=unknown + memory_mib=none + + _KYUA_SYSCTLBYNAME([have_sysctlbyname=yes], [have_sysctlbyname=no]) + if test "${have_sysctlbyname}" = yes; then + _KYUA_SYSCTL_MIB([hw.usermem64], [hw_usermem64], + [memory_mib="hw.usermem64"], []) + if test "${memory_mib}" = none; then + _KYUA_SYSCTL_MIB([hw.usermem], [hw_usermem], + [memory_mib="hw.usermem"], []) + fi + if test "${memory_mib}" != none; then + memory_query=sysctlbyname + fi + fi + + if test "${memory_query}" = unknown; then + AC_MSG_WARN([Don't know how to query the amount of physical memory]) + AC_MSG_WARN([The test case's require.memory property will not work]) + fi + + AC_DEFINE_UNQUOTED([MEMORY_QUERY_TYPE], ["${memory_query}"], + [Define to the memory query type]) + AC_DEFINE_UNQUOTED([MEMORY_QUERY_SYSCTL_MIB], ["${memory_mib}"], + [Define to the name of the sysctl MIB]) +]) + + +dnl Detects the availability of the sysctlbyname(3) function. +dnl +dnl \param action_if_found Code to run if the function is found. +dnl \param action_if_not_found Code to run if the function is not found. +AC_DEFUN([_KYUA_SYSCTLBYNAME], [ + AC_CHECK_HEADERS([sys/types.h sys/sysctl.h]) dnl Darwin 11.2 + AC_CHECK_HEADERS([sys/param.h sys/sysctl.h]) dnl NetBSD 6.0 + + AC_CHECK_FUNCS([sysctlbyname], [$1], [$2]) +]) + + +dnl Looks for a specific sysctl MIB. +dnl +dnl \pre sysctlbyname(3) must be present in the system. +dnl +dnl \param mib_name The name of the MIB to check for. +dnl \param flat_mib_name The name of the MIB as a shell variable, for use in +dnl cache variable names. This should be automatically computed with +dnl m4_bpatsubst or similar, but my inability to make the code readable +dnl made me add this parameter instead. +dnl \param action_if_found Code to run if the MIB is found. +dnl \param action_if_not_found Code to run if the MIB is not found. +AC_DEFUN([_KYUA_SYSCTL_MIB], [ + AC_CACHE_CHECK( + [if the $1 sysctl MIB exists], + [kyua_cv_sysctl_$2], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([ +#if defined(HAVE_SYS_TYPES_H) +# include +#endif +#if defined(HAVE_SYS_PARAM_H) +# include +#endif +#if defined(HAVE_SYS_SYSCTL_H) +# include +#endif +#include +#include +], [ + int64_t memory; + size_t memory_length = sizeof(memory); + if (sysctlbyname("$1", &memory, &memory_length, NULL, 0) == -1) + return EXIT_FAILURE; + else + return EXIT_SUCCESS; +])], + [kyua_cv_sysctl_$2=yes], + [kyua_cv_sysctl_$2=no]) + ]) + if test "${kyua_cv_sysctl_$2}" = yes; then + m4_default([$3], [:]) + else + m4_default([$4], [:]) + fi +]) diff --git a/m4/signals.m4 b/m4/signals.m4 new file mode 100644 index 000000000000..8e8b56e1eb73 --- /dev/null +++ b/m4/signals.m4 @@ -0,0 +1,92 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl +dnl KYUA_LAST_SIGNO +dnl +dnl Detect the last valid signal number. +dnl +AC_DEFUN([KYUA_LAST_SIGNO], [ + AC_CACHE_CHECK( + [for the last valid signal], + [kyua_cv_signals_lastno], [ + AC_RUN_IFELSE([AC_LANG_PROGRAM([#include +#include +#include +#include +#include +#include ], [ + static const int max_signals = 256; + int i; + FILE *f; + + i = 0; + while (i < max_signals) { + i++; + if (i != SIGKILL && i != SIGSTOP) { + struct sigaction sa; + int ret; + + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + ret = sigaction(i, &sa, NULL); + if (ret == -1) { + warn("sigaction(%d) failed", i); + if (errno == EINVAL) { + i--; + break; + } else + err(EXIT_FAILURE, "sigaction failed"); + } + } + } + if (i == max_signals) + errx(EXIT_FAILURE, "too many signals"); + + f = fopen("conftest.cnt", "w"); + if (f == NULL) + err(EXIT_FAILURE, "failed to open file"); + + fprintf(f, "%d\n", i); + fclose(f); + + return EXIT_SUCCESS; +])], + [if test ! -f conftest.cnt; then + kyua_cv_signals_lastno=15 + else + kyua_cv_signals_lastno=$(cat conftest.cnt) + rm -f conftest.cnt + fi], + [kyua_cv_signals_lastno=15]) + ]) + AC_DEFINE_UNQUOTED([LAST_SIGNO], [${kyua_cv_signals_lastno}], + [Define to the last valid signal number]) +]) diff --git a/m4/uname.m4 b/m4/uname.m4 new file mode 100644 index 000000000000..bcb3d0d39a71 --- /dev/null +++ b/m4/uname.m4 @@ -0,0 +1,63 @@ +dnl Copyright 2010 The Kyua Authors. +dnl All rights reserved. +dnl +dnl Redistribution and use in source and binary forms, with or without +dnl modification, are permitted provided that the following conditions are +dnl met: +dnl +dnl * Redistributions of source code must retain the above copyright +dnl notice, this list of conditions and the following disclaimer. +dnl * Redistributions in binary form must reproduce the above copyright +dnl notice, this list of conditions and the following disclaimer in the +dnl documentation and/or other materials provided with the distribution. +dnl * Neither the name of Google Inc. nor the names of its contributors +dnl may be used to endorse or promote products derived from this software +dnl without specific prior written permission. +dnl +dnl THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +dnl "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +dnl LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +dnl A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +dnl OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +dnl SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +dnl LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +dnl DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +dnl THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +dnl OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dnl +dnl KYUA_UNAME_ARCHITECTURE +dnl +dnl Checks for the current architecture name (aka processor type) and defines +dnl the KYUA_ARCHITECTURE macro to its value. +dnl +AC_DEFUN([KYUA_UNAME_ARCHITECTURE], [ + AC_MSG_CHECKING([for architecture name]) + AC_ARG_VAR([KYUA_ARCHITECTURE], + [Name of the system architecture (aka processor type)]) + if test x"${KYUA_ARCHITECTURE-unset}" = x"unset"; then + KYUA_ARCHITECTURE="$(uname -p)" + fi + AC_DEFINE_UNQUOTED([KYUA_ARCHITECTURE], "${KYUA_ARCHITECTURE}", + [Name of the system architecture (aka processor type)]) + AC_MSG_RESULT([${KYUA_ARCHITECTURE}]) +]) + +dnl +dnl KYUA_UNAME_PLATFORM +dnl +dnl Checks for the current platform name (aka machine name) and defines +dnl the KYUA_PLATFORM macro to its value. +dnl +AC_DEFUN([KYUA_UNAME_PLATFORM], [ + AC_MSG_CHECKING([for platform name]) + AC_ARG_VAR([KYUA_PLATFORM], + [Name of the system platform (aka machine name)]) + if test x"${KYUA_PLATFORM-unset}" = x"unset"; then + KYUA_PLATFORM="$(uname -m)" + fi + AC_DEFINE_UNQUOTED([KYUA_PLATFORM], "${KYUA_PLATFORM}", + [Name of the system platform (aka machine name)]) + AC_MSG_RESULT([${KYUA_PLATFORM}]) +]) diff --git a/main.cpp b/main.cpp new file mode 100644 index 000000000000..4344248f89db --- /dev/null +++ b/main.cpp @@ -0,0 +1,50 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/main.hpp" + + +/// Program entry point. +/// +/// The whole purpose of this extremely-simple function is to delegate execution +/// to an internal module that does not contain a proper ::main() function. +/// This is to allow unit-testing of the internal code. +/// +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// +/// \return 0 on success, some other integer on error. +/// +/// \throw std::exception This throws any uncaught exception. Such exceptions +/// are bugs, but we let them propagate so that the runtime will abort and +/// dump core. +int +main(const int argc, const char* const* const argv) +{ + return cli::main(argc, argv); +} diff --git a/misc/Makefile.am.inc b/misc/Makefile.am.inc new file mode 100644 index 000000000000..e235c7ee364e --- /dev/null +++ b/misc/Makefile.am.inc @@ -0,0 +1,32 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +dist_misc_DATA = misc/context.html +dist_misc_DATA += misc/index.html +dist_misc_DATA += misc/report.css +dist_misc_DATA += misc/test_result.html diff --git a/misc/context.html b/misc/context.html new file mode 100644 index 000000000000..cb8f16c582fb --- /dev/null +++ b/misc/context.html @@ -0,0 +1,55 @@ + + + + + Execution context + + + + + +

Execution context

+ +
    +
  • Work directory: %%cwd%%
  • +
+ +

Environment variables

+ +
    +%loop env_var iter +
  • %%env_var(iter)%%: %%env_var_value(iter)%%
  • +%endloop +
+ + + diff --git a/misc/index.html b/misc/index.html new file mode 100644 index 000000000000..ca53ff3623fb --- /dev/null +++ b/misc/index.html @@ -0,0 +1,187 @@ + + + + + + Tests summary + + + + + + +

Summary of test results

+ +

Overall result: +%if bad_tests_count + %%bad_tests_count%% TESTS FAILING +%else + ALL TESTS PASSING +%endif +

+ + + + + + + + + + +%if length(broken_test_cases) + + + + +%else + + + + +%endif +%if length(failed_test_cases) + + + + +%else + + + + +%endif + +%if length(xfail_test_cases) + +%else + +%endif + + + +%if length(skipped_test_cases) + +%else + +%endif + + + +%if length(passed_test_cases) + +%else + +%endif + + + +
Test case resultCount
Broken%%length(broken_test_cases)%%
Broken%%broken_tests_count%%
Failed%%length(failed_test_cases)%%
Failed%%failed_tests_count%%
Expected failuresExpected failures%%xfail_tests_count%%
SkippedSkipped%%skipped_tests_count%%
PassedPassed%%passed_tests_count%%
+ +

Execution context

+ +

Timing data:

+ +
    +
  • Start time: %%start_time%%
  • +
  • End time: %%end_time%%
  • +
  • Duration: %%duration%%
  • +
+ + +%if length(broken_test_cases) +

Broken test cases

+ + +%endif + + +%if length(failed_test_cases) +

Failed test cases

+ + +%endif + + +%if length(xfail_test_cases) +

Expected failures

+ + +%endif + + +%if length(skipped_test_cases) +

Skipped test cases

+ + +%endif + + +%if length(passed_test_cases) +

Passed test cases

+ + +%endif + + + + diff --git a/misc/report.css b/misc/report.css new file mode 100644 index 000000000000..ede4c5255fd6 --- /dev/null +++ b/misc/report.css @@ -0,0 +1,78 @@ +/* Copyright 2012 The Kyua Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Google Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +body { + background: white; + text-color: black; +} + +h1 { + color: #00d000; +} + +h2 { + color: #00a000; +} + +p.overall font.good { + color: #00ff00; +} + +p.overall font.bad { + color: #ff0000; +} + +pre { + background-color: #e0f0e0; + margin-left: 20px; + margin-right: 20px; + padding: 5px; +} + +table.tests-count { + border-width: 1; + border-style: solid; + border-color: #b0e0b0; + padding: 0; +} + +table.tests-count td { + padding: 3px; +} + +table.tests-count td.numeric { + text-align: right; +} + +table.tests-count tr.bad { + background: #e0b0b0; +} + +table.tests-count thead tr { + background: #b0e0b0; +} diff --git a/misc/test_result.html b/misc/test_result.html new file mode 100644 index 000000000000..4c4a4132b66c --- /dev/null +++ b/misc/test_result.html @@ -0,0 +1,76 @@ + + + + + Test case: %%test_case%% + + + + + +

Test case: %%test_case%%

+ +
    +
  • Test program: %%test_program%%
  • +
  • Result: %%result%%
  • +
  • Start time: %%start_time%%
  • +
  • End time: %%end_time%%
  • +
  • Duration: %%duration%%
  • +
  • Execution context
  • +
+ +

Metadata

+ +
    +%loop metadata_var iter +
  • %%metadata_var(iter)%% = %%metadata_value(iter)%%
  • +%endloop +
+ +

Standard output

+ +%if defined(stdout) +
%%stdout%%
+%else +Test case did not write anything to stdout. +%endif + +

Standard error

+ +%if defined(stderr) +
%%stderr%%
+%else +Test case did not write anything to stderr. +%endif + + + diff --git a/model/Kyuafile b/model/Kyuafile new file mode 100644 index 000000000000..9dae3b9c64ce --- /dev/null +++ b/model/Kyuafile @@ -0,0 +1,10 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="context_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="metadata_test"} +atf_test_program{name="test_case_test"} +atf_test_program{name="test_program_test"} +atf_test_program{name="test_result_test"} diff --git a/model/Makefile.am.inc b/model/Makefile.am.inc new file mode 100644 index 000000000000..2bd33914f680 --- /dev/null +++ b/model/Makefile.am.inc @@ -0,0 +1,89 @@ +# Copyright 2014 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +MODEL_CFLAGS = $(UTILS_CFLAGS) +MODEL_LIBS = libmodel.a $(UTILS_LIBS) + +noinst_LIBRARIES += libmodel.a +libmodel_a_CPPFLAGS = $(UTILS_CFLAGS) +libmodel_a_SOURCES = model/context.cpp +libmodel_a_SOURCES += model/context.hpp +libmodel_a_SOURCES += model/context_fwd.hpp +libmodel_a_SOURCES += model/exceptions.cpp +libmodel_a_SOURCES += model/exceptions.hpp +libmodel_a_SOURCES += model/metadata.cpp +libmodel_a_SOURCES += model/metadata.hpp +libmodel_a_SOURCES += model/metadata_fwd.hpp +libmodel_a_SOURCES += model/test_case.cpp +libmodel_a_SOURCES += model/test_case.hpp +libmodel_a_SOURCES += model/test_case_fwd.hpp +libmodel_a_SOURCES += model/test_program.cpp +libmodel_a_SOURCES += model/test_program.hpp +libmodel_a_SOURCES += model/test_program_fwd.hpp +libmodel_a_SOURCES += model/test_result.cpp +libmodel_a_SOURCES += model/test_result.hpp +libmodel_a_SOURCES += model/test_result_fwd.hpp +libmodel_a_SOURCES += model/types.hpp + +if WITH_ATF +tests_modeldir = $(pkgtestsdir)/model + +tests_model_DATA = model/Kyuafile +EXTRA_DIST += $(tests_model_DATA) + +tests_model_PROGRAMS = model/context_test +model_context_test_SOURCES = model/context_test.cpp +model_context_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_context_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/exceptions_test +model_exceptions_test_SOURCES = model/exceptions_test.cpp +model_exceptions_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_exceptions_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/metadata_test +model_metadata_test_SOURCES = model/metadata_test.cpp +model_metadata_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_metadata_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/test_case_test +model_test_case_test_SOURCES = model/test_case_test.cpp +model_test_case_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_test_case_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/test_program_test +model_test_program_test_SOURCES = model/test_program_test.cpp +model_test_program_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_test_program_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +tests_model_PROGRAMS += model/test_result_test +model_test_result_test_SOURCES = model/test_result_test.cpp +model_test_result_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS) +model_test_result_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS) + +endif diff --git a/model/README b/model/README new file mode 100644 index 000000000000..cf13a82b7338 --- /dev/null +++ b/model/README @@ -0,0 +1,11 @@ +This directory contains the classes that form the data model of Kyua. + +The classes in this directory are intended to be pure data types without +any complex logic. As such, they are simple containers and support the +common operations you would expect from them: in particular, comparisons +and formatting for debugging purposes. + +All the classes in the data model have to have an on-disk representation +provided by the store module; if they don't, they don't belong in the +model. Some of these classes may also have special behavior at run-time, +and this is provided by the engine module. diff --git a/model/context.cpp b/model/context.cpp new file mode 100644 index 000000000000..5afe89759d94 --- /dev/null +++ b/model/context.cpp @@ -0,0 +1,159 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/context.hpp" + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/text/operations.ipp" + +namespace fs = utils::fs; +namespace text = utils::text; + + +/// Internal implementation of a context. +struct model::context::impl : utils::noncopyable { + /// The current working directory. + fs::path _cwd; + + /// The environment variables. + std::map< std::string, std::string > _env; + + /// Constructor. + /// + /// \param cwd_ The current working directory. + /// \param env_ The environment variables. + impl(const fs::path& cwd_, + const std::map< std::string, std::string >& env_) : + _cwd(cwd_), + _env(env_) + { + } + + /// Equality comparator. + /// + /// \param other The object to compare to. + /// + /// \return True if the two objects are equal; false otherwise. + bool + operator==(const impl& other) const + { + return _cwd == other._cwd && _env == other._env; + } +}; + + +/// Constructs a new context. +/// +/// \param cwd_ The current working directory. +/// \param env_ The environment variables. +model::context::context(const fs::path& cwd_, + const std::map< std::string, std::string >& env_) : + _pimpl(new impl(cwd_, env_)) +{ +} + + +/// Destructor. +model::context::~context(void) +{ +} + + +/// Returns the current working directory of the context. +/// +/// \return A path. +const fs::path& +model::context::cwd(void) const +{ + return _pimpl->_cwd; +} + + +/// Returns the environment variables of the context. +/// +/// \return A variable name to variable value mapping. +const std::map< std::string, std::string >& +model::context::env(void) const +{ + return _pimpl->_env; +} + + +/// Equality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +bool +model::context::operator==(const context& other) const +{ + return *_pimpl == *other._pimpl; +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +model::context::operator!=(const context& other) const +{ + return !(*this == other); +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +model::operator<<(std::ostream& output, const context& object) +{ + output << F("context{cwd=%s, env=[") + % text::quote(object.cwd().str(), '\''); + + const std::map< std::string, std::string >& env = object.env(); + bool first = true; + for (std::map< std::string, std::string >::const_iterator + iter = env.begin(); iter != env.end(); ++iter) { + if (!first) + output << ", "; + first = false; + + output << F("%s=%s") % (*iter).first + % text::quote((*iter).second, '\''); + } + + output << "]}"; + return output; +} diff --git a/model/context.hpp b/model/context.hpp new file mode 100644 index 000000000000..d11ae8ba80b9 --- /dev/null +++ b/model/context.hpp @@ -0,0 +1,76 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/context.hpp +/// Representation of runtime contexts. + +#if !defined(MODEL_CONTEXT_HPP) +#define MODEL_CONTEXT_HPP + +#include "model/context_fwd.hpp" + +#include +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace model { + + +/// Representation of a runtime context. +/// +/// The instances of this class are unique (i.e. copying the objects only yields +/// a shallow copy that shares the same internal implementation). This is a +/// requirement for the 'store' API model. +class context { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + +public: + context(const utils::fs::path&, + const std::map< std::string, std::string >&); + ~context(void); + + const utils::fs::path& cwd(void) const; + const std::map< std::string, std::string >& env(void) const; + + bool operator==(const context&) const; + bool operator!=(const context&) const; +}; + + +std::ostream& operator<<(std::ostream&, const context&); + + +} // namespace model + +#endif // !defined(MODEL_CONTEXT_HPP) diff --git a/model/context_fwd.hpp b/model/context_fwd.hpp new file mode 100644 index 000000000000..000ed864e948 --- /dev/null +++ b/model/context_fwd.hpp @@ -0,0 +1,43 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/context_fwd.hpp +/// Forward declarations for model/context.hpp + +#if !defined(MODEL_CONTEXT_FWD_HPP) +#define MODEL_CONTEXT_FWD_HPP + +namespace model { + + +class context; + + +} // namespace model + +#endif // !defined(MODEL_CONTEXT_FWD_HPP) diff --git a/model/context_test.cpp b/model/context_test.cpp new file mode 100644 index 000000000000..8990990710f2 --- /dev/null +++ b/model/context_test.cpp @@ -0,0 +1,106 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/context.hpp" + +#include +#include +#include + +#include + +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(ctor_and_getters); +ATF_TEST_CASE_BODY(ctor_and_getters) +{ + std::map< std::string, std::string > env; + env["foo"] = "first"; + env["bar"] = "second"; + const model::context context(fs::path("/foo/bar"), env); + ATF_REQUIRE_EQ(fs::path("/foo/bar"), context.cwd()); + ATF_REQUIRE(env == context.env()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne); +ATF_TEST_CASE_BODY(operators_eq_and_ne) +{ + std::map< std::string, std::string > env; + env["foo"] = "first"; + const model::context context1(fs::path("/foo/bar"), env); + const model::context context2(fs::path("/foo/bar"), env); + const model::context context3(fs::path("/foo/baz"), env); + env["bar"] = "second"; + const model::context context4(fs::path("/foo/bar"), env); + ATF_REQUIRE( context1 == context2); + ATF_REQUIRE(!(context1 != context2)); + ATF_REQUIRE(!(context1 == context3)); + ATF_REQUIRE( context1 != context3); + ATF_REQUIRE(!(context1 == context4)); + ATF_REQUIRE( context1 != context4); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__empty_env); +ATF_TEST_CASE_BODY(output__empty_env) +{ + const std::map< std::string, std::string > env; + const model::context context(fs::path("/foo/bar"), env); + + std::ostringstream str; + str << context; + ATF_REQUIRE_EQ("context{cwd='/foo/bar', env=[]}", str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__some_env); +ATF_TEST_CASE_BODY(output__some_env) +{ + std::map< std::string, std::string > env; + env["foo"] = "first"; + env["bar"] = "second' var"; + const model::context context(fs::path("/foo/bar"), env); + + std::ostringstream str; + str << context; + ATF_REQUIRE_EQ("context{cwd='/foo/bar', env=[bar='second\\' var', " + "foo='first']}", str.str()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ctor_and_getters); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne); + ATF_ADD_TEST_CASE(tcs, output__empty_env); + ATF_ADD_TEST_CASE(tcs, output__some_env); +} diff --git a/model/exceptions.cpp b/model/exceptions.cpp new file mode 100644 index 000000000000..dc511a2b7e8f --- /dev/null +++ b/model/exceptions.cpp @@ -0,0 +1,76 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/exceptions.hpp" + +#include "utils/format/macros.hpp" + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +model::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +model::error::~error(void) throw() +{ +} + + +/// Constructs a new format_error. +/// +/// \param message The plain-text error message. +model::format_error::format_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +model::format_error::~format_error(void) throw() +{ +} + + +/// Constructs a new not_found_error. +/// +/// \param message The plain-text error message. +model::not_found_error::not_found_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +model::not_found_error::~not_found_error(void) throw() +{ +} diff --git a/model/exceptions.hpp b/model/exceptions.hpp new file mode 100644 index 000000000000..ff4970fc37d7 --- /dev/null +++ b/model/exceptions.hpp @@ -0,0 +1,71 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/exceptions.hpp +/// Exception types raised by the model module. +/// +/// There is no model/exceptions_fwd.hpp counterpart because this file is +/// inteded to be used only from within .cpp files to either raise or +/// handle raised exceptions, neither of which are possible with just +/// forward declarations. + +#if !defined(MODEL_EXCEPTIONS_HPP) +#define MODEL_EXCEPTIONS_HPP + +#include + +namespace model { + + +/// Base exception for model errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// Error while parsing external data. +class format_error : public error { +public: + explicit format_error(const std::string&); + virtual ~format_error(void) throw(); +}; + + +/// A requested element could not be found. +class not_found_error : public error { +public: + explicit not_found_error(const std::string&); + virtual ~not_found_error(void) throw(); +}; + + +} // namespace model + +#endif // !defined(MODEL_EXCEPTIONS_HPP) diff --git a/model/exceptions_test.cpp b/model/exceptions_test.cpp new file mode 100644 index 000000000000..e9c17c0cc19a --- /dev/null +++ b/model/exceptions_test.cpp @@ -0,0 +1,65 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/exceptions.hpp" + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const model::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_error); +ATF_TEST_CASE_BODY(format_error) +{ + const model::format_error e("Some other text"); + ATF_REQUIRE(std::strcmp("Some other text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(not_found_error); +ATF_TEST_CASE_BODY(not_found_error) +{ + const model::not_found_error e("Missing foo"); + ATF_REQUIRE(std::strcmp("Missing foo", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, format_error); + ATF_ADD_TEST_CASE(tcs, not_found_error); +} diff --git a/model/metadata.cpp b/model/metadata.cpp new file mode 100644 index 000000000000..d27e3237dcf2 --- /dev/null +++ b/model/metadata.cpp @@ -0,0 +1,1068 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/metadata.hpp" + +#include + +#include "model/exceptions.hpp" +#include "model/types.hpp" +#include "utils/config/exceptions.hpp" +#include "utils/config/nodes.ipp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.hpp" +#include "utils/units.hpp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace text = utils::text; +namespace units = utils::units; + +using utils::optional; + + +namespace { + + +/// Global instance of defaults. +/// +/// This exists so that the getters in metadata can return references instead +/// of object copies. Use get_defaults() to query. +static optional< config::tree > defaults; + + +/// A leaf node that holds a bytes quantity. +class bytes_node : public config::native_leaf_node< units::bytes > { +public: + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< bytes_node > new_node(new bytes_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Pushes the node's value onto the Lua stack. + void + push_lua(lutok::state& /* state */) const + { + UNREACHABLE; + } + + /// Sets the value of the node from an entry in the Lua stack. + void + set_lua(lutok::state& /* state */, const int /* index */) + { + UNREACHABLE; + } +}; + + +/// A leaf node that holds a time delta. +class delta_node : public config::typed_leaf_node< datetime::delta > { +public: + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< delta_node > new_node(new delta_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Sets the value of the node from a raw string representation. + /// + /// \param raw_value The value to set the node to. + /// + /// \throw value_error If the value is invalid. + void + set_string(const std::string& raw_value) + { + unsigned int seconds; + try { + seconds = text::to_type< unsigned int >(raw_value); + } catch (const text::error& e) { + throw config::value_error(F("Invalid time delta %s") % raw_value); + } + set(datetime::delta(seconds, 0)); + } + + /// Converts the contents of the node to a string. + /// + /// \pre The node must have a value. + /// + /// \return A string representation of the value held by the node. + std::string + to_string(void) const + { + return F("%s") % value().seconds; + } + + /// Pushes the node's value onto the Lua stack. + void + push_lua(lutok::state& /* state */) const + { + UNREACHABLE; + } + + /// Sets the value of the node from an entry in the Lua stack. + void + set_lua(lutok::state& /* state */, const int /* index */) + { + UNREACHABLE; + } +}; + + +/// A leaf node that holds a "required user" property. +/// +/// This node is just a string, but it provides validation of the only allowed +/// values. +class user_node : public config::string_node { + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< user_node > new_node(new user_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Checks a given user textual representation for validity. + /// + /// \param user The value to validate. + /// + /// \throw config::value_error If the value is not valid. + void + validate(const value_type& user) const + { + if (!user.empty() && user != "root" && user != "unprivileged") + throw config::value_error("Invalid required user value"); + } +}; + + +/// A leaf node that holds a set of paths. +/// +/// This node type is used to represent the value of the required files and +/// required programs, for example, and these do not allow relative paths. We +/// check this here. +class paths_set_node : public config::base_set_node< fs::path > { + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< paths_set_node > new_node(new paths_set_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Converts a single path to the native type. + /// + /// \param raw_value The value to parse. + /// + /// \return The parsed value. + /// + /// \throw config::value_error If the value is invalid. + fs::path + parse_one(const std::string& raw_value) const + { + try { + return fs::path(raw_value); + } catch (const fs::error& e) { + throw config::value_error(e.what()); + } + } + + /// Checks a collection of paths for validity. + /// + /// \param paths The value to validate. + /// + /// \throw config::value_error If the value is not valid. + void + validate(const value_type& paths) const + { + for (value_type::const_iterator iter = paths.begin(); + iter != paths.end(); ++iter) { + const fs::path& path = *iter; + if (!path.is_absolute() && path.ncomponents() > 1) + throw config::value_error(F("Relative path '%s' not allowed") % + *iter); + } + } +}; + + +/// Initializes a tree to hold test case requirements. +/// +/// \param [in,out] tree The tree to initialize. +static void +init_tree(config::tree& tree) +{ + tree.define< config::strings_set_node >("allowed_architectures"); + tree.define< config::strings_set_node >("allowed_platforms"); + tree.define_dynamic("custom"); + tree.define< config::string_node >("description"); + tree.define< config::bool_node >("has_cleanup"); + tree.define< config::bool_node >("is_exclusive"); + tree.define< config::strings_set_node >("required_configs"); + tree.define< bytes_node >("required_disk_space"); + tree.define< paths_set_node >("required_files"); + tree.define< bytes_node >("required_memory"); + tree.define< paths_set_node >("required_programs"); + tree.define< user_node >("required_user"); + tree.define< delta_node >("timeout"); +} + + +/// Sets default values on a tree object. +/// +/// \param [in,out] tree The tree to configure. +static void +set_defaults(config::tree& tree) +{ + tree.set< config::strings_set_node >("allowed_architectures", + model::strings_set()); + tree.set< config::strings_set_node >("allowed_platforms", + model::strings_set()); + tree.set< config::string_node >("description", ""); + tree.set< config::bool_node >("has_cleanup", false); + tree.set< config::bool_node >("is_exclusive", false); + tree.set< config::strings_set_node >("required_configs", + model::strings_set()); + tree.set< bytes_node >("required_disk_space", units::bytes(0)); + tree.set< paths_set_node >("required_files", model::paths_set()); + tree.set< bytes_node >("required_memory", units::bytes(0)); + tree.set< paths_set_node >("required_programs", model::paths_set()); + tree.set< user_node >("required_user", ""); + // TODO(jmmv): We shouldn't be setting a default timeout like this. See + // Issue 5 for details. + tree.set< delta_node >("timeout", datetime::delta(300, 0)); +} + + +/// Queries the global defaults tree object with lazy initialization. +/// +/// \return A metadata tree. This object is statically allocated so it is +/// acceptable to obtain references to it and its members. +const config::tree& +get_defaults(void) +{ + if (!defaults) { + config::tree props; + init_tree(props); + set_defaults(props); + defaults = props; + } + return defaults.get(); +} + + +/// Looks up a value in a tree with error rewriting. +/// +/// \tparam NodeType The type of the node. +/// \param tree The tree in which to insert the value. +/// \param key The key to set. +/// +/// \return A read-write reference to the value in the node. +/// +/// \throw model::error If the key is not known or if the value is not valid. +template< class NodeType > +typename NodeType::value_type& +lookup_rw(config::tree& tree, const std::string& key) +{ + try { + return tree.lookup_rw< NodeType >(key); + } catch (const config::unknown_key_error& e) { + throw model::error(F("Unknown metadata property %s") % key); + } catch (const config::value_error& e) { + throw model::error(F("Invalid value for metadata property %s: %s") % + key % e.what()); + } +} + + +/// Sets a value in a tree with error rewriting. +/// +/// \tparam NodeType The type of the node. +/// \param tree The tree in which to insert the value. +/// \param key The key to set. +/// \param value The value to set the node to. +/// +/// \throw model::error If the key is not known or if the value is not valid. +template< class NodeType > +void +set(config::tree& tree, const std::string& key, + const typename NodeType::value_type& value) +{ + try { + tree.set< NodeType >(key, value); + } catch (const config::unknown_key_error& e) { + throw model::error(F("Unknown metadata property %s") % key); + } catch (const config::value_error& e) { + throw model::error(F("Invalid value for metadata property %s: %s") % + key % e.what()); + } +} + + +} // anonymous namespace + + +/// Internal implementation of the metadata class. +struct model::metadata::impl : utils::noncopyable { + /// Metadata properties. + config::tree props; + + /// Constructor. + /// + /// \param props_ Metadata properties of the test. + impl(const utils::config::tree& props_) : + props(props_) + { + } + + /// Equality comparator. + /// + /// \param other The other object to compare this one to. + /// + /// \return True if this object and other are equal; false otherwise. + bool + operator==(const impl& other) const + { + return (get_defaults().combine(props) == + get_defaults().combine(other.props)); + } +}; + + +/// Constructor. +/// +/// \param props Metadata properties of the test. +model::metadata::metadata(const utils::config::tree& props) : + _pimpl(new impl(props)) +{ +} + + +/// Destructor. +model::metadata::~metadata(void) +{ +} + + +/// Applies a set of overrides to this metadata object. +/// +/// \param overrides The overrides to apply. Any values explicitly set in this +/// other object will override any possible values set in this object. +/// +/// \return A new metadata object with the combination. +model::metadata +model::metadata::apply_overrides(const metadata& overrides) const +{ + return metadata(_pimpl->props.combine(overrides._pimpl->props)); +} + + +/// Returns the architectures allowed by the test. +/// +/// \return Set of architectures, or empty if this does not apply. +const model::strings_set& +model::metadata::allowed_architectures(void) const +{ + if (_pimpl->props.is_set("allowed_architectures")) { + return _pimpl->props.lookup< config::strings_set_node >( + "allowed_architectures"); + } else { + return get_defaults().lookup< config::strings_set_node >( + "allowed_architectures"); + } +} + + +/// Returns the platforms allowed by the test. +/// +/// \return Set of platforms, or empty if this does not apply. +const model::strings_set& +model::metadata::allowed_platforms(void) const +{ + if (_pimpl->props.is_set("allowed_platforms")) { + return _pimpl->props.lookup< config::strings_set_node >( + "allowed_platforms"); + } else { + return get_defaults().lookup< config::strings_set_node >( + "allowed_platforms"); + } +} + + +/// Returns all the user-defined metadata properties. +/// +/// \return A key/value map of properties. +model::properties_map +model::metadata::custom(void) const +{ + return _pimpl->props.all_properties("custom", true); +} + + +/// Returns the description of the test. +/// +/// \return Textual description; may be empty. +const std::string& +model::metadata::description(void) const +{ + if (_pimpl->props.is_set("description")) { + return _pimpl->props.lookup< config::string_node >("description"); + } else { + return get_defaults().lookup< config::string_node >("description"); + } +} + + +/// Returns whether the test has a cleanup part or not. +/// +/// \return True if there is a cleanup part; false otherwise. +bool +model::metadata::has_cleanup(void) const +{ + if (_pimpl->props.is_set("has_cleanup")) { + return _pimpl->props.lookup< config::bool_node >("has_cleanup"); + } else { + return get_defaults().lookup< config::bool_node >("has_cleanup"); + } +} + + +/// Returns whether the test is exclusive or not. +/// +/// \return True if the test has to be run on its own, not concurrently with any +/// other tests; false otherwise. +bool +model::metadata::is_exclusive(void) const +{ + if (_pimpl->props.is_set("is_exclusive")) { + return _pimpl->props.lookup< config::bool_node >("is_exclusive"); + } else { + return get_defaults().lookup< config::bool_node >("is_exclusive"); + } +} + + +/// Returns the list of configuration variables needed by the test. +/// +/// \return Set of configuration variables. +const model::strings_set& +model::metadata::required_configs(void) const +{ + if (_pimpl->props.is_set("required_configs")) { + return _pimpl->props.lookup< config::strings_set_node >( + "required_configs"); + } else { + return get_defaults().lookup< config::strings_set_node >( + "required_configs"); + } +} + + +/// Returns the amount of free disk space required by the test. +/// +/// \return Number of bytes, or 0 if this does not apply. +const units::bytes& +model::metadata::required_disk_space(void) const +{ + if (_pimpl->props.is_set("required_disk_space")) { + return _pimpl->props.lookup< bytes_node >("required_disk_space"); + } else { + return get_defaults().lookup< bytes_node >("required_disk_space"); + } +} + + +/// Returns the list of files needed by the test. +/// +/// \return Set of paths. +const model::paths_set& +model::metadata::required_files(void) const +{ + if (_pimpl->props.is_set("required_files")) { + return _pimpl->props.lookup< paths_set_node >("required_files"); + } else { + return get_defaults().lookup< paths_set_node >("required_files"); + } +} + + +/// Returns the amount of memory required by the test. +/// +/// \return Number of bytes, or 0 if this does not apply. +const units::bytes& +model::metadata::required_memory(void) const +{ + if (_pimpl->props.is_set("required_memory")) { + return _pimpl->props.lookup< bytes_node >("required_memory"); + } else { + return get_defaults().lookup< bytes_node >("required_memory"); + } +} + + +/// Returns the list of programs needed by the test. +/// +/// \return Set of paths. +const model::paths_set& +model::metadata::required_programs(void) const +{ + if (_pimpl->props.is_set("required_programs")) { + return _pimpl->props.lookup< paths_set_node >("required_programs"); + } else { + return get_defaults().lookup< paths_set_node >("required_programs"); + } +} + + +/// Returns the user required by the test. +/// +/// \return One of unprivileged, root or empty. +const std::string& +model::metadata::required_user(void) const +{ + if (_pimpl->props.is_set("required_user")) { + return _pimpl->props.lookup< user_node >("required_user"); + } else { + return get_defaults().lookup< user_node >("required_user"); + } +} + + +/// Returns the timeout of the test. +/// +/// \return A time delta; should be compared to default_timeout to see if it has +/// been overriden. +const datetime::delta& +model::metadata::timeout(void) const +{ + if (_pimpl->props.is_set("timeout")) { + return _pimpl->props.lookup< delta_node >("timeout"); + } else { + return get_defaults().lookup< delta_node >("timeout"); + } +} + + +/// Externalizes the metadata to a set of key/value textual pairs. +/// +/// \return A key/value representation of the metadata. +model::properties_map +model::metadata::to_properties(void) const +{ + const config::tree fully_specified = get_defaults().combine(_pimpl->props); + return fully_specified.all_properties(); +} + + +/// Equality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +bool +model::metadata::operator==(const metadata& other) const +{ + return _pimpl == other._pimpl || *_pimpl == *other._pimpl; +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +model::metadata::operator!=(const metadata& other) const +{ + return !(*this == other); +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +model::operator<<(std::ostream& output, const metadata& object) +{ + output << "metadata{"; + + bool first = true; + const model::properties_map props = object.to_properties(); + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + if (!first) + output << ", "; + output << F("%s=%s") % (*iter).first % + text::quote((*iter).second, '\''); + first = false; + } + + output << "}"; + return output; +} + + +/// Internal implementation of the metadata_builder class. +struct model::metadata_builder::impl : utils::noncopyable { + /// Collection of requirements. + config::tree props; + + /// Whether we have created a metadata object or not. + bool built; + + /// Constructor. + impl(void) : + built(false) + { + init_tree(props); + } + + /// Constructor. + /// + /// \param base The base model to construct a copy from. + impl(const model::metadata& base) : + props(base._pimpl->props.deep_copy()), + built(false) + { + } +}; + + +/// Constructor. +model::metadata_builder::metadata_builder(void) : + _pimpl(new impl()) +{ +} + + +/// Constructor. +model::metadata_builder::metadata_builder(const model::metadata& base) : + _pimpl(new impl(base)) +{ +} + + +/// Destructor. +model::metadata_builder::~metadata_builder(void) +{ +} + + +/// Accumulates an additional allowed architecture. +/// +/// \param arch The architecture. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_allowed_architecture(const std::string& arch) +{ + if (!_pimpl->props.is_set("allowed_architectures")) { + _pimpl->props.set< config::strings_set_node >( + "allowed_architectures", + get_defaults().lookup< config::strings_set_node >( + "allowed_architectures")); + } + lookup_rw< config::strings_set_node >( + _pimpl->props, "allowed_architectures").insert(arch); + return *this; +} + + +/// Accumulates an additional allowed platform. +/// +/// \param platform The platform. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_allowed_platform(const std::string& platform) +{ + if (!_pimpl->props.is_set("allowed_platforms")) { + _pimpl->props.set< config::strings_set_node >( + "allowed_platforms", + get_defaults().lookup< config::strings_set_node >( + "allowed_platforms")); + } + lookup_rw< config::strings_set_node >( + _pimpl->props, "allowed_platforms").insert(platform); + return *this; +} + + +/// Accumulates a single user-defined property. +/// +/// \param key Name of the property to define. +/// \param value Value of the property. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_custom(const std::string& key, + const std::string& value) +{ + _pimpl->props.set_string(F("custom.%s") % key, value); + return *this; +} + + +/// Accumulates an additional required configuration variable. +/// +/// \param var The name of the configuration variable. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_required_config(const std::string& var) +{ + if (!_pimpl->props.is_set("required_configs")) { + _pimpl->props.set< config::strings_set_node >( + "required_configs", + get_defaults().lookup< config::strings_set_node >( + "required_configs")); + } + lookup_rw< config::strings_set_node >( + _pimpl->props, "required_configs").insert(var); + return *this; +} + + +/// Accumulates an additional required file. +/// +/// \param path The path to the file. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_required_file(const fs::path& path) +{ + if (!_pimpl->props.is_set("required_files")) { + _pimpl->props.set< paths_set_node >( + "required_files", + get_defaults().lookup< paths_set_node >("required_files")); + } + lookup_rw< paths_set_node >(_pimpl->props, "required_files").insert(path); + return *this; +} + + +/// Accumulates an additional required program. +/// +/// \param path The path to the program. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::add_required_program(const fs::path& path) +{ + if (!_pimpl->props.is_set("required_programs")) { + _pimpl->props.set< paths_set_node >( + "required_programs", + get_defaults().lookup< paths_set_node >("required_programs")); + } + lookup_rw< paths_set_node >(_pimpl->props, + "required_programs").insert(path); + return *this; +} + + +/// Sets the architectures allowed by the test. +/// +/// \param as Set of architectures. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_allowed_architectures( + const model::strings_set& as) +{ + set< config::strings_set_node >(_pimpl->props, "allowed_architectures", as); + return *this; +} + + +/// Sets the platforms allowed by the test. +/// +/// \return ps Set of platforms. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_allowed_platforms(const model::strings_set& ps) +{ + set< config::strings_set_node >(_pimpl->props, "allowed_platforms", ps); + return *this; +} + + +/// Sets the user-defined properties. +/// +/// \param props The custom properties to set. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_custom(const model::properties_map& props) +{ + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) + _pimpl->props.set_string(F("custom.%s") % (*iter).first, + (*iter).second); + return *this; +} + + +/// Sets the description of the test. +/// +/// \param description Textual description of the test. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_description(const std::string& description) +{ + set< config::string_node >(_pimpl->props, "description", description); + return *this; +} + + +/// Sets whether the test has a cleanup part or not. +/// +/// \param cleanup True if the test has a cleanup part; false otherwise. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_has_cleanup(const bool cleanup) +{ + set< config::bool_node >(_pimpl->props, "has_cleanup", cleanup); + return *this; +} + + +/// Sets whether the test is exclusive or not. +/// +/// \param exclusive True if the test is exclusive; false otherwise. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_is_exclusive(const bool exclusive) +{ + set< config::bool_node >(_pimpl->props, "is_exclusive", exclusive); + return *this; +} + + +/// Sets the list of configuration variables needed by the test. +/// +/// \param vars Set of configuration variables. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_configs(const model::strings_set& vars) +{ + set< config::strings_set_node >(_pimpl->props, "required_configs", vars); + return *this; +} + + +/// Sets the amount of free disk space required by the test. +/// +/// \param bytes Number of bytes. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_disk_space(const units::bytes& bytes) +{ + set< bytes_node >(_pimpl->props, "required_disk_space", bytes); + return *this; +} + + +/// Sets the list of files needed by the test. +/// +/// \param files Set of paths. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_files(const model::paths_set& files) +{ + set< paths_set_node >(_pimpl->props, "required_files", files); + return *this; +} + + +/// Sets the amount of memory required by the test. +/// +/// \param bytes Number of bytes. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_memory(const units::bytes& bytes) +{ + set< bytes_node >(_pimpl->props, "required_memory", bytes); + return *this; +} + + +/// Sets the list of programs needed by the test. +/// +/// \param progs Set of paths. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_programs(const model::paths_set& progs) +{ + set< paths_set_node >(_pimpl->props, "required_programs", progs); + return *this; +} + + +/// Sets the user required by the test. +/// +/// \param user One of unprivileged, root or empty. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_required_user(const std::string& user) +{ + set< user_node >(_pimpl->props, "required_user", user); + return *this; +} + + +/// Sets a metadata property by name from its textual representation. +/// +/// \param key The property to set. +/// \param value The value to set the property to. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid or the key does not exist. +model::metadata_builder& +model::metadata_builder::set_string(const std::string& key, + const std::string& value) +{ + try { + _pimpl->props.set_string(key, value); + } catch (const config::unknown_key_error& e) { + throw model::format_error(F("Unknown metadata property %s") % key); + } catch (const config::value_error& e) { + throw model::format_error( + F("Invalid value for metadata property %s: %s") % key % e.what()); + } + return *this; +} + + +/// Sets the timeout of the test. +/// +/// \param timeout The timeout to set. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_timeout(const datetime::delta& timeout) +{ + set< delta_node >(_pimpl->props, "timeout", timeout); + return *this; +} + + +/// Creates a new metadata object. +/// +/// \pre This has not yet been called. We only support calling this function +/// once due to the way the internal tree works: we pass around references, not +/// deep copies, so if we allowed a second build, we'd encourage reusing the +/// same builder to construct different metadata objects, and this could have +/// unintended consequences. +/// +/// \return The constructed metadata object. +model::metadata +model::metadata_builder::build(void) const +{ + PRE(!_pimpl->built); + _pimpl->built = true; + + return metadata(_pimpl->props); +} diff --git a/model/metadata.hpp b/model/metadata.hpp new file mode 100644 index 000000000000..c7dd4519f122 --- /dev/null +++ b/model/metadata.hpp @@ -0,0 +1,130 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/metadata.hpp +/// Definition of the "test metadata" concept. + +#if !defined(MODEL_METADATA_HPP) +#define MODEL_METADATA_HPP + +#include "model/metadata_fwd.hpp" + +#include +#include +#include + +#include "model/types.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/noncopyable.hpp" +#include "utils/units_fwd.hpp" + +namespace model { + + +/// Collection of metadata properties of a test. +class metadata { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class metadata_builder; + +public: + metadata(const utils::config::tree&); + ~metadata(void); + + metadata apply_overrides(const metadata&) const; + + const strings_set& allowed_architectures(void) const; + const strings_set& allowed_platforms(void) const; + model::properties_map custom(void) const; + const std::string& description(void) const; + bool has_cleanup(void) const; + bool is_exclusive(void) const; + const strings_set& required_configs(void) const; + const utils::units::bytes& required_disk_space(void) const; + const paths_set& required_files(void) const; + const utils::units::bytes& required_memory(void) const; + const paths_set& required_programs(void) const; + const std::string& required_user(void) const; + const utils::datetime::delta& timeout(void) const; + + model::properties_map to_properties(void) const; + + bool operator==(const metadata&) const; + bool operator!=(const metadata&) const; +}; + + +std::ostream& operator<<(std::ostream&, const metadata&); + + +/// Builder for a metadata object. +class metadata_builder : utils::noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +public: + metadata_builder(void); + explicit metadata_builder(const metadata&); + ~metadata_builder(void); + + metadata_builder& add_allowed_architecture(const std::string&); + metadata_builder& add_allowed_platform(const std::string&); + metadata_builder& add_custom(const std::string&, const std::string&); + metadata_builder& add_required_config(const std::string&); + metadata_builder& add_required_file(const utils::fs::path&); + metadata_builder& add_required_program(const utils::fs::path&); + + metadata_builder& set_allowed_architectures(const strings_set&); + metadata_builder& set_allowed_platforms(const strings_set&); + metadata_builder& set_custom(const model::properties_map&); + metadata_builder& set_description(const std::string&); + metadata_builder& set_has_cleanup(const bool); + metadata_builder& set_is_exclusive(const bool); + metadata_builder& set_required_configs(const strings_set&); + metadata_builder& set_required_disk_space(const utils::units::bytes&); + metadata_builder& set_required_files(const paths_set&); + metadata_builder& set_required_memory(const utils::units::bytes&); + metadata_builder& set_required_programs(const paths_set&); + metadata_builder& set_required_user(const std::string&); + metadata_builder& set_string(const std::string&, const std::string&); + metadata_builder& set_timeout(const utils::datetime::delta&); + + metadata build(void) const; +}; + + +} // namespace model + +#endif // !defined(MODEL_METADATA_HPP) diff --git a/model/metadata_fwd.hpp b/model/metadata_fwd.hpp new file mode 100644 index 000000000000..8a8c5c09d77b --- /dev/null +++ b/model/metadata_fwd.hpp @@ -0,0 +1,44 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/metadata_fwd.hpp +/// Forward declarations for model/metadata.hpp + +#if !defined(MODEL_METADATA_FWD_HPP) +#define MODEL_METADATA_FWD_HPP + +namespace model { + + +class metadata; +class metadata_builder; + + +} // namespace model + +#endif // !defined(MODEL_METADATA_FWD_HPP) diff --git a/model/metadata_test.cpp b/model/metadata_test.cpp new file mode 100644 index 000000000000..7b22653ec1a2 --- /dev/null +++ b/model/metadata_test.cpp @@ -0,0 +1,461 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/metadata.hpp" + +#include + +#include + +#include "model/types.hpp" +#include "utils/datetime.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(defaults); +ATF_TEST_CASE_BODY(defaults) +{ + const model::metadata md = model::metadata_builder().build(); + ATF_REQUIRE(md.allowed_architectures().empty()); + ATF_REQUIRE(md.allowed_platforms().empty()); + ATF_REQUIRE(md.allowed_platforms().empty()); + ATF_REQUIRE(md.custom().empty()); + ATF_REQUIRE(md.description().empty()); + ATF_REQUIRE(!md.has_cleanup()); + ATF_REQUIRE(!md.is_exclusive()); + ATF_REQUIRE(md.required_configs().empty()); + ATF_REQUIRE_EQ(units::bytes(0), md.required_disk_space()); + ATF_REQUIRE(md.required_files().empty()); + ATF_REQUIRE_EQ(units::bytes(0), md.required_memory()); + ATF_REQUIRE(md.required_programs().empty()); + ATF_REQUIRE(md.required_user().empty()); + ATF_REQUIRE(datetime::delta(300, 0) == md.timeout()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(add); +ATF_TEST_CASE_BODY(add) +{ + model::strings_set architectures; + architectures.insert("1-architecture"); + architectures.insert("2-architecture"); + + model::strings_set platforms; + platforms.insert("1-platform"); + platforms.insert("2-platform"); + + model::properties_map custom; + custom["1-custom"] = "first"; + custom["2-custom"] = "second"; + + model::strings_set configs; + configs.insert("1-config"); + configs.insert("2-config"); + + model::paths_set files; + files.insert(fs::path("1-file")); + files.insert(fs::path("2-file")); + + model::paths_set programs; + programs.insert(fs::path("1-program")); + programs.insert(fs::path("2-program")); + + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("1-architecture") + .add_allowed_platform("1-platform") + .add_custom("1-custom", "first") + .add_custom("2-custom", "second") + .add_required_config("1-config") + .add_required_file(fs::path("1-file")) + .add_required_program(fs::path("1-program")) + .add_allowed_architecture("2-architecture") + .add_allowed_platform("2-platform") + .add_required_config("2-config") + .add_required_file(fs::path("2-file")) + .add_required_program(fs::path("2-program")) + .build(); + + ATF_REQUIRE(architectures == md.allowed_architectures()); + ATF_REQUIRE(platforms == md.allowed_platforms()); + ATF_REQUIRE(custom == md.custom()); + ATF_REQUIRE(configs == md.required_configs()); + ATF_REQUIRE(files == md.required_files()); + ATF_REQUIRE(programs == md.required_programs()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(copy); +ATF_TEST_CASE_BODY(copy) +{ + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("1-architecture") + .add_allowed_platform("1-platform") + .build(); + + const model::metadata md2 = model::metadata_builder(md1) + .add_allowed_architecture("2-architecture") + .build(); + + ATF_REQUIRE_EQ(1, md1.allowed_architectures().size()); + ATF_REQUIRE_EQ(2, md2.allowed_architectures().size()); + ATF_REQUIRE_EQ(1, md1.allowed_platforms().size()); + ATF_REQUIRE_EQ(1, md2.allowed_platforms().size()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(apply_overrides); +ATF_TEST_CASE_BODY(apply_overrides) +{ + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("1-architecture") + .add_allowed_platform("1-platform") + .set_description("Explicit description") + .build(); + + const model::metadata md2 = model::metadata_builder() + .add_allowed_architecture("2-architecture") + .set_description("") + .set_timeout(datetime::delta(500, 0)) + .build(); + + const model::metadata merge_1_2 = model::metadata_builder() + .add_allowed_architecture("2-architecture") + .add_allowed_platform("1-platform") + .set_description("") + .set_timeout(datetime::delta(500, 0)) + .build(); + ATF_REQUIRE_EQ(merge_1_2, md1.apply_overrides(md2)); + + const model::metadata merge_2_1 = model::metadata_builder() + .add_allowed_architecture("1-architecture") + .add_allowed_platform("1-platform") + .set_description("Explicit description") + .set_timeout(datetime::delta(500, 0)) + .build(); + ATF_REQUIRE_EQ(merge_2_1, md2.apply_overrides(md1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(override_all_with_setters); +ATF_TEST_CASE_BODY(override_all_with_setters) +{ + model::strings_set architectures; + architectures.insert("the-architecture"); + + model::strings_set platforms; + platforms.insert("the-platforms"); + + model::properties_map custom; + custom["first"] = "hello"; + custom["second"] = "bye"; + + const std::string description = "Some long text"; + + model::strings_set configs; + configs.insert("the-configs"); + + model::paths_set files; + files.insert(fs::path("the-files")); + + const units::bytes disk_space(6789); + + const units::bytes memory(12345); + + model::paths_set programs; + programs.insert(fs::path("the-programs")); + + const std::string user = "root"; + + const datetime::delta timeout(123, 0); + + const model::metadata md = model::metadata_builder() + .set_allowed_architectures(architectures) + .set_allowed_platforms(platforms) + .set_custom(custom) + .set_description(description) + .set_has_cleanup(true) + .set_is_exclusive(true) + .set_required_configs(configs) + .set_required_disk_space(disk_space) + .set_required_files(files) + .set_required_memory(memory) + .set_required_programs(programs) + .set_required_user(user) + .set_timeout(timeout) + .build(); + + ATF_REQUIRE(architectures == md.allowed_architectures()); + ATF_REQUIRE(platforms == md.allowed_platforms()); + ATF_REQUIRE(custom == md.custom()); + ATF_REQUIRE_EQ(description, md.description()); + ATF_REQUIRE(md.has_cleanup()); + ATF_REQUIRE(md.is_exclusive()); + ATF_REQUIRE(configs == md.required_configs()); + ATF_REQUIRE_EQ(disk_space, md.required_disk_space()); + ATF_REQUIRE(files == md.required_files()); + ATF_REQUIRE_EQ(memory, md.required_memory()); + ATF_REQUIRE(programs == md.required_programs()); + ATF_REQUIRE_EQ(user, md.required_user()); + ATF_REQUIRE(timeout == md.timeout()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(override_all_with_set_string); +ATF_TEST_CASE_BODY(override_all_with_set_string) +{ + model::strings_set architectures; + architectures.insert("a1"); + architectures.insert("a2"); + + model::strings_set platforms; + platforms.insert("p1"); + platforms.insert("p2"); + + model::properties_map custom; + custom["user-defined"] = "the-value"; + + const std::string description = "Another long text"; + + model::strings_set configs; + configs.insert("config-var"); + + model::paths_set files; + files.insert(fs::path("plain")); + files.insert(fs::path("/absolute/path")); + + const units::bytes disk_space( + static_cast< uint64_t >(16) * 1024 * 1024 * 1024); + + const units::bytes memory(1024 * 1024); + + model::paths_set programs; + programs.insert(fs::path("program")); + programs.insert(fs::path("/absolute/prog")); + + const std::string user = "unprivileged"; + + const datetime::delta timeout(45, 0); + + const model::metadata md = model::metadata_builder() + .set_string("allowed_architectures", "a1 a2") + .set_string("allowed_platforms", "p1 p2") + .set_string("custom.user-defined", "the-value") + .set_string("description", "Another long text") + .set_string("has_cleanup", "true") + .set_string("is_exclusive", "true") + .set_string("required_configs", "config-var") + .set_string("required_disk_space", "16G") + .set_string("required_files", "plain /absolute/path") + .set_string("required_memory", "1M") + .set_string("required_programs", "program /absolute/prog") + .set_string("required_user", "unprivileged") + .set_string("timeout", "45") + .build(); + + ATF_REQUIRE(architectures == md.allowed_architectures()); + ATF_REQUIRE(platforms == md.allowed_platforms()); + ATF_REQUIRE(custom == md.custom()); + ATF_REQUIRE_EQ(description, md.description()); + ATF_REQUIRE(md.has_cleanup()); + ATF_REQUIRE(md.is_exclusive()); + ATF_REQUIRE(configs == md.required_configs()); + ATF_REQUIRE_EQ(disk_space, md.required_disk_space()); + ATF_REQUIRE(files == md.required_files()); + ATF_REQUIRE_EQ(memory, md.required_memory()); + ATF_REQUIRE(programs == md.required_programs()); + ATF_REQUIRE_EQ(user, md.required_user()); + ATF_REQUIRE(timeout == md.timeout()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_properties); +ATF_TEST_CASE_BODY(to_properties) +{ + const model::metadata md = model::metadata_builder() + .add_allowed_architecture("abc") + .add_required_file(fs::path("foo")) + .add_required_file(fs::path("bar")) + .set_required_memory(units::bytes(1024)) + .add_custom("foo", "bar") + .build(); + + model::properties_map props; + props["allowed_architectures"] = "abc"; + props["allowed_platforms"] = ""; + props["custom.foo"] = "bar"; + props["description"] = ""; + props["has_cleanup"] = "false"; + props["is_exclusive"] = "false"; + props["required_configs"] = ""; + props["required_disk_space"] = "0"; + props["required_files"] = "bar foo"; + props["required_memory"] = "1.00K"; + props["required_programs"] = ""; + props["required_user"] = ""; + props["timeout"] = "300"; + ATF_REQUIRE_EQ(props, md.to_properties()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__empty); +ATF_TEST_CASE_BODY(operators_eq_and_ne__empty) +{ + const model::metadata md1 = model::metadata_builder().build(); + const model::metadata md2 = model::metadata_builder().build(); + ATF_REQUIRE( md1 == md2); + ATF_REQUIRE(!(md1 != md2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__copy) +{ + const model::metadata md1 = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + const model::metadata md2 = md1; + ATF_REQUIRE( md1 == md2); + ATF_REQUIRE(!(md1 != md2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__equal); +ATF_TEST_CASE_BODY(operators_eq_and_ne__equal) +{ + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("a") + .add_allowed_architecture("b") + .add_custom("foo", "bar") + .build(); + const model::metadata md2 = model::metadata_builder() + .add_allowed_architecture("b") + .add_allowed_architecture("a") + .add_custom("foo", "bar") + .build(); + ATF_REQUIRE( md1 == md2); + ATF_REQUIRE(!(md1 != md2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__equal_overriden_defaults); +ATF_TEST_CASE_BODY(operators_eq_and_ne__equal_overriden_defaults) +{ + const model::metadata defaults = model::metadata_builder().build(); + + const model::metadata md1 = model::metadata_builder() + .add_allowed_architecture("a") + .build(); + const model::metadata md2 = model::metadata_builder() + .add_allowed_architecture("a") + .set_timeout(defaults.timeout()) + .build(); + ATF_REQUIRE( md1 == md2); + ATF_REQUIRE(!(md1 != md2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__different); +ATF_TEST_CASE_BODY(operators_eq_and_ne__different) +{ + const model::metadata md1 = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + const model::metadata md2 = model::metadata_builder() + .add_custom("foo", "bar") + .add_custom("baz", "foo bar") + .build(); + ATF_REQUIRE(!(md1 == md2)); + ATF_REQUIRE( md1 != md2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__defaults); +ATF_TEST_CASE_BODY(output__defaults) +{ + std::ostringstream str; + str << model::metadata_builder().build(); + ATF_REQUIRE_EQ("metadata{allowed_architectures='', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='false', " + "required_configs='', " + "required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}", + str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__some_values); +ATF_TEST_CASE_BODY(output__some_values) +{ + std::ostringstream str; + str << model::metadata_builder() + .add_allowed_architecture("abc") + .add_required_file(fs::path("foo")) + .add_required_file(fs::path("bar")) + .set_is_exclusive(true) + .set_required_memory(units::bytes(1024)) + .build(); + ATF_REQUIRE_EQ( + "metadata{allowed_architectures='abc', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='true', " + "required_configs='', " + "required_disk_space='0', required_files='bar foo', " + "required_memory='1.00K', " + "required_programs='', required_user='', timeout='300'}", + str.str()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, defaults); + ATF_ADD_TEST_CASE(tcs, add); + ATF_ADD_TEST_CASE(tcs, copy); + ATF_ADD_TEST_CASE(tcs, apply_overrides); + ATF_ADD_TEST_CASE(tcs, override_all_with_setters); + ATF_ADD_TEST_CASE(tcs, override_all_with_set_string); + ATF_ADD_TEST_CASE(tcs, to_properties); + + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__empty); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__copy); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__equal); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__equal_overriden_defaults); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__different); + + ATF_ADD_TEST_CASE(tcs, output__defaults); + ATF_ADD_TEST_CASE(tcs, output__some_values); + + // TODO(jmmv): Add tests for error conditions (invalid keys and invalid + // values). +} diff --git a/model/test_case.cpp b/model/test_case.cpp new file mode 100644 index 000000000000..f5f6a979eed3 --- /dev/null +++ b/model/test_case.cpp @@ -0,0 +1,339 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/test_case.hpp" + +#include "model/metadata.hpp" +#include "model/test_result.hpp" +#include "utils/format/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +/// Internal implementation for a test_case. +struct model::test_case::impl : utils::noncopyable { + /// Name of the test case; must be unique within the test program. + std::string name; + + /// Metadata of the container test program. + /// + /// Yes, this is a pointer. Yes, we do not own the object pointed to. + /// However, because this is only intended to point to the metadata object + /// of test programs _containing_ this test case, we can assume that the + /// referenced object will be alive for the lifetime of this test case. + const model::metadata* md_defaults; + + /// Test case metadata. + model::metadata md; + + /// Fake result to return instead of running the test case. + optional< model::test_result > fake_result; + + /// Constructor. + /// + /// \param name_ The name of the test case within the test program. + /// \param md_defaults_ Metadata of the container test program. + /// \param md_ Metadata of the test case. + /// \param fake_result_ Fake result to return instead of running the test + /// case. + impl(const std::string& name_, + const model::metadata* md_defaults_, + const model::metadata& md_, + const optional< model::test_result >& fake_result_) : + name(name_), + md_defaults(md_defaults_), + md(md_), + fake_result(fake_result_) + { + } + + /// Gets the test case metadata. + /// + /// This combines the test case's metadata with any possible test program + /// metadata, using the latter as defaults. + /// + /// \return The test case metadata. + model::metadata + get_metadata(void) const + { + if (md_defaults != NULL) { + return md_defaults->apply_overrides(md); + } else { + return md; + } + } + + /// Equality comparator. + /// + /// \param other The other object to compare this one to. + /// + /// \return True if this object and other are equal; false otherwise. + bool + operator==(const impl& other) const + { + return (name == other.name && + get_metadata() == other.get_metadata() && + fake_result == other.fake_result); + } +}; + + +/// Constructs a new test case from an already-built impl oject. +/// +/// \param pimpl_ The internal representation of the test case. +model::test_case::test_case(std::shared_ptr< impl > pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Constructs a new test case. +/// +/// \param name_ The name of the test case within the test program. +/// \param md_ Metadata of the test case. +model::test_case::test_case(const std::string& name_, + const model::metadata& md_) : + _pimpl(new impl(name_, NULL, md_, none)) +{ +} + + + +/// Constructs a new fake test case. +/// +/// A fake test case is a test case that is not really defined by the test +/// program. Such test cases have a name surrounded by '__' and, when executed, +/// they return a fixed, pre-recorded result. +/// +/// This is necessary for the cases where listing the test cases of a test +/// program fails. In this scenario, we generate a single test case within +/// the test program that unconditionally returns a failure. +/// +/// TODO(jmmv): Need to get rid of this. We should be able to report the +/// status of test programs independently of test cases, as some interfaces +/// don't know about the latter at all. +/// +/// \param name_ The name to give to this fake test case. This name has to be +/// prefixed and suffixed by '__' to clearly denote that this is internal. +/// \param description_ The description of the test case, if any. +/// \param test_result_ The fake result to return when this test case is run. +model::test_case::test_case( + const std::string& name_, + const std::string& description_, + const model::test_result& test_result_) : + _pimpl(new impl( + name_, + NULL, + model::metadata_builder().set_description(description_).build(), + utils::make_optional(test_result_))) +{ + PRE_MSG(name_.length() > 4 && name_.substr(0, 2) == "__" && + name_.substr(name_.length() - 2) == "__", + "Invalid fake name provided to fake test case"); +} + + +/// Destroys a test case. +model::test_case::~test_case(void) +{ +} + + +/// Constructs a new test case applying metadata defaults. +/// +/// This method is intended to be used by the container test program when +/// ownership of the test is given to it. At that point, the test case receives +/// the default metadata properties of the test program, not the global +/// defaults. +/// +/// \param defaults The metadata properties to use as defaults. The provided +/// object's lifetime MUST extend the lifetime of the test case. Because +/// this is only intended to point at the metadata of the test program +/// containing this test case, this assumption should hold. +/// +/// \return A new test case. +model::test_case +model::test_case::apply_metadata_defaults(const metadata* defaults) const +{ + return test_case(std::shared_ptr< impl >(new impl( + _pimpl->name, + defaults, + _pimpl->md, + _pimpl->fake_result))); +} + + +/// Gets the test case name. +/// +/// \return The test case name, relative to the test program. +const std::string& +model::test_case::name(void) const +{ + return _pimpl->name; +} + + +/// Gets the test case metadata. +/// +/// This combines the test case's metadata with any possible test program +/// metadata, using the latter as defaults. You should use this method in +/// generaland not get_raw_metadata(). +/// +/// \return The test case metadata. +model::metadata +model::test_case::get_metadata(void) const +{ + return _pimpl->get_metadata(); +} + + +/// Gets the original test case metadata without test program overrides. +/// +/// This method should be used for storage purposes as serialized test cases +/// should record exactly whatever the test case reported and not what the test +/// program may have provided. The final values will be reconstructed at load +/// time. +/// +/// \return The test case metadata. +const model::metadata& +model::test_case::get_raw_metadata(void) const +{ + return _pimpl->md; +} + + +/// Gets the fake result pre-stored for this test case. +/// +/// \return A fake result, or none if not defined. +optional< model::test_result > +model::test_case::fake_result(void) const +{ + return _pimpl->fake_result; +} + + +/// Equality comparator. +/// +/// \warning Because test cases reference their container test programs, and +/// test programs include test cases, we cannot perform a full comparison here: +/// otherwise, we'd enter an inifinte loop. Therefore, out of necessity, this +/// does NOT compare whether the container test programs of the affected test +/// cases are the same. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +bool +model::test_case::operator==(const test_case& other) const +{ + return _pimpl == other._pimpl || *_pimpl == *other._pimpl; +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +model::test_case::operator!=(const test_case& other) const +{ + return !(*this == other); +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +model::operator<<(std::ostream& output, const test_case& object) +{ + output << F("test_case{name=%s, metadata=%s}") + % text::quote(object.name(), '\'') + % object.get_metadata(); + return output; +} + + +/// Adds an already-constructed test case. +/// +/// \param test_case The test case to add. +/// +/// \return A reference to this builder. +model::test_cases_map_builder& +model::test_cases_map_builder::add(const test_case& test_case) +{ + _test_cases.insert( + test_cases_map::value_type(test_case.name(), test_case)); + return *this; +} + + +/// Constructs and adds a new test case with default metadata. +/// +/// \param test_case_name The name of the test case to add. +/// +/// \return A reference to this builder. +model::test_cases_map_builder& +model::test_cases_map_builder::add(const std::string& test_case_name) +{ + return add(test_case(test_case_name, metadata_builder().build())); +} + + +/// Constructs and adds a new test case with explicit metadata. +/// +/// \param test_case_name The name of the test case to add. +/// \param metadata The metadata of the test case. +/// +/// \return A reference to this builder. +model::test_cases_map_builder& +model::test_cases_map_builder::add(const std::string& test_case_name, + const metadata& metadata) +{ + return add(test_case(test_case_name, metadata)); +} + + +/// Creates a new test_cases_map. +/// +/// \return The constructed test_cases_map. +model::test_cases_map +model::test_cases_map_builder::build(void) const +{ + return _test_cases; +} diff --git a/model/test_case.hpp b/model/test_case.hpp new file mode 100644 index 000000000000..3c6fe32c8e62 --- /dev/null +++ b/model/test_case.hpp @@ -0,0 +1,98 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/test_case.hpp +/// Definition of the "test case" concept. + +#if !defined(MODEL_TEST_CASE_HPP) +#define MODEL_TEST_CASE_HPP + +#include "model/test_case_fwd.hpp" + +#include +#include +#include + +#include "model/metadata_fwd.hpp" +#include "model/test_result_fwd.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional_fwd.hpp" + +namespace model { + + +/// Representation of a test case. +/// +/// Test cases, on their own, are useless. They only make sense in the context +/// of the container test program and as such this class should not be used +/// directly. +class test_case { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + test_case(std::shared_ptr< impl >); + +public: + test_case(const std::string&, const metadata&); + test_case(const std::string&, const std::string&, const test_result&); + ~test_case(void); + + test_case apply_metadata_defaults(const metadata*) const; + + const std::string& name(void) const; + metadata get_metadata(void) const; + const metadata& get_raw_metadata(void) const; + utils::optional< test_result > fake_result(void) const; + + bool operator==(const test_case&) const; + bool operator!=(const test_case&) const; +}; + + +/// Builder for a test_cases_map object. +class test_cases_map_builder : utils::noncopyable { + /// Accumulator for the map being built. + test_cases_map _test_cases; + +public: + test_cases_map_builder& add(const test_case&); + test_cases_map_builder& add(const std::string&); + test_cases_map_builder& add(const std::string&, const metadata&); + + test_cases_map build(void) const; +}; + + +std::ostream& operator<<(std::ostream&, const test_case&); + + +} // namespace model + +#endif // !defined(MODEL_TEST_CASE_HPP) diff --git a/model/test_case_fwd.hpp b/model/test_case_fwd.hpp new file mode 100644 index 000000000000..72a40e513083 --- /dev/null +++ b/model/test_case_fwd.hpp @@ -0,0 +1,51 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/test_case_fwd.hpp +/// Forward declarations for model/test_case.hpp + +#if !defined(MODEL_TEST_CASE_FWD_HPP) +#define MODEL_TEST_CASE_FWD_HPP + +#include +#include + +namespace model { + + +class test_case; +class test_cases_map_builder; + + +/// Collection of test cases keyed by their name. +typedef std::map< std::string, model::test_case > test_cases_map; + + +} // namespace model + +#endif // !defined(MODEL_TEST_CASE_FWD_HPP) diff --git a/model/test_case_test.cpp b/model/test_case_test.cpp new file mode 100644 index 000000000000..1a55de0fab42 --- /dev/null +++ b/model/test_case_test.cpp @@ -0,0 +1,263 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/test_case.hpp" + +#include + +#include + +#include "model/metadata.hpp" +#include "model/test_result.hpp" +#include "utils/datetime.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__ctor_and_getters) +ATF_TEST_CASE_BODY(test_case__ctor_and_getters) +{ + const model::metadata md = model::metadata_builder() + .add_custom("first", "value") + .build(); + const model::test_case test_case("foo", md); + ATF_REQUIRE_EQ("foo", test_case.name()); + ATF_REQUIRE_EQ(md, test_case.get_metadata()); + ATF_REQUIRE_EQ(md, test_case.get_raw_metadata()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__fake_result) +ATF_TEST_CASE_BODY(test_case__fake_result) +{ + const model::test_result result(model::test_result_skipped, + "Some reason"); + const model::test_case test_case("__foo__", "Some description", result); + ATF_REQUIRE_EQ("__foo__", test_case.name()); + ATF_REQUIRE_EQ(result, test_case.fake_result().get()); + + const model::metadata exp_metadata = model::metadata_builder() + .set_description("Some description") + .build(); + ATF_REQUIRE_EQ(exp_metadata, test_case.get_metadata()); + ATF_REQUIRE_EQ(exp_metadata, test_case.get_raw_metadata()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__apply_metadata_overrides__real_test_case) +ATF_TEST_CASE_BODY(test_case__apply_metadata_overrides__real_test_case) +{ + const model::metadata overrides = model::metadata_builder() + .add_required_config("the-variable") + .set_description("The test case") + .build(); + const model::test_case base_test_case("foo", overrides); + + const model::metadata defaults = model::metadata_builder() + .set_description("Default description") + .set_timeout(datetime::delta(10, 0)) + .build(); + + const model::test_case test_case = base_test_case.apply_metadata_defaults( + &defaults); + + const model::metadata expected = model::metadata_builder() + .add_required_config("the-variable") + .set_description("The test case") + .set_timeout(datetime::delta(10, 0)) + .build(); + ATF_REQUIRE_EQ(expected, test_case.get_metadata()); + ATF_REQUIRE_EQ(overrides, test_case.get_raw_metadata()); + + // Ensure the original (although immutable) test case was not touched. + ATF_REQUIRE_EQ(overrides, base_test_case.get_metadata()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__apply_metadata_overrides__fake_test_case) +ATF_TEST_CASE_BODY(test_case__apply_metadata_overrides__fake_test_case) +{ + const model::test_result result(model::test_result_skipped, "Irrelevant"); + const model::test_case base_test_case("__foo__", "Fake test", result); + + const model::metadata overrides = model::metadata_builder() + .set_description("Fake test") + .build(); + + const model::metadata defaults = model::metadata_builder() + .add_allowed_platform("some-value") + .set_description("Default description") + .build(); + + const model::test_case test_case = base_test_case.apply_metadata_defaults( + &defaults); + + const model::metadata expected = model::metadata_builder() + .add_allowed_platform("some-value") + .set_description("Fake test") + .build(); + ATF_REQUIRE_EQ(expected, test_case.get_metadata()); + ATF_REQUIRE_EQ(overrides, test_case.get_raw_metadata()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__operators_eq_and_ne__copy); +ATF_TEST_CASE_BODY(test_case__operators_eq_and_ne__copy) +{ + const model::test_case tc1("name", model::metadata_builder().build()); + const model::test_case tc2 = tc1; + ATF_REQUIRE( tc1 == tc2); + ATF_REQUIRE(!(tc1 != tc2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__operators_eq_and_ne__not_copy); +ATF_TEST_CASE_BODY(test_case__operators_eq_and_ne__not_copy) +{ + const std::string base_name("name"); + const model::metadata base_metadata = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + + const model::test_case base_tc(base_name, base_metadata); + + // Construct with all same values. + { + const model::test_case other_tc(base_name, base_metadata); + + ATF_REQUIRE( base_tc == other_tc); + ATF_REQUIRE(!(base_tc != other_tc)); + } + + // Construct with all same values but different metadata objects. + { + const model::metadata other_metadata = model::metadata_builder() + .add_custom("foo", "bar") + .set_timeout(base_metadata.timeout()) + .build(); + const model::test_case other_tc(base_name, other_metadata); + + ATF_REQUIRE( base_tc == other_tc); + ATF_REQUIRE(!(base_tc != other_tc)); + } + + // Different name. + { + const model::test_case other_tc("other", base_metadata); + + ATF_REQUIRE(!(base_tc == other_tc)); + ATF_REQUIRE( base_tc != other_tc); + } + + // Different metadata. + { + const model::test_case other_tc(base_name, + model::metadata_builder().build()); + + ATF_REQUIRE(!(base_tc == other_tc)); + ATF_REQUIRE( base_tc != other_tc); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_case__output); +ATF_TEST_CASE_BODY(test_case__output) +{ + const model::test_case tc1( + "the-name", model::metadata_builder() + .add_allowed_platform("foo").add_custom("bar", "baz").build()); + std::ostringstream str; + str << tc1; + ATF_REQUIRE_EQ( + "test_case{name='the-name', " + "metadata=metadata{allowed_architectures='', allowed_platforms='foo', " + "custom.bar='baz', description='', has_cleanup='false', " + "is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}}", + str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_cases_map__builder); +ATF_TEST_CASE_BODY(test_cases_map__builder) +{ + model::test_cases_map_builder builder; + model::test_cases_map exp_test_cases; + + ATF_REQUIRE_EQ(exp_test_cases, builder.build()); + + builder.add("default-metadata"); + { + const model::test_case tc1("default-metadata", + model::metadata_builder().build()); + exp_test_cases.insert( + model::test_cases_map::value_type(tc1.name(), tc1)); + } + ATF_REQUIRE_EQ(exp_test_cases, builder.build()); + + builder.add("with-metadata", + model::metadata_builder().set_description("text").build()); + { + const model::test_case tc1("with-metadata", + model::metadata_builder() + .set_description("text").build()); + exp_test_cases.insert( + model::test_cases_map::value_type(tc1.name(), tc1)); + } + ATF_REQUIRE_EQ(exp_test_cases, builder.build()); + + const model::test_case tc1("fully_built", + model::metadata_builder() + .set_description("something else").build()); + builder.add(tc1); + exp_test_cases.insert(model::test_cases_map::value_type(tc1.name(), tc1)); + ATF_REQUIRE_EQ(exp_test_cases, builder.build()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, test_case__ctor_and_getters); + ATF_ADD_TEST_CASE(tcs, test_case__fake_result); + + ATF_ADD_TEST_CASE(tcs, test_case__apply_metadata_overrides__real_test_case); + ATF_ADD_TEST_CASE(tcs, test_case__apply_metadata_overrides__fake_test_case); + + ATF_ADD_TEST_CASE(tcs, test_case__operators_eq_and_ne__copy); + ATF_ADD_TEST_CASE(tcs, test_case__operators_eq_and_ne__not_copy); + + ATF_ADD_TEST_CASE(tcs, test_case__output); + + ATF_ADD_TEST_CASE(tcs, test_cases_map__builder); +} diff --git a/model/test_program.cpp b/model/test_program.cpp new file mode 100644 index 000000000000..b9181aa49537 --- /dev/null +++ b/model/test_program.cpp @@ -0,0 +1,452 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/test_program.hpp" + +#include +#include + +#include "model/exceptions.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_result.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace fs = utils::fs; +namespace text = utils::text; + +using utils::none; + + +/// Internal implementation of a test_program. +struct model::test_program::impl : utils::noncopyable { + /// Name of the test program interface. + std::string interface_name; + + /// Name of the test program binary relative to root. + fs::path binary; + + /// Root of the test suite containing the test program. + fs::path root; + + /// Name of the test suite this program belongs to. + std::string test_suite_name; + + /// Metadata of the test program. + model::metadata md; + + /// List of test cases in the test program. + /// + /// Must be queried via the test_program::test_cases() method. + model::test_cases_map test_cases; + + /// Constructor. + /// + /// \param interface_name_ Name of the test program interface. + /// \param binary_ The name of the test program binary relative to root_. + /// \param root_ The root of the test suite containing the test program. + /// \param test_suite_name_ The name of the test suite this program + /// belongs to. + /// \param md_ Metadata of the test program. + /// \param test_cases_ The collection of test cases in the test program. + impl(const std::string& interface_name_, const fs::path& binary_, + const fs::path& root_, const std::string& test_suite_name_, + const model::metadata& md_, const model::test_cases_map& test_cases_) : + interface_name(interface_name_), + binary(binary_), + root(root_), + test_suite_name(test_suite_name_), + md(md_) + { + PRE_MSG(!binary.is_absolute(), + F("The program '%s' must be relative to the root of the test " + "suite '%s'") % binary % root); + + set_test_cases(test_cases_); + } + + /// Sets the list of test cases of the test program. + /// + /// \param test_cases_ The new list of test cases. + void + set_test_cases(const model::test_cases_map& test_cases_) + { + for (model::test_cases_map::const_iterator iter = test_cases_.begin(); + iter != test_cases_.end(); ++iter) { + const std::string& name = (*iter).first; + const model::test_case& test_case = (*iter).second; + + PRE_MSG(name == test_case.name(), + F("The test case '%s' has been registered with the " + "non-matching name '%s'") % name % test_case.name()); + + test_cases.insert(model::test_cases_map::value_type( + name, test_case.apply_metadata_defaults(&md))); + } + INV(test_cases.size() == test_cases_.size()); + } +}; + + +/// Constructs a new test program. +/// +/// \param interface_name_ Name of the test program interface. +/// \param binary_ The name of the test program binary relative to root_. +/// \param root_ The root of the test suite containing the test program. +/// \param test_suite_name_ The name of the test suite this program belongs to. +/// \param md_ Metadata of the test program. +/// \param test_cases_ The collection of test cases in the test program. +model::test_program::test_program(const std::string& interface_name_, + const fs::path& binary_, + const fs::path& root_, + const std::string& test_suite_name_, + const model::metadata& md_, + const model::test_cases_map& test_cases_) : + _pimpl(new impl(interface_name_, binary_, root_, test_suite_name_, md_, + test_cases_)) +{ +} + + +/// Destroys a test program. +model::test_program::~test_program(void) +{ +} + + +/// Gets the name of the test program interface. +/// +/// \return An interface name. +const std::string& +model::test_program::interface_name(void) const +{ + return _pimpl->interface_name; +} + + +/// Gets the path to the test program relative to the root of the test suite. +/// +/// \return The relative path to the test program binary. +const fs::path& +model::test_program::relative_path(void) const +{ + return _pimpl->binary; +} + + +/// Gets the absolute path to the test program. +/// +/// \return The absolute path to the test program binary. +const fs::path +model::test_program::absolute_path(void) const +{ + const fs::path full_path = _pimpl->root / _pimpl->binary; + return full_path.is_absolute() ? full_path : full_path.to_absolute(); +} + + +/// Gets the root of the test suite containing this test program. +/// +/// \return The path to the root of the test suite. +const fs::path& +model::test_program::root(void) const +{ + return _pimpl->root; +} + + +/// Gets the name of the test suite containing this test program. +/// +/// \return The name of the test suite. +const std::string& +model::test_program::test_suite_name(void) const +{ + return _pimpl->test_suite_name; +} + + +/// Gets the metadata of the test program. +/// +/// \return The metadata. +const model::metadata& +model::test_program::get_metadata(void) const +{ + return _pimpl->md; +} + + +/// Gets a test case by its name. +/// +/// \param name The name of the test case to locate. +/// +/// \return The requested test case. +/// +/// \throw not_found_error If the specified test case is not in the test +/// program. +const model::test_case& +model::test_program::find(const std::string& name) const +{ + const test_cases_map& tcs = test_cases(); + + const test_cases_map::const_iterator iter = tcs.find(name); + if (iter == tcs.end()) + throw not_found_error(F("Unknown test case %s in test program %s") % + name % relative_path()); + return (*iter).second; +} + + +/// Gets the list of test cases from the test program. +/// +/// \return The list of test cases provided by the test program. +const model::test_cases_map& +model::test_program::test_cases(void) const +{ + return _pimpl->test_cases; +} + + +/// Sets the list of test cases of the test program. +/// +/// This can only be called once and it may only be called from within +/// overridden test_cases() before that method ever returns a value for the +/// first time. Any other invocations will result in inconsistent program +/// state. +/// +/// \param test_cases_ The new list of test cases. +void +model::test_program::set_test_cases(const model::test_cases_map& test_cases_) +{ + PRE(_pimpl->test_cases.empty()); + _pimpl->set_test_cases(test_cases_); +} + + +/// Equality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +bool +model::test_program::operator==(const test_program& other) const +{ + return _pimpl == other._pimpl || ( + _pimpl->interface_name == other._pimpl->interface_name && + _pimpl->binary == other._pimpl->binary && + _pimpl->root == other._pimpl->root && + _pimpl->test_suite_name == other._pimpl->test_suite_name && + _pimpl->md == other._pimpl->md && + test_cases() == other.test_cases()); +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +model::test_program::operator!=(const test_program& other) const +{ + return !(*this == other); +} + + +/// Less-than comparator. +/// +/// A test program is considered to be less than another if and only if the +/// former's absolute path is less than the absolute path of the latter. In +/// other words, the absolute path is used here as the test program's +/// identifier. +/// +/// This simplistic less-than operator overload is provided so that test +/// programs can be held in sets and other containers. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object sorts before the other object; false otherwise. +bool +model::test_program::operator<(const test_program& other) const +{ + return absolute_path() < other.absolute_path(); +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +model::operator<<(std::ostream& output, const test_program& object) +{ + output << F("test_program{interface=%s, binary=%s, root=%s, test_suite=%s, " + "metadata=%s, test_cases=%s}") + % text::quote(object.interface_name(), '\'') + % text::quote(object.relative_path().str(), '\'') + % text::quote(object.root().str(), '\'') + % text::quote(object.test_suite_name(), '\'') + % object.get_metadata() + % object.test_cases(); + return output; +} + + +/// Internal implementation of the test_program_builder class. +struct model::test_program_builder::impl : utils::noncopyable { + /// Partially-constructed program with only the required properties. + model::test_program prototype; + + /// Optional metadata for the test program. + model::metadata metadata; + + /// Collection of test cases. + model::test_cases_map test_cases; + + /// Whether we have created a test_program object or not. + bool built; + + /// Constructor. + /// + /// \param prototype_ The partially constructed program with only the + /// required properties. + impl(const model::test_program& prototype_) : + prototype(prototype_), + metadata(model::metadata_builder().build()), + built(false) + { + } +}; + + +/// Constructs a new builder with non-optional values. +/// +/// \param interface_name_ Name of the test program interface. +/// \param binary_ The name of the test program binary relative to root_. +/// \param root_ The root of the test suite containing the test program. +/// \param test_suite_name_ The name of the test suite this program belongs to. +model::test_program_builder::test_program_builder( + const std::string& interface_name_, const fs::path& binary_, + const fs::path& root_, const std::string& test_suite_name_) : + _pimpl(new impl(model::test_program(interface_name_, binary_, root_, + test_suite_name_, + model::metadata_builder().build(), + model::test_cases_map()))) +{ +} + + +/// Destructor. +model::test_program_builder::~test_program_builder(void) +{ +} + + +/// Accumulates an additional test case with default metadata. +/// +/// \param test_case_name The name of the test case. +/// +/// \return A reference to this builder. +model::test_program_builder& +model::test_program_builder::add_test_case(const std::string& test_case_name) +{ + return add_test_case(test_case_name, model::metadata_builder().build()); +} + + +/// Accumulates an additional test case. +/// +/// \param test_case_name The name of the test case. +/// \param metadata The test case metadata. +/// +/// \return A reference to this builder. +model::test_program_builder& +model::test_program_builder::add_test_case(const std::string& test_case_name, + const model::metadata& metadata) +{ + const model::test_case test_case(test_case_name, metadata); + PRE_MSG(_pimpl->test_cases.find(test_case_name) == _pimpl->test_cases.end(), + F("Attempted to re-register test case '%s'") % test_case_name); + _pimpl->test_cases.insert(model::test_cases_map::value_type( + test_case_name, test_case)); + return *this; +} + + +/// Sets the test program metadata. +/// +/// \return metadata The metadata for the test program. +/// +/// \return A reference to this builder. +model::test_program_builder& +model::test_program_builder::set_metadata(const model::metadata& metadata) +{ + _pimpl->metadata = metadata; + return *this; +} + + +/// Creates a new test_program object. +/// +/// \pre This has not yet been called. We only support calling this function +/// once. +/// +/// \return The constructed test_program object. +model::test_program +model::test_program_builder::build(void) const +{ + PRE(!_pimpl->built); + _pimpl->built = true; + + return test_program(_pimpl->prototype.interface_name(), + _pimpl->prototype.relative_path(), + _pimpl->prototype.root(), + _pimpl->prototype.test_suite_name(), + _pimpl->metadata, + _pimpl->test_cases); +} + + +/// Creates a new dynamically-allocated test_program object. +/// +/// \pre This has not yet been called. We only support calling this function +/// once. +/// +/// \return The constructed test_program object. +model::test_program_ptr +model::test_program_builder::build_ptr(void) const +{ + const test_program result = build(); + return test_program_ptr(new test_program(result)); +} diff --git a/model/test_program.hpp b/model/test_program.hpp new file mode 100644 index 000000000000..974ec2a12d19 --- /dev/null +++ b/model/test_program.hpp @@ -0,0 +1,110 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/test_program.hpp +/// Definition of the "test program" concept. + +#if !defined(MODEL_TEST_PROGRAM_HPP) +#define MODEL_TEST_PROGRAM_HPP + +#include "model/test_program_fwd.hpp" + +#include +#include +#include +#include + +#include "model/metadata_fwd.hpp" +#include "model/test_case_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/noncopyable.hpp" + +namespace model { + + +/// Representation of a test program. +class test_program { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + +protected: + void set_test_cases(const model::test_cases_map&); + +public: + test_program(const std::string&, const utils::fs::path&, + const utils::fs::path&, const std::string&, + const model::metadata&, const model::test_cases_map&); + virtual ~test_program(void); + + const std::string& interface_name(void) const; + const utils::fs::path& root(void) const; + const utils::fs::path& relative_path(void) const; + const utils::fs::path absolute_path(void) const; + const std::string& test_suite_name(void) const; + const model::metadata& get_metadata(void) const; + + const model::test_case& find(const std::string&) const; + virtual const model::test_cases_map& test_cases(void) const; + + bool operator==(const test_program&) const; + bool operator!=(const test_program&) const; + bool operator<(const test_program&) const; +}; + + +std::ostream& operator<<(std::ostream&, const test_program&); + + +/// Builder for a test_program object. +class test_program_builder : utils::noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +public: + test_program_builder(const std::string&, const utils::fs::path&, + const utils::fs::path&, const std::string&); + ~test_program_builder(void); + + test_program_builder& add_test_case(const std::string&); + test_program_builder& add_test_case(const std::string&, + const model::metadata&); + + test_program_builder& set_metadata(const model::metadata&); + + test_program build(void) const; + test_program_ptr build_ptr(void) const; +}; + + +} // namespace model + +#endif // !defined(MODEL_TEST_PROGRAM_HPP) diff --git a/model/test_program_fwd.hpp b/model/test_program_fwd.hpp new file mode 100644 index 000000000000..100f017c30a6 --- /dev/null +++ b/model/test_program_fwd.hpp @@ -0,0 +1,55 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/test_program_fwd.hpp +/// Forward declarations for model/test_program.hpp + +#if !defined(MODEL_TEST_PROGRAM_FWD_HPP) +#define MODEL_TEST_PROGRAM_FWD_HPP + +#include +#include + + +namespace model { + + +class test_program; + + +/// Pointer to a test program. +typedef std::shared_ptr< test_program > test_program_ptr; + + +/// Collection of test programs. +typedef std::vector< test_program_ptr > test_programs_vector; + + +} // namespace model + +#endif // !defined(MODEL_TEST_PROGRAM_FWD_HPP) diff --git a/model/test_program_test.cpp b/model/test_program_test.cpp new file mode 100644 index 000000000000..f9a8f7e59da3 --- /dev/null +++ b/model/test_program_test.cpp @@ -0,0 +1,711 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/test_program.hpp" + +extern "C" { +#include + +#include +} + +#include +#include + +#include + +#include "model/exceptions.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_result.hpp" +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + +namespace fs = utils::fs; + + +namespace { + + +/// Test program that sets its test cases lazily. +/// +/// This test class exists to test the behavior of a test_program object when +/// the class is extended to offer lazy loading of test cases. We simulate such +/// lazy loading here by storing the list of test cases aside at construction +/// time and later setting it lazily the first time test_cases() is called. +class lazy_test_program : public model::test_program { + /// Whether set_test_cases() has yet been called or not. + mutable bool _set_test_cases_called; + + /// The list of test cases for this test program. + /// + /// Only use this in the call to set_test_cases(). All other reads of the + /// test cases list should happen via the parent class' test_cases() method. + model::test_cases_map _lazy_test_cases; + +public: + /// Constructs a new test program. + /// + /// \param interface_name_ Name of the test program interface. + /// \param binary_ The name of the test program binary relative to root_. + /// \param root_ The root of the test suite containing the test program. + /// \param test_suite_name_ The name of the test suite. + /// \param metadata_ Metadata of the test program. + /// \param test_cases_ The collection of test cases in the test program. + lazy_test_program(const std::string& interface_name_, + const utils::fs::path& binary_, + const utils::fs::path& root_, + const std::string& test_suite_name_, + const model::metadata& metadata_, + const model::test_cases_map& test_cases_) : + test_program(interface_name_, binary_, root_, test_suite_name_, + metadata_, model::test_cases_map()), + _set_test_cases_called(false), + _lazy_test_cases(test_cases_) + { + } + + /// Lazily sets the test cases on the parent and returns them. + /// + /// \return The list of test cases. + const model::test_cases_map& + test_cases(void) const + { + if (!_set_test_cases_called) { + const_cast< lazy_test_program* >(this)->set_test_cases( + _lazy_test_cases); + _set_test_cases_called = true; + } + return test_program::test_cases(); + } +}; + + +} // anonymous namespace + + +/// Runs a ctor_and_getters test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_ctor_and_getters(void) +{ + const model::metadata tp_md = model::metadata_builder() + .add_custom("first", "foo") + .add_custom("second", "bar") + .build(); + const model::metadata tc_md = model::metadata_builder() + .add_custom("first", "baz") + .build(); + + const TestProgram test_program( + "mock", fs::path("binary"), fs::path("root"), "suite-name", tp_md, + model::test_cases_map_builder().add("foo", tc_md).build()); + + + ATF_REQUIRE_EQ("mock", test_program.interface_name()); + ATF_REQUIRE_EQ(fs::path("binary"), test_program.relative_path()); + ATF_REQUIRE_EQ(fs::current_path() / "root/binary", + test_program.absolute_path()); + ATF_REQUIRE_EQ(fs::path("root"), test_program.root()); + ATF_REQUIRE_EQ("suite-name", test_program.test_suite_name()); + ATF_REQUIRE_EQ(tp_md, test_program.get_metadata()); + + const model::metadata exp_tc_md = model::metadata_builder() + .add_custom("first", "baz") + .add_custom("second", "bar") + .build(); + const model::test_cases_map exp_tcs = model::test_cases_map_builder() + .add("foo", exp_tc_md) + .build(); + ATF_REQUIRE_EQ(exp_tcs, test_program.test_cases()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ctor_and_getters); +ATF_TEST_CASE_BODY(ctor_and_getters) +{ + check_ctor_and_getters< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__ctor_and_getters); +ATF_TEST_CASE_BODY(derived__ctor_and_getters) +{ + check_ctor_and_getters< lazy_test_program >(); +} + + +/// Runs a find_ok test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_find_ok(void) +{ + const model::test_case test_case("main", model::metadata_builder().build()); + + const TestProgram test_program( + "mock", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), + model::test_cases_map_builder().add(test_case).build()); + + const model::test_case& found_test_case = test_program.find("main"); + ATF_REQUIRE_EQ(test_case, found_test_case); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find__ok); +ATF_TEST_CASE_BODY(find__ok) +{ + check_find_ok< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__find__ok); +ATF_TEST_CASE_BODY(derived__find__ok) +{ + check_find_ok< lazy_test_program >(); +} + + +/// Runs a find_missing test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_find_missing(void) +{ + const TestProgram test_program( + "mock", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), + model::test_cases_map_builder().add("main").build()); + + ATF_REQUIRE_THROW_RE(model::not_found_error, + "case.*abc.*program.*non-existent", + test_program.find("abc")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find__missing); +ATF_TEST_CASE_BODY(find__missing) +{ + check_find_missing< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__find__missing); +ATF_TEST_CASE_BODY(derived__find__missing) +{ + check_find_missing< lazy_test_program >(); +} + + +/// Runs a metadata_inheritance test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_metadata_inheritance(void) +{ + const model::test_cases_map test_cases = model::test_cases_map_builder() + .add("inherit-all") + .add("inherit-some", + model::metadata_builder() + .set_description("Overriden description") + .build()) + .add("inherit-none", + model::metadata_builder() + .add_allowed_architecture("overriden-arch") + .add_allowed_platform("overriden-platform") + .set_description("Overriden description") + .build()) + .build(); + + const model::metadata metadata = model::metadata_builder() + .add_allowed_architecture("base-arch") + .set_description("Base description") + .build(); + const TestProgram test_program( + "plain", fs::path("non-existent"), fs::path("."), "suite-name", + metadata, test_cases); + + { + const model::metadata exp_metadata = model::metadata_builder() + .add_allowed_architecture("base-arch") + .set_description("Base description") + .build(); + ATF_REQUIRE_EQ(exp_metadata, + test_program.find("inherit-all").get_metadata()); + } + + { + const model::metadata exp_metadata = model::metadata_builder() + .add_allowed_architecture("base-arch") + .set_description("Overriden description") + .build(); + ATF_REQUIRE_EQ(exp_metadata, + test_program.find("inherit-some").get_metadata()); + } + + { + const model::metadata exp_metadata = model::metadata_builder() + .add_allowed_architecture("overriden-arch") + .add_allowed_platform("overriden-platform") + .set_description("Overriden description") + .build(); + ATF_REQUIRE_EQ(exp_metadata, + test_program.find("inherit-none").get_metadata()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(metadata_inheritance); +ATF_TEST_CASE_BODY(metadata_inheritance) +{ + check_metadata_inheritance< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__metadata_inheritance); +ATF_TEST_CASE_BODY(derived__metadata_inheritance) +{ + check_metadata_inheritance< lazy_test_program >(); +} + + +/// Runs a operators_eq_and_ne__copy test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_operators_eq_and_ne__copy(void) +{ + const TestProgram tp1( + "plain", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), + model::test_cases_map()); + const TestProgram tp2 = tp1; + ATF_REQUIRE( tp1 == tp2); + ATF_REQUIRE(!(tp1 != tp2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__copy) +{ + check_operators_eq_and_ne__copy< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__operators_eq_and_ne__copy); +ATF_TEST_CASE_BODY(derived__operators_eq_and_ne__copy) +{ + check_operators_eq_and_ne__copy< lazy_test_program >(); +} + + +/// Runs a operators_eq_and_ne__not_copy test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_operators_eq_and_ne__not_copy(void) +{ + const std::string base_interface("plain"); + const fs::path base_relative_path("the/test/program"); + const fs::path base_root("/the/root"); + const std::string base_test_suite("suite-name"); + const model::metadata base_metadata = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + + const model::test_cases_map base_tcs = model::test_cases_map_builder() + .add("main", model::metadata_builder() + .add_custom("second", "baz") + .build()) + .build(); + + const TestProgram base_tp( + base_interface, base_relative_path, base_root, base_test_suite, + base_metadata, base_tcs); + + // Construct with all same values. + { + const model::test_cases_map other_tcs = model::test_cases_map_builder() + .add("main", model::metadata_builder() + .add_custom("second", "baz") + .build()) + .build(); + + const TestProgram other_tp( + base_interface, base_relative_path, base_root, base_test_suite, + base_metadata, other_tcs); + + ATF_REQUIRE( base_tp == other_tp); + ATF_REQUIRE(!(base_tp != other_tp)); + } + + // Construct with same final metadata values but using a different + // intermediate representation. The original test program has one property + // in the base test program definition and another in the test case; here, + // we put both definitions explicitly in the test case. + { + const model::test_cases_map other_tcs = model::test_cases_map_builder() + .add("main", model::metadata_builder() + .add_custom("foo", "bar") + .add_custom("second", "baz") + .build()) + .build(); + + const TestProgram other_tp( + base_interface, base_relative_path, base_root, base_test_suite, + base_metadata, other_tcs); + + ATF_REQUIRE( base_tp == other_tp); + ATF_REQUIRE(!(base_tp != other_tp)); + } + + // Different interface. + { + const TestProgram other_tp( + "atf", base_relative_path, base_root, base_test_suite, + base_metadata, base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different relative path. + { + const TestProgram other_tp( + base_interface, fs::path("a/b/c"), base_root, base_test_suite, + base_metadata, base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different root. + { + const TestProgram other_tp( + base_interface, base_relative_path, fs::path("."), base_test_suite, + base_metadata, base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different test suite. + { + const TestProgram other_tp( + base_interface, base_relative_path, base_root, "different-suite", + base_metadata, base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different metadata. + { + const TestProgram other_tp( + base_interface, base_relative_path, base_root, base_test_suite, + model::metadata_builder().build(), base_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } + + // Different test cases. + { + const model::test_cases_map other_tcs = model::test_cases_map_builder() + .add("foo").build(); + + const TestProgram other_tp( + base_interface, base_relative_path, base_root, base_test_suite, + base_metadata, other_tcs); + + ATF_REQUIRE(!(base_tp == other_tp)); + ATF_REQUIRE( base_tp != other_tp); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__not_copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__not_copy) +{ + check_operators_eq_and_ne__not_copy< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__operators_eq_and_ne__not_copy); +ATF_TEST_CASE_BODY(derived__operators_eq_and_ne__not_copy) +{ + check_operators_eq_and_ne__not_copy< lazy_test_program >(); +} + + +/// Runs a operator_lt test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_operator_lt(void) +{ + const TestProgram tp1( + "plain", fs::path("a/b/c"), fs::path("/foo/bar"), "suite-name", + model::metadata_builder().build(), + model::test_cases_map()); + const TestProgram tp2( + "atf", fs::path("c"), fs::path("/foo/bar"), "suite-name", + model::metadata_builder().build(), + model::test_cases_map()); + const TestProgram tp3( + "plain", fs::path("a/b/c"), fs::path("/abc"), "suite-name", + model::metadata_builder().build(), + model::test_cases_map()); + + ATF_REQUIRE(!(tp1 < tp1)); + + ATF_REQUIRE( tp1 < tp2); + ATF_REQUIRE(!(tp2 < tp1)); + + ATF_REQUIRE(!(tp1 < tp3)); + ATF_REQUIRE( tp3 < tp1); + + // And now, test the actual reason why we want to have an < overload by + // attempting to put the various programs in a set. + std::set< TestProgram > programs; + programs.insert(tp1); + programs.insert(tp2); + programs.insert(tp3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operator_lt); +ATF_TEST_CASE_BODY(operator_lt) +{ + check_operator_lt< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__operator_lt); +ATF_TEST_CASE_BODY(derived__operator_lt) +{ + check_operator_lt< lazy_test_program >(); +} + + +/// Runs a output__no_test_cases test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_output__no_test_cases(void) +{ + TestProgram tp( + "plain", fs::path("binary/path"), fs::path("/the/root"), "suite-name", + model::metadata_builder().add_allowed_architecture("a").build(), + model::test_cases_map()); + + std::ostringstream str; + str << tp; + ATF_REQUIRE_EQ( + "test_program{interface='plain', binary='binary/path', " + "root='/the/root', test_suite='suite-name', " + "metadata=metadata{allowed_architectures='a', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}, " + "test_cases=map()}", + str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__no_test_cases); +ATF_TEST_CASE_BODY(output__no_test_cases) +{ + check_output__no_test_cases< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__output__no_test_cases); +ATF_TEST_CASE_BODY(derived__output__no_test_cases) +{ + check_output__no_test_cases< lazy_test_program >(); +} + + +/// Runs a output__some_test_cases test. +/// +/// \tparam TestProgram Either model::test_program or lazy_test_program. +template< class TestProgram > +static void +check_output__some_test_cases(void) +{ + const model::test_cases_map test_cases = model::test_cases_map_builder() + .add("the-name", model::metadata_builder() + .add_allowed_platform("foo") + .add_custom("bar", "baz") + .build()) + .add("another-name") + .build(); + + const TestProgram tp = TestProgram( + "plain", fs::path("binary/path"), fs::path("/the/root"), "suite-name", + model::metadata_builder().add_allowed_architecture("a").build(), + test_cases); + + std::ostringstream str; + str << tp; + ATF_REQUIRE_EQ( + "test_program{interface='plain', binary='binary/path', " + "root='/the/root', test_suite='suite-name', " + "metadata=metadata{allowed_architectures='a', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}, " + "test_cases=map(" + "another-name=test_case{name='another-name', " + "metadata=metadata{allowed_architectures='a', allowed_platforms='', " + "description='', has_cleanup='false', is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}}, " + "the-name=test_case{name='the-name', " + "metadata=metadata{allowed_architectures='a', allowed_platforms='foo', " + "custom.bar='baz', description='', has_cleanup='false', " + "is_exclusive='false', " + "required_configs='', required_disk_space='0', required_files='', " + "required_memory='0', " + "required_programs='', required_user='', timeout='300'}})}", + str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__some_test_cases); +ATF_TEST_CASE_BODY(output__some_test_cases) +{ + check_output__some_test_cases< model::test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(derived__output__some_test_cases); +ATF_TEST_CASE_BODY(derived__output__some_test_cases) +{ + check_output__some_test_cases< lazy_test_program >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(builder__defaults); +ATF_TEST_CASE_BODY(builder__defaults) +{ + const model::test_program expected( + "mock", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), model::test_cases_map()); + + const model::test_program built = model::test_program_builder( + "mock", fs::path("non-existent"), fs::path("."), "suite-name") + .build(); + + ATF_REQUIRE_EQ(built, expected); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(builder__overrides); +ATF_TEST_CASE_BODY(builder__overrides) +{ + const model::metadata md = model::metadata_builder() + .add_custom("foo", "bar") + .build(); + const model::test_cases_map tcs = model::test_cases_map_builder() + .add("first") + .add("second", md) + .build(); + const model::test_program expected( + "mock", fs::path("binary"), fs::path("root"), "suite-name", md, tcs); + + const model::test_program built = model::test_program_builder( + "mock", fs::path("binary"), fs::path("root"), "suite-name") + .add_test_case("first") + .add_test_case("second", md) + .set_metadata(md) + .build(); + + ATF_REQUIRE_EQ(built, expected); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(builder__ptr); +ATF_TEST_CASE_BODY(builder__ptr) +{ + const model::test_program expected( + "mock", fs::path("non-existent"), fs::path("."), "suite-name", + model::metadata_builder().build(), model::test_cases_map()); + + const model::test_program_ptr built = model::test_program_builder( + "mock", fs::path("non-existent"), fs::path("."), "suite-name") + .build_ptr(); + + ATF_REQUIRE_EQ(*built, expected); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ctor_and_getters); + ATF_ADD_TEST_CASE(tcs, find__ok); + ATF_ADD_TEST_CASE(tcs, find__missing); + ATF_ADD_TEST_CASE(tcs, metadata_inheritance); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__copy); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__not_copy); + ATF_ADD_TEST_CASE(tcs, operator_lt); + ATF_ADD_TEST_CASE(tcs, output__no_test_cases); + ATF_ADD_TEST_CASE(tcs, output__some_test_cases); + + ATF_ADD_TEST_CASE(tcs, derived__ctor_and_getters); + ATF_ADD_TEST_CASE(tcs, derived__find__ok); + ATF_ADD_TEST_CASE(tcs, derived__find__missing); + ATF_ADD_TEST_CASE(tcs, derived__metadata_inheritance); + ATF_ADD_TEST_CASE(tcs, derived__operators_eq_and_ne__copy); + ATF_ADD_TEST_CASE(tcs, derived__operators_eq_and_ne__not_copy); + ATF_ADD_TEST_CASE(tcs, derived__operator_lt); + ATF_ADD_TEST_CASE(tcs, derived__output__no_test_cases); + ATF_ADD_TEST_CASE(tcs, derived__output__some_test_cases); + + ATF_ADD_TEST_CASE(tcs, builder__defaults); + ATF_ADD_TEST_CASE(tcs, builder__overrides); + ATF_ADD_TEST_CASE(tcs, builder__ptr); +} diff --git a/model/test_result.cpp b/model/test_result.cpp new file mode 100644 index 000000000000..7392e77f5561 --- /dev/null +++ b/model/test_result.cpp @@ -0,0 +1,142 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/test_result.hpp" + +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +/// Constructs a base result. +/// +/// \param type_ The type of the result. +/// \param reason_ The reason explaining the result, if any. It is OK for this +/// to be empty, which is actually the default. +model::test_result::test_result(const test_result_type type_, + const std::string& reason_) : + _type(type_), + _reason(reason_) +{ +} + + +/// Returns the type of the result. +/// +/// \return A result type. +model::test_result_type +model::test_result::type(void) const +{ + return _type; +} + + +/// Returns the reason explaining the result. +/// +/// \return A textual reason, possibly empty. +const std::string& +model::test_result::reason(void) const +{ + return _reason; +} + + +/// True if the test case result has a positive connotation. +/// +/// \return Whether the test case is good or not. +bool +model::test_result::good(void) const +{ + switch (_type) { + case test_result_expected_failure: + case test_result_passed: + case test_result_skipped: + return true; + + case test_result_broken: + case test_result_failed: + return false; + } + UNREACHABLE; +} + + +/// Equality comparator. +/// +/// \param other The test result to compare to. +/// +/// \return True if the other object is equal to this one, false otherwise. +bool +model::test_result::operator==(const test_result& other) const +{ + return _type == other._type && _reason == other._reason; +} + + +/// Inequality comparator. +/// +/// \param other The test result to compare to. +/// +/// \return True if the other object is different from this one, false +/// otherwise. +bool +model::test_result::operator!=(const test_result& other) const +{ + return !(*this == other); +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +model::operator<<(std::ostream& output, const test_result& object) +{ + std::string result_name; + switch (object.type()) { + case test_result_broken: result_name = "broken"; break; + case test_result_expected_failure: result_name = "expected_failure"; break; + case test_result_failed: result_name = "failed"; break; + case test_result_passed: result_name = "passed"; break; + case test_result_skipped: result_name = "skipped"; break; + } + const std::string& reason = object.reason(); + if (reason.empty()) { + output << F("model::test_result{type=%s}") + % text::quote(result_name, '\''); + } else { + output << F("model::test_result{type=%s, reason=%s}") + % text::quote(result_name, '\'') % text::quote(reason, '\''); + } + return output; +} diff --git a/model/test_result.hpp b/model/test_result.hpp new file mode 100644 index 000000000000..b9c439ce789a --- /dev/null +++ b/model/test_result.hpp @@ -0,0 +1,79 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/test_result.hpp +/// Definition of the "test result" concept. + +#if !defined(MODEL_TEST_RESULT_HPP) +#define MODEL_TEST_RESULT_HPP + +#include "model/test_result_fwd.hpp" + +#include +#include + +namespace model { + + +/// Representation of a single test result. +/// +/// A test result is a simple pair of (type, reason). The type indicates the +/// semantics of the results, and the optional reason provides an extra +/// description of the result type. +/// +/// In general, a 'passed' result will not have a reason attached, because a +/// successful test case does not deserve any kind of explanation. We used to +/// special-case this with a very complex class hierarchy, but it proved to +/// result in an extremely-complex to maintain code base that provided no +/// benefits. As a result, we allow any test type to carry a reason. +class test_result { + /// The type of the result. + test_result_type _type; + + /// A description of the result; may be empty. + std::string _reason; + +public: + test_result(const test_result_type, const std::string& = ""); + + test_result_type type(void) const; + const std::string& reason(void) const; + + bool good(void) const; + + bool operator==(const test_result&) const; + bool operator!=(const test_result&) const; +}; + + +std::ostream& operator<<(std::ostream&, const test_result&); + + +} // namespace model + +#endif // !defined(MODEL_TEST_RESULT_HPP) diff --git a/model/test_result_fwd.hpp b/model/test_result_fwd.hpp new file mode 100644 index 000000000000..d7871e81d23e --- /dev/null +++ b/model/test_result_fwd.hpp @@ -0,0 +1,53 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/test_result_fwd.hpp +/// Forward declarations for model/test_result.hpp + +#if !defined(MODEL_TEST_RESULT_FWD_HPP) +#define MODEL_TEST_RESULT_FWD_HPP + +namespace model { + + +/// Definitions for all possible test case results. +enum test_result_type { + test_result_broken, + test_result_expected_failure, + test_result_failed, + test_result_passed, + test_result_skipped, +}; + + +class test_result; + + +} // namespace model + +#endif // !defined(MODEL_TEST_RESULT_FWD_HPP) diff --git a/model/test_result_test.cpp b/model/test_result_test.cpp new file mode 100644 index 000000000000..355587d37aee --- /dev/null +++ b/model/test_result_test.cpp @@ -0,0 +1,185 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "model/test_result.hpp" + +#include + +#include + + +/// Creates a test case to validate the getters. +/// +/// \param name The name of the test case; "__getters" will be appended. +/// \param expected_type The expected type of the result. +/// \param expected_reason The expected reason for the result. +/// \param result The result to query. +#define GETTERS_TEST(name, expected_type, expected_reason, result) \ + ATF_TEST_CASE_WITHOUT_HEAD(name ## __getters); \ + ATF_TEST_CASE_BODY(name ## __getters) \ + { \ + ATF_REQUIRE(expected_type == result.type()); \ + ATF_REQUIRE_EQ(expected_reason, result.reason()); \ + } + + +/// Creates a test case to validate the good() method. +/// +/// \param name The name of the test case; "__good" will be appended. +/// \param expected The expected result of good(). +/// \param result_type The result type to check. +#define GOOD_TEST(name, expected, result_type) \ + ATF_TEST_CASE_WITHOUT_HEAD(name ## __good); \ + ATF_TEST_CASE_BODY(name ## __good) \ + { \ + ATF_REQUIRE_EQ(expected, model::test_result(result_type).good()); \ + } + + +/// Creates a test case to validate the operator<< method. +/// +/// \param name The name of the test case; "__output" will be appended. +/// \param expected The expected string in the output. +/// \param result The result to format. +#define OUTPUT_TEST(name, expected, result) \ + ATF_TEST_CASE_WITHOUT_HEAD(name ## __output); \ + ATF_TEST_CASE_BODY(name ## __output) \ + { \ + std::ostringstream output; \ + output << "prefix" << result << "suffix"; \ + ATF_REQUIRE_EQ("prefix" + std::string(expected) + "suffix", \ + output.str()); \ + } + + +GETTERS_TEST( + broken, + model::test_result_broken, + "The reason", + model::test_result(model::test_result_broken, "The reason")); +GETTERS_TEST( + expected_failure, + model::test_result_expected_failure, + "The reason", + model::test_result(model::test_result_expected_failure, "The reason")); +GETTERS_TEST( + failed, + model::test_result_failed, + "The reason", + model::test_result(model::test_result_failed, "The reason")); +GETTERS_TEST( + passed, + model::test_result_passed, + "", + model::test_result(model::test_result_passed)); +GETTERS_TEST( + skipped, + model::test_result_skipped, + "The reason", + model::test_result(model::test_result_skipped, "The reason")); + + +GOOD_TEST(broken, false, model::test_result_broken); +GOOD_TEST(expected_failure, true, model::test_result_expected_failure); +GOOD_TEST(failed, false, model::test_result_failed); +GOOD_TEST(passed, true, model::test_result_passed); +GOOD_TEST(skipped, true, model::test_result_skipped); + + +OUTPUT_TEST( + broken, + "model::test_result{type='broken', reason='foo'}", + model::test_result(model::test_result_broken, "foo")); +OUTPUT_TEST( + expected_failure, + "model::test_result{type='expected_failure', reason='abc def'}", + model::test_result(model::test_result_expected_failure, "abc def")); +OUTPUT_TEST( + failed, + "model::test_result{type='failed', reason='some \\'string'}", + model::test_result(model::test_result_failed, "some 'string")); +OUTPUT_TEST( + passed, + "model::test_result{type='passed'}", + model::test_result(model::test_result_passed, "")); +OUTPUT_TEST( + skipped, + "model::test_result{type='skipped', reason='last message'}", + model::test_result(model::test_result_skipped, "last message")); + + +ATF_TEST_CASE_WITHOUT_HEAD(operator_eq); +ATF_TEST_CASE_BODY(operator_eq) +{ + const model::test_result result1(model::test_result_broken, "Foo"); + const model::test_result result2(model::test_result_broken, "Foo"); + const model::test_result result3(model::test_result_broken, "Bar"); + const model::test_result result4(model::test_result_failed, "Foo"); + + ATF_REQUIRE( result1 == result1); + ATF_REQUIRE( result1 == result2); + ATF_REQUIRE(!(result1 == result3)); + ATF_REQUIRE(!(result1 == result4)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operator_ne); +ATF_TEST_CASE_BODY(operator_ne) +{ + const model::test_result result1(model::test_result_broken, "Foo"); + const model::test_result result2(model::test_result_broken, "Foo"); + const model::test_result result3(model::test_result_broken, "Bar"); + const model::test_result result4(model::test_result_failed, "Foo"); + + ATF_REQUIRE(!(result1 != result1)); + ATF_REQUIRE(!(result1 != result2)); + ATF_REQUIRE( result1 != result3); + ATF_REQUIRE( result1 != result4); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, broken__getters); + ATF_ADD_TEST_CASE(tcs, broken__good); + ATF_ADD_TEST_CASE(tcs, broken__output); + ATF_ADD_TEST_CASE(tcs, expected_failure__getters); + ATF_ADD_TEST_CASE(tcs, expected_failure__good); + ATF_ADD_TEST_CASE(tcs, expected_failure__output); + ATF_ADD_TEST_CASE(tcs, failed__getters); + ATF_ADD_TEST_CASE(tcs, failed__good); + ATF_ADD_TEST_CASE(tcs, failed__output); + ATF_ADD_TEST_CASE(tcs, passed__getters); + ATF_ADD_TEST_CASE(tcs, passed__good); + ATF_ADD_TEST_CASE(tcs, passed__output); + ATF_ADD_TEST_CASE(tcs, skipped__getters); + ATF_ADD_TEST_CASE(tcs, skipped__good); + ATF_ADD_TEST_CASE(tcs, skipped__output); + ATF_ADD_TEST_CASE(tcs, operator_eq); + ATF_ADD_TEST_CASE(tcs, operator_ne); +} diff --git a/model/types.hpp b/model/types.hpp new file mode 100644 index 000000000000..e877b6f58d46 --- /dev/null +++ b/model/types.hpp @@ -0,0 +1,61 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file model/types.hpp +/// Definition of miscellaneous base types required by our classes. +/// +/// We consider objects coming from the STL and from the utils module to be +/// base types. + +#if !defined(MODEL_TYPES_HPP) +#define MODEL_TYPES_HPP + +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace model { + + +/// Collection of paths. +typedef std::set< utils::fs::path > paths_set; + + +/// Collection of strings. +typedef std::set< std::string > strings_set; + + +/// Collection of test properties in their textual form. +typedef std::map< std::string, std::string > properties_map; + + +} // namespace model + +#endif // !defined(MODEL_TYPES_HPP) diff --git a/store/Kyuafile b/store/Kyuafile new file mode 100644 index 000000000000..ada2f7c0e88c --- /dev/null +++ b/store/Kyuafile @@ -0,0 +1,15 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="dbtypes_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="layout_test"} +atf_test_program{name="metadata_test"} +atf_test_program{name="migrate_test"} +atf_test_program{name="read_backend_test"} +atf_test_program{name="read_transaction_test"} +atf_test_program{name="schema_inttest"} +atf_test_program{name="transaction_test"} +atf_test_program{name="write_backend_test"} +atf_test_program{name="write_transaction_test"} diff --git a/store/Makefile.am.inc b/store/Makefile.am.inc new file mode 100644 index 000000000000..13c7c70a10d9 --- /dev/null +++ b/store/Makefile.am.inc @@ -0,0 +1,145 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +STORE_CFLAGS = $(MODEL_CFLAGS) $(UTILS_CFLAGS) +STORE_LIBS = libstore.a $(MODEL_LIBS) $(UTILS_LIBS) + +noinst_LIBRARIES += libstore.a +libstore_a_CPPFLAGS = -DKYUA_STOREDIR=\"$(storedir)\" +libstore_a_CPPFLAGS += $(UTILS_CFLAGS) +libstore_a_SOURCES = store/dbtypes.cpp +libstore_a_SOURCES += store/dbtypes.hpp +libstore_a_SOURCES += store/exceptions.cpp +libstore_a_SOURCES += store/exceptions.hpp +libstore_a_SOURCES += store/layout.cpp +libstore_a_SOURCES += store/layout.hpp +libstore_a_SOURCES += store/layout_fwd.hpp +libstore_a_SOURCES += store/metadata.cpp +libstore_a_SOURCES += store/metadata.hpp +libstore_a_SOURCES += store/metadata_fwd.hpp +libstore_a_SOURCES += store/migrate.cpp +libstore_a_SOURCES += store/migrate.hpp +libstore_a_SOURCES += store/read_backend.cpp +libstore_a_SOURCES += store/read_backend.hpp +libstore_a_SOURCES += store/read_backend_fwd.hpp +libstore_a_SOURCES += store/read_transaction.cpp +libstore_a_SOURCES += store/read_transaction.hpp +libstore_a_SOURCES += store/read_transaction_fwd.hpp +libstore_a_SOURCES += store/write_backend.cpp +libstore_a_SOURCES += store/write_backend.hpp +libstore_a_SOURCES += store/write_backend_fwd.hpp +libstore_a_SOURCES += store/write_transaction.cpp +libstore_a_SOURCES += store/write_transaction.hpp +libstore_a_SOURCES += store/write_transaction_fwd.hpp + +dist_store_DATA = store/migrate_v1_v2.sql +dist_store_DATA += store/migrate_v2_v3.sql +dist_store_DATA += store/schema_v3.sql + +if WITH_ATF +tests_storedir = $(pkgtestsdir)/store + +tests_store_DATA = store/Kyuafile +tests_store_DATA += store/schema_v1.sql +tests_store_DATA += store/schema_v2.sql +tests_store_DATA += store/testdata_v1.sql +tests_store_DATA += store/testdata_v2.sql +tests_store_DATA += store/testdata_v3_1.sql +tests_store_DATA += store/testdata_v3_2.sql +tests_store_DATA += store/testdata_v3_3.sql +tests_store_DATA += store/testdata_v3_4.sql +EXTRA_DIST += $(tests_store_DATA) + +tests_store_PROGRAMS = store/dbtypes_test +store_dbtypes_test_SOURCES = store/dbtypes_test.cpp +store_dbtypes_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_dbtypes_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/exceptions_test +store_exceptions_test_SOURCES = store/exceptions_test.cpp +store_exceptions_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_exceptions_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/layout_test +store_layout_test_SOURCES = store/layout_test.cpp +store_layout_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +store_layout_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/metadata_test +store_metadata_test_SOURCES = store/metadata_test.cpp +store_metadata_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_metadata_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/migrate_test +store_migrate_test_SOURCES = store/migrate_test.cpp +store_migrate_test_CPPFLAGS = -DKYUA_STOREDIR=\"$(storedir)\" +store_migrate_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) +store_migrate_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/read_backend_test +store_read_backend_test_SOURCES = store/read_backend_test.cpp +store_read_backend_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_read_backend_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/read_transaction_test +store_read_transaction_test_SOURCES = store/read_transaction_test.cpp +store_read_transaction_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_read_transaction_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/schema_inttest +store_schema_inttest_SOURCES = store/schema_inttest.cpp +store_schema_inttest_CPPFLAGS = -DKYUA_STORETESTDATADIR=\"$(tests_storedir)\" +store_schema_inttest_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_schema_inttest_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/transaction_test +store_transaction_test_SOURCES = store/transaction_test.cpp +store_transaction_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_transaction_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/write_backend_test +store_write_backend_test_SOURCES = store/write_backend_test.cpp +store_write_backend_test_CPPFLAGS = -DKYUA_STOREDIR=\"$(storedir)\" +store_write_backend_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_write_backend_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS) + +tests_store_PROGRAMS += store/write_transaction_test +store_write_transaction_test_SOURCES = store/write_transaction_test.cpp +store_write_transaction_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \ + $(ATF_CXX_CFLAGS) +store_write_transaction_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) \ + $(ATF_CXX_LIBS) +endif diff --git a/store/dbtypes.cpp b/store/dbtypes.cpp new file mode 100644 index 000000000000..3ff755aa3307 --- /dev/null +++ b/store/dbtypes.cpp @@ -0,0 +1,255 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/dbtypes.hpp" + +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace sqlite = utils::sqlite; + + +/// Binds a boolean value to a statement parameter. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param value The value to bind. +void +store::bind_bool(sqlite::statement& stmt, const char* field, const bool value) +{ + stmt.bind(field, value ? "true" : "false"); +} + + +/// Binds a time delta to a statement parameter. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param delta The value to bind. +void +store::bind_delta(sqlite::statement& stmt, const char* field, + const datetime::delta& delta) +{ + stmt.bind(field, static_cast< int64_t >(delta.to_microseconds())); +} + + +/// Binds a string to a statement parameter. +/// +/// If the string is not empty, this binds the string itself. Otherwise, it +/// binds a NULL value. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param str The string to bind. +void +store::bind_optional_string(sqlite::statement& stmt, const char* field, + const std::string& str) +{ + if (str.empty()) + stmt.bind(field, sqlite::null()); + else + stmt.bind(field, str); +} + + +/// Binds a test result type to a statement parameter. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param type The result type to bind. +void +store::bind_test_result_type(sqlite::statement& stmt, const char* field, + const model::test_result_type& type) +{ + switch (type) { + case model::test_result_broken: + stmt.bind(field, "broken"); + break; + + case model::test_result_expected_failure: + stmt.bind(field, "expected_failure"); + break; + + case model::test_result_failed: + stmt.bind(field, "failed"); + break; + + case model::test_result_passed: + stmt.bind(field, "passed"); + break; + + case model::test_result_skipped: + stmt.bind(field, "skipped"); + break; + + default: + UNREACHABLE; + } +} + + +/// Binds a timestamp to a statement parameter. +/// +/// \param stmt The statement to which to bind the parameter. +/// \param field The name of the parameter; must exist. +/// \param timestamp The value to bind. +void +store::bind_timestamp(sqlite::statement& stmt, const char* field, + const datetime::timestamp& timestamp) +{ + stmt.bind(field, timestamp.to_microseconds()); +} + + +/// Queries a boolean value from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +bool +store::column_bool(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + if (stmt.column_type(id) != sqlite::type_text) + throw store::integrity_error(F("Boolean value in column %s is not a " + "string") % column); + const std::string value = stmt.column_text(id); + if (value == "true") + return true; + else if (value == "false") + return false; + else + throw store::integrity_error(F("Unknown boolean value '%s'") % value); +} + + +/// Queries a time delta from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +datetime::delta +store::column_delta(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + if (stmt.column_type(id) != sqlite::type_integer) + throw store::integrity_error(F("Time delta in column %s is not an " + "integer") % column); + return datetime::delta::from_microseconds(stmt.column_int64(id)); +} + + +/// Queries an optional string from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +std::string +store::column_optional_string(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + switch (stmt.column_type(id)) { + case sqlite::type_text: + return stmt.column_text(id); + case sqlite::type_null: + return ""; + default: + throw integrity_error(F("Invalid string type in column %s") % column); + } +} + + +/// Queries a test result type from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +model::test_result_type +store::column_test_result_type(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + if (stmt.column_type(id) != sqlite::type_text) + throw store::integrity_error(F("Result type in column %s is not a " + "string") % column); + const std::string type = stmt.column_text(id); + if (type == "passed") { + return model::test_result_passed; + } else if (type == "broken") { + return model::test_result_broken; + } else if (type == "expected_failure") { + return model::test_result_expected_failure; + } else if (type == "failed") { + return model::test_result_failed; + } else if (type == "skipped") { + return model::test_result_skipped; + } else { + throw store::integrity_error(F("Unknown test result type %s") % type); + } +} + + +/// Queries a timestamp from a statement. +/// +/// \param stmt The statement from which to get the column. +/// \param column The name of the column holding the value. +/// +/// \return The parsed value if all goes well. +/// +/// \throw integrity_error If the value in the specified column is invalid. +datetime::timestamp +store::column_timestamp(sqlite::statement& stmt, const char* column) +{ + const int id = stmt.column_id(column); + if (stmt.column_type(id) != sqlite::type_integer) + throw store::integrity_error(F("Timestamp in column %s is not an " + "integer") % column); + const int64_t value = stmt.column_int64(id); + if (value < 0) + throw store::integrity_error(F("Timestamp in column %s must be " + "positive") % column); + return datetime::timestamp::from_microseconds(value); +} diff --git a/store/dbtypes.hpp b/store/dbtypes.hpp new file mode 100644 index 000000000000..919d088d0ecd --- /dev/null +++ b/store/dbtypes.hpp @@ -0,0 +1,68 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/dbtypes.hpp +/// Functions to internalize/externalize various types. +/// +/// These helper functions are only provided to help in the implementation of +/// other modules. Therefore, this header file should never be included from +/// other header files. + +#if defined(STORE_DBTYPES_HPP) +# error "Do not include dbtypes.hpp multiple times" +#endif // !defined(STORE_DBTYPES_HPP) +#define STORE_DBTYPES_HPP + +#include + +#include "model/test_result_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/sqlite/statement_fwd.hpp" + +namespace store { + + +void bind_bool(utils::sqlite::statement&, const char*, const bool); +void bind_delta(utils::sqlite::statement&, const char*, + const utils::datetime::delta&); +void bind_optional_string(utils::sqlite::statement&, const char*, + const std::string&); +void bind_test_result_type(utils::sqlite::statement&, const char*, + const model::test_result_type&); +void bind_timestamp(utils::sqlite::statement&, const char*, + const utils::datetime::timestamp&); +bool column_bool(utils::sqlite::statement&, const char*); +utils::datetime::delta column_delta(utils::sqlite::statement&, const char*); +std::string column_optional_string(utils::sqlite::statement&, const char*); +model::test_result_type column_test_result_type( + utils::sqlite::statement&, const char*); +utils::datetime::timestamp column_timestamp(utils::sqlite::statement&, + const char*); + + +} // namespace store diff --git a/store/dbtypes_test.cpp b/store/dbtypes_test.cpp new file mode 100644 index 000000000000..abe229eab2b6 --- /dev/null +++ b/store/dbtypes_test.cpp @@ -0,0 +1,234 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/dbtypes.hpp" + +#include + +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "utils/datetime.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::none; + + +namespace { + + +/// Validates that a particular bind_x/column_x sequence works. +/// +/// \param bind The store::bind_* function to put the value. +/// \param value The value to store and validate. +/// \param column The store::column_* function to get the value. +template< typename Type1, typename Type2, typename Type3 > +static void +do_ok_test(void (*bind)(sqlite::statement&, const char*, Type1), + Type2 value, + Type3 (*column)(sqlite::statement&, const char*)) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE test (column DONTCARE)"); + + sqlite::statement insert = db.create_statement("INSERT INTO test " + "VALUES (:v)"); + bind(insert, ":v", value); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + ATF_REQUIRE(query.step()); + ATF_REQUIRE(column(query, "column") == value); + ATF_REQUIRE(!query.step()); +} + + +/// Validates an error condition of column_*. +/// +/// \param value The invalid value to insert into the database. +/// \param column The store::column_* function to get the value. +/// \param error_regexp The expected message in the raised integrity_error. +template< typename Type1, typename Type2 > +static void +do_invalid_test(Type1 value, + Type2 (*column)(sqlite::statement&, const char*), + const std::string& error_regexp) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE test (column DONTCARE)"); + + sqlite::statement insert = db.create_statement("INSERT INTO test " + "VALUES (:v)"); + insert.bind(":v", value); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + ATF_REQUIRE(query.step()); + ATF_REQUIRE_THROW_RE(store::integrity_error, error_regexp, + column(query, "column")); + ATF_REQUIRE(!query.step()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(bool__ok); +ATF_TEST_CASE_BODY(bool__ok) +{ + do_ok_test(store::bind_bool, true, store::column_bool); + do_ok_test(store::bind_bool, false, store::column_bool); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool__get_invalid_type); +ATF_TEST_CASE_BODY(bool__get_invalid_type) +{ + do_invalid_test(123, store::column_bool, "not a string"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool__get_invalid_value); +ATF_TEST_CASE_BODY(bool__get_invalid_value) +{ + do_invalid_test("foo", store::column_bool, "Unknown boolean.*foo"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__ok); +ATF_TEST_CASE_BODY(delta__ok) +{ + do_ok_test(store::bind_delta, datetime::delta(15, 34), store::column_delta); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__get_invalid_type); +ATF_TEST_CASE_BODY(delta__get_invalid_type) +{ + do_invalid_test(15.6, store::column_delta, "not an integer"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(optional_string__ok); +ATF_TEST_CASE_BODY(optional_string__ok) +{ + do_ok_test(store::bind_optional_string, "", store::column_optional_string); + do_ok_test(store::bind_optional_string, "a", store::column_optional_string); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(optional_string__get_invalid_type); +ATF_TEST_CASE_BODY(optional_string__get_invalid_type) +{ + do_invalid_test(35, store::column_optional_string, "Invalid string"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_result_type__ok); +ATF_TEST_CASE_BODY(test_result_type__ok) +{ + do_ok_test(store::bind_test_result_type, + model::test_result_passed, + store::column_test_result_type); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_result_type__get_invalid_type); +ATF_TEST_CASE_BODY(test_result_type__get_invalid_type) +{ + do_invalid_test(12, store::column_test_result_type, "not a string"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_result_type__get_invalid_value); +ATF_TEST_CASE_BODY(test_result_type__get_invalid_value) +{ + do_invalid_test("foo", store::column_test_result_type, + "Unknown test result type foo"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__ok); +ATF_TEST_CASE_BODY(timestamp__ok) +{ + do_ok_test(store::bind_timestamp, + datetime::timestamp::from_microseconds(0), + store::column_timestamp); + do_ok_test(store::bind_timestamp, + datetime::timestamp::from_microseconds(123), + store::column_timestamp); + + do_ok_test(store::bind_timestamp, + datetime::timestamp::from_values(2012, 2, 9, 23, 15, 51, 987654), + store::column_timestamp); + do_ok_test(store::bind_timestamp, + datetime::timestamp::from_values(1980, 1, 2, 3, 4, 5, 0), + store::column_timestamp); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__get_invalid_type); +ATF_TEST_CASE_BODY(timestamp__get_invalid_type) +{ + do_invalid_test(35.6, store::column_timestamp, "not an integer"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__get_invalid_value); +ATF_TEST_CASE_BODY(timestamp__get_invalid_value) +{ + do_invalid_test(-1234, store::column_timestamp, "must be positive"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, bool__ok); + ATF_ADD_TEST_CASE(tcs, bool__get_invalid_type); + ATF_ADD_TEST_CASE(tcs, bool__get_invalid_value); + + ATF_ADD_TEST_CASE(tcs, delta__ok); + ATF_ADD_TEST_CASE(tcs, delta__get_invalid_type); + + ATF_ADD_TEST_CASE(tcs, optional_string__ok); + ATF_ADD_TEST_CASE(tcs, optional_string__get_invalid_type); + + ATF_ADD_TEST_CASE(tcs, test_result_type__ok); + ATF_ADD_TEST_CASE(tcs, test_result_type__get_invalid_type); + ATF_ADD_TEST_CASE(tcs, test_result_type__get_invalid_value); + + ATF_ADD_TEST_CASE(tcs, timestamp__ok); + ATF_ADD_TEST_CASE(tcs, timestamp__get_invalid_type); + ATF_ADD_TEST_CASE(tcs, timestamp__get_invalid_value); +} diff --git a/store/exceptions.cpp b/store/exceptions.cpp new file mode 100644 index 000000000000..7459f3db75ac --- /dev/null +++ b/store/exceptions.cpp @@ -0,0 +1,88 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/exceptions.hpp" + +#include "utils/format/macros.hpp" + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +store::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +store::error::~error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +store::integrity_error::integrity_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +store::integrity_error::~integrity_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param version Version of the current schema. +store::old_schema_error::old_schema_error(const int version) : + error(F("The database contains version %s of the schema, which is " + "stale and needs to be upgraded") % version), + _old_version(version) +{ +} + + +/// Destructor for the error. +store::old_schema_error::~old_schema_error(void) throw() +{ +} + + +/// Returns the current schema version in the database. +/// +/// \return A version number. +int +store::old_schema_error::old_version(void) const +{ + return _old_version; +} diff --git a/store/exceptions.hpp b/store/exceptions.hpp new file mode 100644 index 000000000000..e27c7a02fe3a --- /dev/null +++ b/store/exceptions.hpp @@ -0,0 +1,72 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/exceptions.hpp +/// Exception types raised by the store module. + +#if !defined(STORE_EXCEPTIONS_HPP) +#define STORE_EXCEPTIONS_HPP + +#include + +namespace store { + + +/// Base exception for store errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// The data in the database is inconsistent. +class integrity_error : public error { +public: + explicit integrity_error(const std::string&); + virtual ~integrity_error(void) throw(); +}; + + +/// The database schema is old and needs a migration. +class old_schema_error : public error { + /// Version in the database that caused this error. + int _old_version; + +public: + explicit old_schema_error(const int); + virtual ~old_schema_error(void) throw(); + + int old_version(void) const; +}; + + +} // namespace store + + +#endif // !defined(STORE_EXCEPTIONS_HPP) diff --git a/store/exceptions_test.cpp b/store/exceptions_test.cpp new file mode 100644 index 000000000000..ce364e26293c --- /dev/null +++ b/store/exceptions_test.cpp @@ -0,0 +1,65 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/exceptions.hpp" + +#include + +#include + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const store::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integrity_error); +ATF_TEST_CASE_BODY(integrity_error) +{ + const store::integrity_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(old_schema_error); +ATF_TEST_CASE_BODY(old_schema_error) +{ + const store::old_schema_error e(15); + ATF_REQUIRE_MATCH("version 15 .*upgraded", e.what()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, integrity_error); + ATF_ADD_TEST_CASE(tcs, old_schema_error); +} diff --git a/store/layout.cpp b/store/layout.cpp new file mode 100644 index 000000000000..f69cd96cb48d --- /dev/null +++ b/store/layout.cpp @@ -0,0 +1,264 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/layout.hpp" + +#include +#include + +#include "store/exceptions.hpp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/directory.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/fs/operations.hpp" +#include "utils/logging/macros.hpp" +#include "utils/env.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/regex.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace text = utils::text; + +using utils::optional; + + +namespace { + + +/// Finds the results file for the latest run of the given test suite. +/// +/// \param test_suite Identifier of the test suite to query. +/// +/// \return Path to the located database holding the most recent data for the +/// given test suite. +/// +/// \throw store::error If no previous results file can be found. +static fs::path +find_latest(const std::string& test_suite) +{ + const fs::path store_dir = layout::query_store_dir(); + try { + const text::regex preg = text::regex::compile( + F("^results.%s.[0-9]{8}-[0-9]{6}-[0-9]{6}.db$") % test_suite, 0); + + std::string latest; + + const fs::directory dir(store_dir); + for (fs::directory::const_iterator iter = dir.begin(); + iter != dir.end(); ++iter) { + const text::regex_matches matches = preg.match(iter->name); + if (matches) { + if (latest.empty() || iter->name > latest) { + latest = iter->name; + } + } else { + // Not a database file; skip. + } + } + + if (latest.empty()) + throw store::error( + F("No previous results file found for test suite %s") + % test_suite); + + return store_dir / latest; + } catch (const fs::system_error& e) { + LW(F("Failed to open store dir %s: %s") % store_dir % e.what()); + throw store::error(F("No previous results file found for test suite %s") + % test_suite); + } catch (const text::regex_error& e) { + throw store::error(e.what()); + } +} + + +/// Computes the identifier of a new tests results file. +/// +/// \param test_suite Identifier of the test suite. +/// \param when Timestamp to attach to the identifier. +/// +/// \return Identifier of the file to be created. +static std::string +new_id(const std::string& test_suite, const datetime::timestamp& when) +{ + const std::string when_datetime = when.strftime("%Y%m%d-%H%M%S"); + const int when_ms = static_cast(when.to_microseconds() % 1000000); + return F("%s.%s-%06s") % test_suite % when_datetime % when_ms; +} + + +} // anonymous namespace + + +/// Value to request the creation of a new results file with an automatic name. +/// +/// Can be passed to new_db(). +const char* layout::results_auto_create_name = "NEW"; + + +/// Value to request the opening of the latest results file. +/// +/// Can be passed to find_results(). +const char* layout::results_auto_open_name = "LATEST"; + + +/// Resolves the results file for the given identifier. +/// +/// \param id Identifier of the test suite to open. +/// +/// \return Path to the requested file, if any. +/// +/// \throw store::error If there is no matching entry. +fs::path +layout::find_results(const std::string& id) +{ + LI(F("Searching for a results file with id %s") % id); + + if (id == results_auto_open_name) { + const std::string test_suite = test_suite_for_path(fs::current_path()); + return find_latest(test_suite); + } else { + const fs::path id_as_path(id); + + if (fs::exists(id_as_path) && !fs::is_directory(id_as_path)) { + if (id_as_path.is_absolute()) + return id_as_path; + else + return id_as_path.to_absolute(); + } else if (id.find('/') == std::string::npos) { + const fs::path candidate = + query_store_dir() / (F("results.%s.db") % id); + if (fs::exists(candidate)) { + return candidate; + } else { + return find_latest(id); + } + } else { + INV(id.find('/') != std::string::npos); + return find_latest(test_suite_for_path(id_as_path)); + } + } +} + + +/// Computes the path to a new database for the given test suite. +/// +/// \param id Identifier of the test suite to create. +/// \param root Path to the root of the test suite being run, needed to properly +/// autogenerate the identifiers. +/// +/// \return Identifier of the created results file, if applicable, and the path +/// to such file. +layout::results_id_file_pair +layout::new_db(const std::string& id, const fs::path& root) +{ + std::string generated_id; + optional< fs::path > path; + + if (id == results_auto_create_name) { + generated_id = new_id(test_suite_for_path(root), + datetime::timestamp::now()); + path = query_store_dir() / (F("results.%s.db") % generated_id); + fs::mkdir_p(path.get().branch_path(), 0755); + } else { + path = fs::path(id); + } + + return std::make_pair(generated_id, path.get()); +} + + +/// Computes the path to a new database for the given test suite. +/// +/// \param root Path to the root of the test suite being run; needed to properly +/// autogenerate the identifiers. +/// \param when Timestamp for the test suite being run; needed to properly +/// autogenerate the identifiers. +/// +/// \return Identifier of the created results file, if applicable, and the path +/// to such file. +fs::path +layout::new_db_for_migration(const fs::path& root, + const datetime::timestamp& when) +{ + const std::string generated_id = new_id(test_suite_for_path(root), when); + const fs::path path = query_store_dir() / ( + F("results.%s.db") % generated_id); + fs::mkdir_p(path.branch_path(), 0755); + return path; +} + + +/// Gets the path to the store directory. +/// +/// Note that this function does not create the determined directory. It is the +/// responsibility of the caller to do so. +/// +/// \return Path to the directory holding all the database files. +fs::path +layout::query_store_dir(void) +{ + const optional< fs::path > home = utils::get_home(); + if (home) { + const fs::path& home_path = home.get(); + if (home_path.is_absolute()) + return home_path / ".kyua/store"; + else + return home_path.to_absolute() / ".kyua/store"; + } else { + LW("HOME not defined; creating store database in current " + "directory"); + return fs::current_path(); + } +} + + +/// Returns the test suite name for the current directory. +/// +/// \return The identifier of the current test suite. +std::string +layout::test_suite_for_path(const fs::path& path) +{ + std::string test_suite; + if (path.is_absolute()) + test_suite = path.str(); + else + test_suite = path.to_absolute().str(); + PRE(!test_suite.empty() && test_suite[0] == '/'); + + std::replace(test_suite.begin(), test_suite.end(), '/', '_'); + test_suite.erase(0, 1); + + return test_suite; +} diff --git a/store/layout.hpp b/store/layout.hpp new file mode 100644 index 000000000000..48ab89c45104 --- /dev/null +++ b/store/layout.hpp @@ -0,0 +1,84 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/layout.hpp +/// File system layout definition for the Kyua data files. +/// +/// Tests results files are all stored in a centralized directory by default. +/// In the general case, we do not want the user to have to worry about files: +/// we expose an identifier-based interface where each tests results file has a +/// unique identifier. However, we also want to give full freedom to the user +/// to store such files wherever he likes so we have to deal with paths as well. +/// +/// When creating a new results file, the inputs to resolve the path can be: +/// - NEW: Automatic generation of a new results file, so we want to return its +/// public identifier and the path for internal consumption. +/// - A path: The user provided the specific location where he wants the file +/// stored, so we just obey that. There is no public identifier in this case +/// because there is no naming scheme imposed on the generated files. +/// +/// When opening an existing results file, the inputs to resolve the path can +/// be: +/// - LATEST: Given the current directory, we derive the corresponding test +/// suite name and find the latest timestamped file in the centralized +/// location. +/// - A path: If the file exists, we just open that. If it doesn't exist or if +/// it is a directory, we try to resolve that as a test suite name and locate +/// the latest matching timestamped file. +/// - Everything else: Treated as a test suite identifier, so we try to locate +/// the latest matchin timestamped file. + +#if !defined(STORE_LAYOUT_HPP) +#define STORE_LAYOUT_HPP + +#include "store/layout_fwd.hpp" + +#include + +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace store { +namespace layout { + + +extern const char* results_auto_create_name; +extern const char* results_auto_open_name; + +utils::fs::path find_results(const std::string&); +results_id_file_pair new_db(const std::string&, const utils::fs::path&); +utils::fs::path new_db_for_migration(const utils::fs::path&, + const utils::datetime::timestamp&); +utils::fs::path query_store_dir(void); +std::string test_suite_for_path(const utils::fs::path&); + + +} // namespace layout +} // namespace store + +#endif // !defined(STORE_LAYOUT_HPP) diff --git a/store/layout_fwd.hpp b/store/layout_fwd.hpp new file mode 100644 index 000000000000..72d05a27c66a --- /dev/null +++ b/store/layout_fwd.hpp @@ -0,0 +1,54 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/layout_fwd.hpp +/// Forward declarations for store/layout.hpp + +#if !defined(STORE_LAYOUT_FWD_HPP) +#define STORE_LAYOUT_FWD_HPP + +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace store { +namespace layout { + + +/// A pair with the user-visible ID of the results file and its path. +/// +/// It is possible for the ID (first component) to be empty in the cases where +/// the user explicitly requested to create the database in a specific path. +typedef std::pair< std::string, utils::fs::path > results_id_file_pair; + + +} // namespace layout +} // namespace store + +#endif // !defined(STORE_LAYOUT_FWD_HPP) diff --git a/store/layout_test.cpp b/store/layout_test.cpp new file mode 100644 index 000000000000..8564d3aef93c --- /dev/null +++ b/store/layout_test.cpp @@ -0,0 +1,350 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/layout.hpp" + +extern "C" { +#include +} + +#include + +#include + +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__latest); +ATF_TEST_CASE_BODY(find_results__latest) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + const std::string test_suite = layout::test_suite_for_path( + fs::current_path()); + const std::string base = (store_dir / ( + "results." + test_suite + ".")).str(); + + atf::utils::create_file(base + "20140613-194515-000000.db", ""); + ATF_REQUIRE_EQ(base + "20140613-194515-000000.db", + layout::find_results("LATEST").str()); + + atf::utils::create_file(base + "20140614-194515-123456.db", ""); + ATF_REQUIRE_EQ(base + "20140614-194515-123456.db", + layout::find_results("LATEST").str()); + + atf::utils::create_file(base + "20130614-194515-999999.db", ""); + ATF_REQUIRE_EQ(base + "20140614-194515-123456.db", + layout::find_results("LATEST").str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__directory); +ATF_TEST_CASE_BODY(find_results__directory) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + const fs::path dir1("dir1/foo"); + fs::mkdir_p(dir1, 0755); + const fs::path dir2("dir1/bar"); + fs::mkdir_p(dir2, 0755); + + const std::string base1 = (store_dir / ( + "results." + layout::test_suite_for_path(dir1) + ".")).str(); + const std::string base2 = (store_dir / ( + "results." + layout::test_suite_for_path(dir2) + ".")).str(); + + atf::utils::create_file(base1 + "20140613-194515-000000.db", ""); + ATF_REQUIRE_EQ(base1 + "20140613-194515-000000.db", + layout::find_results(dir1.str()).str()); + + atf::utils::create_file(base2 + "20140615-111111-000000.db", ""); + ATF_REQUIRE_EQ(base2 + "20140615-111111-000000.db", + layout::find_results(dir2.str()).str()); + + atf::utils::create_file(base1 + "20140614-194515-123456.db", ""); + ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db", + layout::find_results(dir1.str()).str()); + + atf::utils::create_file(base1 + "20130614-194515-999999.db", ""); + ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db", + layout::find_results(dir1.str()).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__file); +ATF_TEST_CASE_BODY(find_results__file) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + atf::utils::create_file("a-file.db", ""); + ATF_REQUIRE_EQ(fs::path("a-file.db").to_absolute(), + layout::find_results("a-file.db")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__id); +ATF_TEST_CASE_BODY(find_results__id) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + const fs::path dir1("dir1/foo"); + fs::mkdir_p(dir1, 0755); + const fs::path dir2("dir1/bar"); + fs::mkdir_p(dir2, 0755); + + const std::string id1 = layout::test_suite_for_path(dir1); + const std::string base1 = (store_dir / ("results." + id1 + ".")).str(); + const std::string id2 = layout::test_suite_for_path(dir2); + const std::string base2 = (store_dir / ("results." + id2 + ".")).str(); + + atf::utils::create_file(base1 + "20140613-194515-000000.db", ""); + ATF_REQUIRE_EQ(base1 + "20140613-194515-000000.db", + layout::find_results(id1).str()); + + atf::utils::create_file(base2 + "20140615-111111-000000.db", ""); + ATF_REQUIRE_EQ(base2 + "20140615-111111-000000.db", + layout::find_results(id2).str()); + + atf::utils::create_file(base1 + "20140614-194515-123456.db", ""); + ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db", + layout::find_results(id1).str()); + + atf::utils::create_file(base1 + "20130614-194515-999999.db", ""); + ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db", + layout::find_results(id1).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__id_with_timestamp); +ATF_TEST_CASE_BODY(find_results__id_with_timestamp) +{ + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + + const fs::path dir1("dir1/foo"); + fs::mkdir_p(dir1, 0755); + const fs::path dir2("dir1/bar"); + fs::mkdir_p(dir2, 0755); + + const std::string id1 = layout::test_suite_for_path(dir1); + const std::string base1 = (store_dir / ("results." + id1 + ".")).str(); + const std::string id2 = layout::test_suite_for_path(dir2); + const std::string base2 = (store_dir / ("results." + id2 + ".")).str(); + + atf::utils::create_file(base1 + "20140613-194515-000000.db", ""); + atf::utils::create_file(base2 + "20140615-111111-000000.db", ""); + atf::utils::create_file(base1 + "20140614-194515-123456.db", ""); + atf::utils::create_file(base1 + "20130614-194515-999999.db", ""); + + ATF_REQUIRE_MATCH( + "_dir1_foo.20140613-194515-000000.db$", + layout::find_results(id1 + ".20140613-194515-000000").str()); + + ATF_REQUIRE_MATCH( + "_dir1_foo.20140614-194515-123456.db$", + layout::find_results(id1 + ".20140614-194515-123456").str()); + + ATF_REQUIRE_MATCH( + "_dir1_bar.20140615-111111-000000.db$", + layout::find_results(id2 + ".20140615-111111-000000").str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_results__not_found); +ATF_TEST_CASE_BODY(find_results__not_found) +{ + ATF_REQUIRE_THROW_RE( + store::error, + "No previous results file found for test suite foo_bar", + layout::find_results("foo_bar")); + + const fs::path store_dir = layout::query_store_dir(); + fs::mkdir_p(store_dir, 0755); + ATF_REQUIRE_THROW_RE( + store::error, + "No previous results file found for test suite foo_bar", + layout::find_results("foo_bar")); + + const char* candidates[] = { + "results.foo.20140613-194515-012345.db", // Bad test suite. + "results.foo_bar.20140613-194515-012345", // Missing extension. + "foo_bar.20140613-194515-012345.db", // Missing prefix. + "results.foo_bar.2010613-194515-012345.db", // Bad date. + "results.foo_bar.20140613-19515-012345.db", // Bad time. + "results.foo_bar.20140613-194515-01245.db", // Bad microseconds. + NULL, + }; + for (const char** candidate = candidates; *candidate != NULL; ++candidate) { + std::cout << "Current candidate: " << *candidate << '\n'; + atf::utils::create_file((store_dir / *candidate).str(), ""); + ATF_REQUIRE_THROW_RE( + store::error, + "No previous results file found for test suite foo_bar", + layout::find_results("foo_bar")); + } + + atf::utils::create_file( + (store_dir / "results.foo_bar.20140613-194515-012345.db").str(), ""); + layout::find_results("foo_bar"); // Expected not to throw. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(new_db__new); +ATF_TEST_CASE_BODY(new_db__new) +{ + datetime::set_mock_now(2014, 6, 13, 19, 45, 15, 5000); + ATF_REQUIRE(!fs::exists(fs::path(".kyua/store"))); + const layout::results_id_file_pair results = layout::new_db( + "NEW", fs::path("/some/path/to/the/suite")); + ATF_REQUIRE( fs::exists(fs::path(".kyua/store"))); + ATF_REQUIRE( fs::is_directory(fs::path(".kyua/store"))); + + const std::string id = "some_path_to_the_suite.20140613-194515-005000"; + ATF_REQUIRE_EQ(id, results.first); + ATF_REQUIRE_EQ(layout::query_store_dir() / ("results." + id + ".db"), + results.second); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(new_db__explicit); +ATF_TEST_CASE_BODY(new_db__explicit) +{ + ATF_REQUIRE(!fs::exists(fs::path(".kyua/store"))); + const layout::results_id_file_pair results = layout::new_db( + "foo/results-file.db", fs::path("unused")); + ATF_REQUIRE(!fs::exists(fs::path(".kyua/store"))); + ATF_REQUIRE(!fs::exists(fs::path("foo"))); + + ATF_REQUIRE(results.first.empty()); + ATF_REQUIRE_EQ(fs::path("foo/results-file.db"), results.second); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(new_db_for_migration); +ATF_TEST_CASE_BODY(new_db_for_migration) +{ + ATF_REQUIRE(!fs::exists(fs::path(".kyua/store"))); + const fs::path results_file = layout::new_db_for_migration( + fs::path("/some/root"), + datetime::timestamp::from_values(2014, 7, 30, 10, 5, 20, 76500)); + ATF_REQUIRE( fs::exists(fs::path(".kyua/store"))); + ATF_REQUIRE( fs::is_directory(fs::path(".kyua/store"))); + + ATF_REQUIRE_EQ( + layout::query_store_dir() / + "results.some_root.20140730-100520-076500.db", + results_file); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(query_store_dir__home_absolute); +ATF_TEST_CASE_BODY(query_store_dir__home_absolute) +{ + const fs::path home = fs::current_path() / "homedir"; + utils::setenv("HOME", home.str()); + const fs::path store_dir = layout::query_store_dir(); + ATF_REQUIRE(store_dir.is_absolute()); + ATF_REQUIRE_EQ(home / ".kyua/store", store_dir); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(query_store_dir__home_relative); +ATF_TEST_CASE_BODY(query_store_dir__home_relative) +{ + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + const fs::path store_dir = layout::query_store_dir(); + ATF_REQUIRE(store_dir.is_absolute()); + ATF_REQUIRE_MATCH((home / ".kyua/store").str(), store_dir.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(query_store_dir__no_home); +ATF_TEST_CASE_BODY(query_store_dir__no_home) +{ + utils::unsetenv("HOME"); + ATF_REQUIRE_EQ(fs::current_path(), layout::query_store_dir()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_suite_for_path__absolute); +ATF_TEST_CASE_BODY(test_suite_for_path__absolute) +{ + ATF_REQUIRE_EQ("dir1_dir2_dir3", + layout::test_suite_for_path(fs::path("/dir1/dir2/dir3"))); + ATF_REQUIRE_EQ("dir1", + layout::test_suite_for_path(fs::path("/dir1"))); + ATF_REQUIRE_EQ("dir1_dir2", + layout::test_suite_for_path(fs::path("/dir1///dir2"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(test_suite_for_path__relative); +ATF_TEST_CASE_BODY(test_suite_for_path__relative) +{ + const std::string test_suite = layout::test_suite_for_path( + fs::path("foo/bar")); + ATF_REQUIRE_MATCH("_foo_bar$", test_suite); + ATF_REQUIRE_MATCH("^[^_]", test_suite); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, find_results__latest); + ATF_ADD_TEST_CASE(tcs, find_results__directory); + ATF_ADD_TEST_CASE(tcs, find_results__file); + ATF_ADD_TEST_CASE(tcs, find_results__id); + ATF_ADD_TEST_CASE(tcs, find_results__id_with_timestamp); + ATF_ADD_TEST_CASE(tcs, find_results__not_found); + + ATF_ADD_TEST_CASE(tcs, new_db__new); + ATF_ADD_TEST_CASE(tcs, new_db__explicit); + + ATF_ADD_TEST_CASE(tcs, new_db_for_migration); + + ATF_ADD_TEST_CASE(tcs, query_store_dir__home_absolute); + ATF_ADD_TEST_CASE(tcs, query_store_dir__home_relative); + ATF_ADD_TEST_CASE(tcs, query_store_dir__no_home); + + ATF_ADD_TEST_CASE(tcs, test_suite_for_path__absolute); + ATF_ADD_TEST_CASE(tcs, test_suite_for_path__relative); +} diff --git a/store/metadata.cpp b/store/metadata.cpp new file mode 100644 index 000000000000..2d90fe8cb267 --- /dev/null +++ b/store/metadata.cpp @@ -0,0 +1,137 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/metadata.hpp" + +#include "store/exceptions.hpp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace sqlite = utils::sqlite; + + +namespace { + + +/// Fetches an integer column from a statement of the 'metadata' table. +/// +/// \param stmt The statement from which to get the column value. +/// \param column The name of the column to retrieve. +/// +/// \return The value of the column. +/// +/// \throw store::integrity_error If there is a problem fetching the value +/// caused by an invalid schema or data. +static int64_t +int64_column(sqlite::statement& stmt, const char* column) +{ + int index; + try { + index = stmt.column_id(column); + } catch (const sqlite::invalid_column_error& e) { + UNREACHABLE_MSG("Invalid column specification; the SELECT statement " + "should have caught this"); + } + if (stmt.column_type(index) != sqlite::type_integer) + throw store::integrity_error(F("The '%s' column in 'metadata' table " + "has an invalid type") % column); + return stmt.column_int64(index); +} + + +} // anonymous namespace + + +/// Constructs a new metadata object. +/// +/// \param schema_version_ The schema version. +/// \param timestamp_ The time at which this version was created. +store::metadata::metadata(const int schema_version_, const int64_t timestamp_) : + _schema_version(schema_version_), + _timestamp(timestamp_) +{ +} + + +/// Returns the timestamp of this entry. +/// +/// \return The timestamp in this metadata entry. +int64_t +store::metadata::timestamp(void) const +{ + return _timestamp; +} + + +/// Returns the schema version. +/// +/// \return The schema version in this metadata entry. +int +store::metadata::schema_version(void) const +{ + return _schema_version; +} + + +/// Reads the latest metadata entry from the database. +/// +/// \param db The database from which to read the metadata from. +/// +/// \return The current metadata of the database. It is not OK for the metadata +/// table to be empty, so this is guaranteed to return a value unless there is +/// an error. +/// +/// \throw store::integrity_error If the metadata in the database is empty, +/// has an invalid schema or its contents are bogus. +store::metadata +store::metadata::fetch_latest(sqlite::database& db) +{ + try { + sqlite::statement stmt = db.create_statement( + "SELECT schema_version, timestamp FROM metadata " + "ORDER BY schema_version DESC LIMIT 1"); + if (!stmt.step()) + throw store::integrity_error("The 'metadata' table is empty"); + + const int schema_version_ = + static_cast< int >(int64_column(stmt, "schema_version")); + const int64_t timestamp_ = int64_column(stmt, "timestamp"); + + if (stmt.step()) + UNREACHABLE_MSG("Got more than one result from a query that " + "does not permit this; any pragmas defined?"); + + return metadata(schema_version_, timestamp_); + } catch (const sqlite::error& e) { + throw store::integrity_error(F("Invalid metadata schema: %s") % + e.what()); + } +} diff --git a/store/metadata.hpp b/store/metadata.hpp new file mode 100644 index 000000000000..c155af6d5897 --- /dev/null +++ b/store/metadata.hpp @@ -0,0 +1,68 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/metadata.hpp +/// Representation of the database metadata. + +#if !defined(STORE_METADATA_HPP) +#define STORE_METADATA_HPP + +#include "store/metadata_fwd.hpp" + +extern "C" { +#include +} + +#include + +#include "utils/sqlite/database_fwd.hpp" + +namespace store { + + +/// Representation of the database metadata. +class metadata { + /// Current version of the database schema. + int _schema_version; + + /// Timestamp of the last metadata entry in the database. + int64_t _timestamp; + + metadata(const int, const int64_t); + +public: + int64_t timestamp(void) const; + int schema_version(void) const; + + static metadata fetch_latest(utils::sqlite::database&); +}; + + +} // namespace store + +#endif // !defined(STORE_METADATA_HPP) diff --git a/store/metadata_fwd.hpp b/store/metadata_fwd.hpp new file mode 100644 index 000000000000..39aa8c2448d4 --- /dev/null +++ b/store/metadata_fwd.hpp @@ -0,0 +1,43 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/metadata_fwd.hpp +/// Forward declarations for store/metadata.hpp + +#if !defined(STORE_METADATA_FWD_HPP) +#define STORE_METADATA_FWD_HPP + +namespace store { + + +class metadata; + + +} // namespace store + +#endif // !defined(STORE_METADATA_FWD_HPP) diff --git a/store/metadata_test.cpp b/store/metadata_test.cpp new file mode 100644 index 000000000000..e32f1ae38dfb --- /dev/null +++ b/store/metadata_test.cpp @@ -0,0 +1,154 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/metadata.hpp" + +#include + +#include "store/exceptions.hpp" +#include "store/write_backend.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" + +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + + +namespace { + + +/// Creates a test in-memory database. +/// +/// When using this function, you must define a 'require.files' property in this +/// case pointing to store::detail::schema_file(). +/// +/// The database created by this function mimics a real complete database, but +/// without any predefined values. I.e. for our particular case, the metadata +/// table is empty. +/// +/// \return A SQLite database instance. +static sqlite::database +create_database(void) +{ + sqlite::database db = sqlite::database::in_memory(); + store::detail::initialize(db); + db.exec("DELETE FROM metadata"); + return db; +} + + +} // anonymous namespace + + +ATF_TEST_CASE(fetch_latest__ok); +ATF_TEST_CASE_HEAD(fetch_latest__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(fetch_latest__ok) +{ + sqlite::database db = create_database(); + db.exec("INSERT INTO metadata (schema_version, timestamp) " + "VALUES (512, 5678)"); + db.exec("INSERT INTO metadata (schema_version, timestamp) " + "VALUES (256, 1234)"); + + const store::metadata metadata = store::metadata::fetch_latest(db); + ATF_REQUIRE_EQ(5678L, metadata.timestamp()); + ATF_REQUIRE_EQ(512, metadata.schema_version()); +} + + +ATF_TEST_CASE(fetch_latest__empty_metadata); +ATF_TEST_CASE_HEAD(fetch_latest__empty_metadata) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(fetch_latest__empty_metadata) +{ + sqlite::database db = create_database(); + ATF_REQUIRE_THROW_RE(store::integrity_error, "metadata.*empty", + store::metadata::fetch_latest(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(fetch_latest__no_timestamp); +ATF_TEST_CASE_BODY(fetch_latest__no_timestamp) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE metadata (schema_version INTEGER)"); + db.exec("INSERT INTO metadata VALUES (3)"); + + ATF_REQUIRE_THROW_RE(store::integrity_error, + "Invalid metadata.*timestamp", + store::metadata::fetch_latest(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(fetch_latest__no_schema_version); +ATF_TEST_CASE_BODY(fetch_latest__no_schema_version) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE metadata (timestamp INTEGER)"); + db.exec("INSERT INTO metadata VALUES (3)"); + + ATF_REQUIRE_THROW_RE(store::integrity_error, + "Invalid metadata.*schema_version", + store::metadata::fetch_latest(db)); +} + + +ATF_TEST_CASE(fetch_latest__invalid_timestamp); +ATF_TEST_CASE_HEAD(fetch_latest__invalid_timestamp) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(fetch_latest__invalid_timestamp) +{ + sqlite::database db = create_database(); + db.exec("INSERT INTO metadata (schema_version, timestamp) " + "VALUES (3, 'foo')"); + + ATF_REQUIRE_THROW_RE(store::integrity_error, + "timestamp.*invalid type", + store::metadata::fetch_latest(db)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, fetch_latest__ok); + ATF_ADD_TEST_CASE(tcs, fetch_latest__empty_metadata); + ATF_ADD_TEST_CASE(tcs, fetch_latest__no_timestamp); + ATF_ADD_TEST_CASE(tcs, fetch_latest__no_schema_version); + ATF_ADD_TEST_CASE(tcs, fetch_latest__invalid_timestamp); +} diff --git a/store/migrate.cpp b/store/migrate.cpp new file mode 100644 index 000000000000..9ec97c231184 --- /dev/null +++ b/store/migrate.cpp @@ -0,0 +1,287 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/migrate.hpp" + +#include + +#include "store/dbtypes.hpp" +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "store/metadata.hpp" +#include "store/read_backend.hpp" +#include "store/write_backend.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" +#include "utils/text/operations.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Schema version at which we switched to results files. +const int first_chunked_schema_version = 3; + + +/// Queries the schema version of the given database. +/// +/// \param file The database from which to query the schema version. +/// +/// \return The schema version number. +static int +get_schema_version(const fs::path& file) +{ + sqlite::database db = store::detail::open_and_setup( + file, sqlite::open_readonly); + return store::metadata::fetch_latest(db).schema_version(); +} + + +/// Performs a single migration step. +/// +/// Both action_id and old_database are little hacks to support the migration +/// from the historical database to chunked files. We'd use a more generic +/// "replacements" map, but it's not worth it. +/// +/// \param file Database on which to apply the migration step. +/// \param version_from Current schema version in the database. +/// \param version_to Schema version to migrate to. +/// \param action_id If not none, replace ACTION_ID in the migration file with +/// this value. +/// \param old_database If not none, replace OLD_DATABASE in the migration +/// file with this value. +/// +/// \throw error If there is a problem applying the migration. +static void +migrate_schema_step(const fs::path& file, + const int version_from, + const int version_to, + const optional< int64_t > action_id = none, + const optional< fs::path > old_database = none) +{ + LI(F("Migrating schema of %s from version %s to %s") % file % version_from + % version_to); + + PRE(version_to == version_from + 1); + + sqlite::database db = store::detail::open_and_setup( + file, sqlite::open_readwrite); + + const fs::path migration = store::detail::migration_file(version_from, + version_to); + + std::string migration_string; + try { + migration_string = utils::read_file(migration); + } catch (const std::runtime_error& unused_e) { + throw store::error(F("Cannot read migration file '%s'") % migration); + } + if (action_id) { + migration_string = text::replace_all(migration_string, "@ACTION_ID@", + F("%s") % action_id.get()); + } + if (old_database) { + migration_string = text::replace_all(migration_string, "@OLD_DATABASE@", + old_database.get().str()); + } + try { + db.exec(migration_string); + } catch (const sqlite::error& e) { + throw store::error(F("Schema migration failed: %s") % e.what()); + } +} + + +/// Given a historical database, chunks it up into results files. +/// +/// The given database is DELETED on success given that it will have been +/// split up into various different files. +/// +/// \param old_file Path to the old database. +static void +chunk_database(const fs::path& old_file) +{ + PRE(get_schema_version(old_file) == first_chunked_schema_version - 1); + + LI(F("Need to split %s into per-action files") % old_file); + + sqlite::database old_db = store::detail::open_and_setup( + old_file, sqlite::open_readonly); + + sqlite::statement actions_stmt = old_db.create_statement( + "SELECT action_id, cwd FROM actions NATURAL JOIN contexts"); + + sqlite::statement start_time_stmt = old_db.create_statement( + "SELECT test_results.start_time AS start_time " + "FROM test_programs " + " JOIN test_cases " + " ON test_programs.test_program_id == test_cases.test_program_id" + " JOIN test_results " + " ON test_cases.test_case_id == test_results.test_case_id " + "WHERE test_programs.action_id == :action_id " + "ORDER BY start_time LIMIT 1"); + + while (actions_stmt.step()) { + const int64_t action_id = actions_stmt.safe_column_int64("action_id"); + const fs::path cwd(actions_stmt.safe_column_text("cwd")); + + LI(F("Extracting action %s") % action_id); + + start_time_stmt.reset(); + start_time_stmt.bind(":action_id", action_id); + if (!start_time_stmt.step()) { + LI(F("Skipping empty action %s") % action_id); + continue; + } + const datetime::timestamp start_time = store::column_timestamp( + start_time_stmt, "start_time"); + start_time_stmt.step_without_results(); + + const fs::path new_file = store::layout::new_db_for_migration( + cwd, start_time); + if (fs::exists(new_file)) { + LI(F("Skipping action because %s already exists") % new_file); + continue; + } + + LI(F("Creating %s for previous action %s") % new_file % action_id); + + try { + fs::mkdir_p(new_file.branch_path(), 0755); + sqlite::database db = store::detail::open_and_setup( + new_file, sqlite::open_readwrite | sqlite::open_create); + store::detail::initialize(db); + db.close(); + migrate_schema_step(new_file, + first_chunked_schema_version - 1, + first_chunked_schema_version, + utils::make_optional(action_id), + utils::make_optional(old_file)); + } catch (...) { + // TODO(jmmv): Handle this better. + fs::unlink(new_file); + } + } + + fs::unlink(old_file); +} + + +} // anonymous namespace + + +/// Calculates the path to a schema migration file. +/// +/// \param version_from The version from which the database is being upgraded. +/// \param version_to The version to which the database is being upgraded. +/// +/// \return The path to the installed migrate_vX_vY.sql file. +fs::path +store::detail::migration_file(const int version_from, const int version_to) +{ + return fs::path(utils::getenv_with_default("KYUA_STOREDIR", KYUA_STOREDIR)) + / (F("migrate_v%s_v%s.sql") % version_from % version_to); +} + + +/// Backs up a database for schema migration purposes. +/// +/// \todo We should probably use the SQLite backup API instead of doing a raw +/// file copy. We issue our backup call with the database already open, but +/// because it is quiescent, it's OK to do so. +/// +/// \param source Location of the database to be backed up. +/// \param old_version Version of the database's CURRENT schema, used to +/// determine the name of the backup file. +/// +/// \throw error If there is a problem during the backup. +void +store::detail::backup_database(const fs::path& source, const int old_version) +{ + const fs::path target(F("%s.v%s.backup") % source.str() % old_version); + + LI(F("Backing up database %s to %s") % source % target); + try { + fs::copy(source, target); + } catch (const fs::error& e) { + throw store::error(e.what()); + } +} + + +/// Migrates the schema of a database to the current version. +/// +/// The algorithm implemented here performs a migration step for every +/// intermediate version between the schema version in the database to the +/// version implemented in this file. This should permit upgrades from +/// arbitrary old databases. +/// +/// \param file The database whose schema to upgrade. +/// +/// \throw error If there is a problem with the migration. +void +store::migrate_schema(const utils::fs::path& file) +{ + const int version_from = get_schema_version(file); + const int version_to = detail::current_schema_version; + if (version_from == version_to) { + throw error(F("Database already at schema version %s; migration not " + "needed") % version_from); + } else if (version_from > version_to) { + throw error(F("Database at schema version %s, which is newer than the " + "supported version %s") % version_from % version_to); + } + + detail::backup_database(file, version_from); + + int i; + for (i = version_from; i < first_chunked_schema_version - 1; ++i) { + migrate_schema_step(file, i, i + 1); + } + chunk_database(file); + INV(version_to == first_chunked_schema_version); +} diff --git a/store/migrate.hpp b/store/migrate.hpp new file mode 100644 index 000000000000..a2622edc0f87 --- /dev/null +++ b/store/migrate.hpp @@ -0,0 +1,55 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/migrate.hpp +/// Utilities to upgrade a database with an old schema to the latest one. + +#if !defined(STORE_MIGRATE_HPP) +#define STORE_MIGRATE_HPP + +#include "utils/fs/path_fwd.hpp" + +namespace store { + + +namespace detail { + + +utils::fs::path migration_file(const int, const int); +void backup_database(const utils::fs::path&, const int); + + +} // anonymous namespace + + +void migrate_schema(const utils::fs::path&); + + +} // namespace store + +#endif // !defined(STORE_MIGRATE_HPP) diff --git a/store/migrate_test.cpp b/store/migrate_test.cpp new file mode 100644 index 000000000000..b45cc9e5e39e --- /dev/null +++ b/store/migrate_test.cpp @@ -0,0 +1,132 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/migrate.hpp" + +extern "C" { +#include +} + +#include + +#include "store/exceptions.hpp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__backup_database__ok); +ATF_TEST_CASE_BODY(detail__backup_database__ok) +{ + atf::utils::create_file("test.db", "The DB\n"); + store::detail::backup_database(fs::path("test.db"), 13); + ATF_REQUIRE(fs::exists(fs::path("test.db"))); + ATF_REQUIRE(fs::exists(fs::path("test.db.v13.backup"))); + ATF_REQUIRE(atf::utils::compare_file("test.db.v13.backup", "The DB\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__backup_database__ok_overwrite); +ATF_TEST_CASE_BODY(detail__backup_database__ok_overwrite) +{ + atf::utils::create_file("test.db", "Original contents"); + atf::utils::create_file("test.db.v1.backup", "Overwrite me"); + store::detail::backup_database(fs::path("test.db"), 1); + ATF_REQUIRE(fs::exists(fs::path("test.db"))); + ATF_REQUIRE(fs::exists(fs::path("test.db.v1.backup"))); + ATF_REQUIRE(atf::utils::compare_file("test.db.v1.backup", + "Original contents")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__backup_database__fail_open); +ATF_TEST_CASE_BODY(detail__backup_database__fail_open) +{ + ATF_REQUIRE_THROW_RE(store::error, "Cannot open.*foo.db", + store::detail::backup_database(fs::path("foo.db"), 5)); +} + + +ATF_TEST_CASE_WITH_CLEANUP(detail__backup_database__fail_create); +ATF_TEST_CASE_HEAD(detail__backup_database__fail_create) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(detail__backup_database__fail_create) +{ + ATF_REQUIRE(::mkdir("dir", 0755) != -1); + atf::utils::create_file("dir/test.db", "Does not need to be valid"); + ATF_REQUIRE(::chmod("dir", 0111) != -1); + ATF_REQUIRE_THROW_RE( + store::error, "Cannot create.*dir/test.db.v13.backup", + store::detail::backup_database(fs::path("dir/test.db"), 13)); +} +ATF_TEST_CASE_CLEANUP(detail__backup_database__fail_create) +{ + if (::chmod("dir", 0755) == -1) { + // If we cannot restore the original permissions, we cannot do much + // more. However, leaving an unwritable directory behind will cause the + // runtime engine to report us as broken. + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__migration_file__builtin); +ATF_TEST_CASE_BODY(detail__migration_file__builtin) +{ + utils::unsetenv("KYUA_STOREDIR"); + ATF_REQUIRE_EQ(fs::path(KYUA_STOREDIR) / "migrate_v5_v9.sql", + store::detail::migration_file(5, 9)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__migration_file__overriden); +ATF_TEST_CASE_BODY(detail__migration_file__overriden) +{ + utils::setenv("KYUA_STOREDIR", "/tmp/test"); + ATF_REQUIRE_EQ(fs::path("/tmp/test/migrate_v5_v9.sql"), + store::detail::migration_file(5, 9)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, detail__backup_database__ok); + ATF_ADD_TEST_CASE(tcs, detail__backup_database__ok_overwrite); + ATF_ADD_TEST_CASE(tcs, detail__backup_database__fail_open); + ATF_ADD_TEST_CASE(tcs, detail__backup_database__fail_create); + + ATF_ADD_TEST_CASE(tcs, detail__migration_file__builtin); + ATF_ADD_TEST_CASE(tcs, detail__migration_file__overriden); + + // Tests for migrate_schema are in schema_inttest. This is because, for + // such tests to be meaningful, they need to be integration tests and don't + // really fit the goal of this unit-test module. +} diff --git a/store/migrate_v1_v2.sql b/store/migrate_v1_v2.sql new file mode 100644 index 000000000000..52d2f6a8e00c --- /dev/null +++ b/store/migrate_v1_v2.sql @@ -0,0 +1,357 @@ +-- Copyright 2013 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/v1-to-v2.sql +-- Migration of a database with version 1 of the schema to version 2. +-- +-- Version 2 appeared in revision 9a73561a1e3975bba4cbfd19aee6b2365a39519e +-- and its changes were: +-- +-- * Changed the primary key of the metadata table to be the +-- schema_version, not the timestamp. Because timestamps only have +-- second resolution, the old schema made testing of schema migrations +-- difficult. +-- +-- * Introduced the metadatas table, which holds the metadata of all test +-- programs and test cases in an abstract manner regardless of their +-- interface. +-- +-- * Added the metadata_id field to the test_programs and test_cases +-- tables, referencing the new metadatas table. +-- +-- * Changed the precision of the timeout metadata field to be in seconds +-- rather than in microseconds. There is no data loss, and the code that +-- writes the metadata is simplified. +-- +-- * Removed the atf_* and plain_* tables. +-- +-- * Added missing indexes to improve the performance of reports. +-- +-- * Added missing column affinities to the absolute_path and relative_path +-- columns of the test_programs table. + + +-- TODO(jmmv): Implement addition of missing affinities. + + +-- +-- Change primary key of the metadata table. +-- + + +CREATE TABLE new_metadata ( + schema_version INTEGER PRIMARY KEY CHECK (schema_version >= 1), + timestamp TIMESTAMP NOT NULL CHECK (timestamp >= 0) +); + +INSERT INTO new_metadata (schema_version, timestamp) + SELECT schema_version, timestamp FROM metadata; + +DROP TABLE metadata; +ALTER TABLE new_metadata RENAME TO metadata; + + +-- +-- Add the new tables, columns and indexes. +-- + + +CREATE TABLE metadatas ( + metadata_id INTEGER NOT NULL, + property_name TEXT NOT NULL, + property_value TEXT, + + PRIMARY KEY (metadata_id, property_name) +); + + +-- Upgrade the test_programs table by adding missing column affinities and +-- the new metadata_id column. +CREATE TABLE new_test_programs ( + test_program_id INTEGER PRIMARY KEY AUTOINCREMENT, + action_id INTEGER REFERENCES actions, + + absolute_path TEXT NOT NULL, + root TEXT NOT NULL, + relative_path TEXT NOT NULL, + test_suite_name TEXT NOT NULL, + metadata_id INTEGER, + interface TEXT NOT NULL +); +PRAGMA foreign_keys = OFF; +INSERT INTO new_test_programs (test_program_id, action_id, absolute_path, + root, relative_path, test_suite_name, + interface) + SELECT test_program_id, action_id, absolute_path, root, relative_path, + test_suite_name, interface FROM test_programs; +DROP TABLE test_programs; +ALTER TABLE new_test_programs RENAME TO test_programs; +PRAGMA foreign_keys = ON; + + +ALTER TABLE test_cases ADD COLUMN metadata_id INTEGER; + + +CREATE INDEX index_metadatas_by_id + ON metadatas (metadata_id); +CREATE INDEX index_test_programs_by_action_id + ON test_programs (action_id); +CREATE INDEX index_test_cases_by_test_programs_id + ON test_cases (test_program_id); + + +-- +-- Data migration +-- +-- This is, by far, the trickiest part of the migration. +-- TODO(jmmv): Describe the trickiness in here. +-- + + +-- Auxiliary table to construct the final contents of the metadatas table. +-- +-- We construct the contents by writing a row for every metadata property of +-- every test program and test case. Entries corresponding to a test program +-- will have the test_program_id field set to not NULL and entries corresponding +-- to test cases will have the test_case_id set to not NULL. +-- +-- The tricky part, however, is to create the individual identifiers for every +-- metadata entry. We do this by picking the minimum ROWID of a particular set +-- of properties that map to a single test_program_id or test_case_id. +CREATE TABLE tmp_metadatas ( + test_program_id INTEGER DEFAULT NULL, + test_case_id INTEGER DEFAULT NULL, + interface TEXT NOT NULL, + property_name TEXT NOT NULL, + property_value TEXT NOT NULL, + + UNIQUE (test_program_id, test_case_id, property_name) +); +CREATE INDEX index_tmp_metadatas_by_test_case_id + ON tmp_metadatas (test_case_id); +CREATE INDEX index_tmp_metadatas_by_test_program_id + ON tmp_metadatas (test_program_id); + + +-- Populate default metadata values for all test programs and test cases. +-- +-- We do this first to ensure that all test programs and test cases have +-- explicit values for their metadata. Because we want to keep historical data +-- for the tests, we must record these values unconditionally instead of relying +-- on the built-in values in the code. +-- +-- Once this is done, we override any values explicity set by the tests. +CREATE TABLE tmp_default_metadata ( + default_name TEXT PRIMARY KEY, + default_value TEXT NOT NULL +); +INSERT INTO tmp_default_metadata VALUES ('allowed_architectures', ''); +INSERT INTO tmp_default_metadata VALUES ('allowed_platforms', ''); +INSERT INTO tmp_default_metadata VALUES ('description', ''); +INSERT INTO tmp_default_metadata VALUES ('has_cleanup', 'false'); +INSERT INTO tmp_default_metadata VALUES ('required_configs', ''); +INSERT INTO tmp_default_metadata VALUES ('required_files', ''); +INSERT INTO tmp_default_metadata VALUES ('required_memory', '0'); +INSERT INTO tmp_default_metadata VALUES ('required_programs', ''); +INSERT INTO tmp_default_metadata VALUES ('required_user', ''); +INSERT INTO tmp_default_metadata VALUES ('timeout', '300'); +INSERT INTO tmp_metadatas + SELECT test_program_id, NULL, interface, default_name, default_value + FROM test_programs JOIN tmp_default_metadata; +INSERT INTO tmp_metadatas + SELECT NULL, test_case_id, interface, default_name, default_value + FROM test_programs JOIN test_cases + ON test_cases.test_program_id = test_programs.test_program_id + JOIN tmp_default_metadata; +DROP TABLE tmp_default_metadata; + + +-- Populate metadata overrides from plain test programs. +UPDATE tmp_metadatas + SET property_value = ( + SELECT CAST(timeout / 1000000 AS TEXT) FROM plain_test_programs AS aux + WHERE aux.test_program_id = tmp_metadatas.test_program_id) + WHERE test_program_id IS NOT NULL AND property_name = 'timeout' + AND interface = 'plain'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT DISTINCT CAST(timeout / 1000000 AS TEXT) + FROM test_cases AS aux JOIN plain_test_programs + ON aux.test_program_id == plain_test_programs.test_program_id + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'timeout' + AND interface = 'plain'; + + +CREATE INDEX index_tmp_atf_test_cases_multivalues_by_test_case_id + ON atf_test_cases_multivalues (test_case_id); + + +-- Populate metadata overrides from ATF test cases. +UPDATE atf_test_cases SET description = '' WHERE description IS NULL; +UPDATE atf_test_cases SET required_user = '' WHERE required_user IS NULL; + +UPDATE tmp_metadatas + SET property_value = ( + SELECT description FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'description' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT has_cleanup FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'has_cleanup' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT CAST(timeout / 1000000 AS TEXT) FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'timeout' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT CAST(required_memory AS TEXT) FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'required_memory' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT required_user FROM atf_test_cases AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id) + WHERE test_case_id IS NOT NULL AND property_name = 'required_user' + AND interface = 'atf'; +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.arch') + WHERE test_case_id IS NOT NULL AND property_name = 'allowed_architectures' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.arch'); +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.machine') + WHERE test_case_id IS NOT NULL AND property_name = 'allowed_platforms' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.machine'); +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.config') + WHERE test_case_id IS NOT NULL AND property_name = 'required_configs' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.config'); +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.files') + WHERE test_case_id IS NOT NULL AND property_name = 'required_files' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.files'); +UPDATE tmp_metadatas + SET property_value = ( + SELECT GROUP_CONCAT(aux.property_value, ' ') + FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id AND + aux.property_name = 'require.progs') + WHERE test_case_id IS NOT NULL AND property_name = 'required_programs' + AND interface = 'atf' + AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux + WHERE aux.test_case_id = tmp_metadatas.test_case_id + AND property_name = 'require.progs'); + + +-- Fill metadata_id pointers in the test_programs and test_cases tables. +UPDATE test_programs + SET metadata_id = ( + SELECT MIN(ROWID) FROM tmp_metadatas + WHERE tmp_metadatas.test_program_id = test_programs.test_program_id + ); +UPDATE test_cases + SET metadata_id = ( + SELECT MIN(ROWID) FROM tmp_metadatas + WHERE tmp_metadatas.test_case_id = test_cases.test_case_id + ); + + +-- Populate the metadatas table based on tmp_metadatas. +INSERT INTO metadatas (metadata_id, property_name, property_value) + SELECT ( + SELECT MIN(ROWID) FROM tmp_metadatas AS s + WHERE s.test_program_id = tmp_metadatas.test_program_id + ), property_name, property_value + FROM tmp_metadatas WHERE test_program_id IS NOT NULL; +INSERT INTO metadatas (metadata_id, property_name, property_value) + SELECT ( + SELECT MIN(ROWID) FROM tmp_metadatas AS s + WHERE s.test_case_id = tmp_metadatas.test_case_id + ), property_name, property_value + FROM tmp_metadatas WHERE test_case_id IS NOT NULL; + + +-- Drop temporary entities used during the migration. +DROP INDEX index_tmp_atf_test_cases_multivalues_by_test_case_id; +DROP INDEX index_tmp_metadatas_by_test_program_id; +DROP INDEX index_tmp_metadatas_by_test_case_id; +DROP TABLE tmp_metadatas; + + +-- +-- Drop obsolete tables. +-- + + +DROP TABLE atf_test_cases; +DROP TABLE atf_test_cases_multivalues; +DROP TABLE plain_test_programs; + + +-- +-- Update the metadata version. +-- + + +INSERT INTO metadata (timestamp, schema_version) + VALUES (strftime('%s', 'now'), 2); diff --git a/store/migrate_v2_v3.sql b/store/migrate_v2_v3.sql new file mode 100644 index 000000000000..7e6061cccf11 --- /dev/null +++ b/store/migrate_v2_v3.sql @@ -0,0 +1,120 @@ +-- Copyright 2014 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/v2-to-v3.sql +-- Migration of a database with version 2 of the schema to version 3. +-- +-- Version 3 appeared in revision 084d740b1da635946d153475156e335ddfc4aed6 +-- and its changes were: +-- +-- * Removal of historical data. +-- +-- Because from v2 to v3 we went from a unified database to many separate +-- databases, this file is parameterized on @ACTION_ID@. The file has to +-- be executed once per action with this string replaced. + + +ATTACH DATABASE "@OLD_DATABASE@" AS old_store; + + +-- New database already contains a record for v3. Just import older entries. +INSERT INTO metadata SELECT * FROM old_store.metadata; + +INSERT INTO contexts + SELECT cwd + FROM old_store.actions + NATURAL JOIN old_store.contexts + WHERE action_id == @ACTION_ID@; + +INSERT INTO env_vars + SELECT var_name, var_value + FROM old_store.actions + NATURAL JOIN old_store.contexts + NATURAL JOIN old_store.env_vars + WHERE action_id == @ACTION_ID@; + +INSERT INTO metadatas + SELECT metadata_id, property_name, property_value + FROM old_store.metadatas + WHERE metadata_id IN ( + SELECT test_programs.metadata_id + FROM old_store.test_programs + WHERE action_id == @ACTION_ID@ + ) OR metadata_id IN ( + SELECT test_cases.metadata_id + FROM old_store.test_programs JOIN old_store.test_cases + ON test_programs.test_program_id == test_cases.test_program_id + WHERE action_id == @ACTION_ID@ + ); + +INSERT INTO test_programs + SELECT test_program_id, absolute_path, root, relative_path, + test_suite_name, metadata_id, interface + FROM old_store.test_programs + WHERE action_id == @ACTION_ID@; + +INSERT INTO test_cases + SELECT test_cases.test_case_id, test_cases.test_program_id, + test_cases.name, test_cases.metadata_id + FROM old_store.test_cases JOIN old_store.test_programs + ON test_cases.test_program_id == test_programs.test_program_id + WHERE action_id == @ACTION_ID@; + +INSERT INTO test_results + SELECT test_results.test_case_id, test_results.result_type, + test_results.result_reason, test_results.start_time, test_results.end_time + FROM old_store.test_results + JOIN old_store.test_cases + ON test_results.test_case_id == test_cases.test_case_id + JOIN old_store.test_programs + ON test_cases.test_program_id == test_programs.test_program_id + WHERE action_id == @ACTION_ID@; + +INSERT INTO files + SELECT files.file_id, files.contents + FROM old_store.files + JOIN old_store.test_case_files + ON files.file_id == test_case_files.file_id + JOIN old_store.test_cases + ON test_case_files.test_case_id == test_cases.test_case_id + JOIN old_store.test_programs + ON test_cases.test_program_id == test_programs.test_program_id + WHERE action_id == @ACTION_ID@; + +INSERT INTO test_case_files + SELECT test_case_files.test_case_id, test_case_files.file_name, + test_case_files.file_id + FROM old_store.test_case_files + JOIN old_store.test_cases + ON test_case_files.test_case_id == test_cases.test_case_id + JOIN old_store.test_programs + ON test_cases.test_program_id == test_programs.test_program_id + WHERE action_id == @ACTION_ID@; + + +DETACH DATABASE old_store; diff --git a/store/read_backend.cpp b/store/read_backend.cpp new file mode 100644 index 000000000000..bc5b860d402c --- /dev/null +++ b/store/read_backend.cpp @@ -0,0 +1,160 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/read_backend.hpp" + +#include "store/exceptions.hpp" +#include "store/metadata.hpp" +#include "store/read_transaction.hpp" +#include "store/write_backend.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + + +/// Opens a database and defines session pragmas. +/// +/// This auxiliary function ensures that, every time we open a SQLite database, +/// we define the same set of pragmas for it. +/// +/// \param file The database file to be opened. +/// \param flags The flags for the open; see sqlite::database::open. +/// +/// \return The opened database. +/// +/// \throw store::error If there is a problem opening or creating the database. +sqlite::database +store::detail::open_and_setup(const fs::path& file, const int flags) +{ + try { + sqlite::database database = sqlite::database::open(file, flags); + database.exec("PRAGMA foreign_keys = ON"); + return database; + } catch (const sqlite::error& e) { + throw store::error(F("Cannot open '%s': %s") % file % e.what()); + } +} + + +/// Internal implementation for the backend. +struct store::read_backend::impl : utils::noncopyable { + /// The SQLite database this backend talks to. + sqlite::database database; + + /// Constructor. + /// + /// \param database_ The SQLite database instance. + /// \param metadata_ The metadata for the loaded database. This must match + /// the schema version we implement in this module; otherwise, a + /// migration is necessary. + /// + /// \throw integrity_error If the schema in the database is too modern, + /// which might indicate some form of corruption or an old binary. + /// \throw old_schema_error If the schema in the database is older than our + /// currently-implemented version and needs an upgrade. The caller can + /// use migrate_schema() to fix this problem. + impl(sqlite::database& database_, const metadata& metadata_) : + database(database_) + { + const int database_version = metadata_.schema_version(); + + if (database_version == detail::current_schema_version) { + // OK. + } else if (database_version < detail::current_schema_version) { + throw old_schema_error(database_version); + } else if (database_version > detail::current_schema_version) { + throw integrity_error( + F("Database at schema version %s, which is newer than the " + "supported version %s") + % database_version % detail::current_schema_version); + } + } +}; + + +/// Constructs a new backend. +/// +/// \param pimpl_ The internal data. +store::read_backend::read_backend(impl* pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Destructor. +store::read_backend::~read_backend(void) +{ +} + + +/// Opens a database in read-only mode. +/// +/// \param file The database file to be opened. +/// +/// \return The backend representation. +/// +/// \throw store::error If there is any problem opening the database. +store::read_backend +store::read_backend::open_ro(const fs::path& file) +{ + sqlite::database db = detail::open_and_setup(file, sqlite::open_readonly); + return read_backend(new impl(db, metadata::fetch_latest(db))); +} + + +/// Closes the SQLite database. +void +store::read_backend::close(void) +{ + _pimpl->database.close(); +} + + +/// Gets the connection to the SQLite database. +/// +/// \return A database connection. +sqlite::database& +store::read_backend::database(void) +{ + return _pimpl->database; +} + + +/// Opens a read-only transaction. +/// +/// \return A new transaction. +store::read_transaction +store::read_backend::start_read(void) +{ + return read_transaction(*this); +} diff --git a/store/read_backend.hpp b/store/read_backend.hpp new file mode 100644 index 000000000000..2ddb6e650c86 --- /dev/null +++ b/store/read_backend.hpp @@ -0,0 +1,77 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/read_backend.hpp +/// Interface to the backend database for read-only operations. + +#if !defined(STORE_READ_BACKEND_HPP) +#define STORE_READ_BACKEND_HPP + +#include "store/read_backend_fwd.hpp" + +#include + +#include "store/read_transaction_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/sqlite/database_fwd.hpp" + +namespace store { + + +namespace detail { + + +utils::sqlite::database open_and_setup(const utils::fs::path&, const int); + + +} // anonymous namespace + + +/// Public interface to the database store for read-only operations. +class read_backend { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + read_backend(impl*); + +public: + ~read_backend(void); + + static read_backend open_ro(const utils::fs::path&); + void close(void); + + utils::sqlite::database& database(void); + read_transaction start_read(void); +}; + + +} // namespace store + +#endif // !defined(STORE_READ_BACKEND_HPP) diff --git a/store/read_backend_fwd.hpp b/store/read_backend_fwd.hpp new file mode 100644 index 000000000000..4d7f5aa1429b --- /dev/null +++ b/store/read_backend_fwd.hpp @@ -0,0 +1,43 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/read_backend_fwd.hpp +/// Forward declarations for store/read_backend.hpp + +#if !defined(STORE_READ_BACKEND_FWD_HPP) +#define STORE_READ_BACKEND_FWD_HPP + +namespace store { + + +class read_backend; + + +} // namespace store + +#endif // !defined(STORE_READ_BACKEND_FWD_HPP) diff --git a/store/read_backend_test.cpp b/store/read_backend_test.cpp new file mode 100644 index 000000000000..062966cd226d --- /dev/null +++ b/store/read_backend_test.cpp @@ -0,0 +1,152 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/read_backend.hpp" + +#include + +#include "store/exceptions.hpp" +#include "store/metadata.hpp" +#include "store/write_backend.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" + +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__open_and_setup__ok); +ATF_TEST_CASE_BODY(detail__open_and_setup__ok) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + db.exec("CREATE TABLE one (foo INTEGER PRIMARY KEY AUTOINCREMENT);"); + db.exec("CREATE TABLE two (foo INTEGER REFERENCES one);"); + db.close(); + } + + sqlite::database db = store::detail::open_and_setup( + fs::path("test.db"), sqlite::open_readwrite); + db.exec("INSERT INTO one (foo) VALUES (12);"); + // Ensure foreign keys have been enabled. + db.exec("INSERT INTO two (foo) VALUES (12);"); + ATF_REQUIRE_THROW(sqlite::error, + db.exec("INSERT INTO two (foo) VALUES (34);")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__open_and_setup__missing_file); +ATF_TEST_CASE_BODY(detail__open_and_setup__missing_file) +{ + ATF_REQUIRE_THROW_RE(store::error, "Cannot open 'missing.db': ", + store::detail::open_and_setup(fs::path("missing.db"), + sqlite::open_readonly)); + ATF_REQUIRE(!fs::exists(fs::path("missing.db"))); +} + + +ATF_TEST_CASE(read_backend__open_ro__ok); +ATF_TEST_CASE_HEAD(read_backend__open_ro__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(read_backend__open_ro__ok) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + store::detail::initialize(db); + } + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_backend__open_ro__missing_file); +ATF_TEST_CASE_BODY(read_backend__open_ro__missing_file) +{ + ATF_REQUIRE_THROW_RE(store::error, "Cannot open 'missing.db': ", + store::read_backend::open_ro(fs::path("missing.db"))); + ATF_REQUIRE(!fs::exists(fs::path("missing.db"))); +} + + +ATF_TEST_CASE(read_backend__open_ro__integrity_error); +ATF_TEST_CASE_HEAD(read_backend__open_ro__integrity_error) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(read_backend__open_ro__integrity_error) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + store::detail::initialize(db); + db.exec("DELETE FROM metadata"); + } + ATF_REQUIRE_THROW_RE(store::integrity_error, "metadata.*empty", + store::read_backend::open_ro(fs::path("test.db"))); +} + + +ATF_TEST_CASE(read_backend__close); +ATF_TEST_CASE_HEAD(read_backend__close) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(read_backend__close) +{ + store::write_backend::open_rw(fs::path("test.db")); // Create database. + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); + backend.close(); + ATF_REQUIRE_THROW(utils::sqlite::error, + backend.database().exec("SELECT * FROM metadata")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, detail__open_and_setup__ok); + ATF_ADD_TEST_CASE(tcs, detail__open_and_setup__missing_file); + + ATF_ADD_TEST_CASE(tcs, read_backend__open_ro__ok); + ATF_ADD_TEST_CASE(tcs, read_backend__open_ro__missing_file); + ATF_ADD_TEST_CASE(tcs, read_backend__open_ro__integrity_error); + ATF_ADD_TEST_CASE(tcs, read_backend__close); +} diff --git a/store/read_transaction.cpp b/store/read_transaction.cpp new file mode 100644 index 000000000000..68539c8346e0 --- /dev/null +++ b/store/read_transaction.cpp @@ -0,0 +1,532 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/read_transaction.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/dbtypes.hpp" +#include "store/exceptions.hpp" +#include "store/read_backend.hpp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" +#include "utils/sqlite/transaction.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::optional; + + +namespace { + + +/// Retrieves the environment variables of the context. +/// +/// \param db The SQLite database. +/// +/// \return The environment variables of the specified context. +/// +/// \throw sqlite::error If there is a problem loading the variables. +static std::map< std::string, std::string > +get_env_vars(sqlite::database& db) +{ + std::map< std::string, std::string > env; + + sqlite::statement stmt = db.create_statement( + "SELECT var_name, var_value FROM env_vars"); + + while (stmt.step()) { + const std::string name = stmt.safe_column_text("var_name"); + const std::string value = stmt.safe_column_text("var_value"); + env[name] = value; + } + + return env; +} + + +/// Retrieves a metadata object. +/// +/// \param db The SQLite database. +/// \param metadata_id The identifier of the metadata. +/// +/// \return A new metadata object. +static model::metadata +get_metadata(sqlite::database& db, const int64_t metadata_id) +{ + model::metadata_builder builder; + + sqlite::statement stmt = db.create_statement( + "SELECT * FROM metadatas WHERE metadata_id == :metadata_id"); + stmt.bind(":metadata_id", metadata_id); + while (stmt.step()) { + const std::string name = stmt.safe_column_text("property_name"); + const std::string value = stmt.safe_column_text("property_value"); + builder.set_string(name, value); + } + + return builder.build(); +} + + +/// Gets a file from the database. +/// +/// \param db The database to query the file from. +/// \param file_id The identifier of the file to be queried. +/// +/// \return A textual representation of the file contents. +/// +/// \throw integrity_error If there is any problem in the loaded data or if the +/// file cannot be found. +static std::string +get_file(sqlite::database& db, const int64_t file_id) +{ + sqlite::statement stmt = db.create_statement( + "SELECT contents FROM files WHERE file_id == :file_id"); + stmt.bind(":file_id", file_id); + if (!stmt.step()) + throw store::integrity_error(F("Cannot find referenced file %s") % + file_id); + + try { + const sqlite::blob raw_contents = stmt.safe_column_blob("contents"); + const std::string contents( + static_cast< const char *>(raw_contents.memory), raw_contents.size); + + const bool more = stmt.step(); + INV(!more); + + return contents; + } catch (const sqlite::error& e) { + throw store::integrity_error(e.what()); + } +} + + +/// Gets all the test cases within a particular test program. +/// +/// \param db The database to query the information from. +/// \param test_program_id The identifier of the test program whose test cases +/// to query. +/// +/// \return The collection of loaded test cases. +/// +/// \throw integrity_error If there is any problem in the loaded data. +static model::test_cases_map +get_test_cases(sqlite::database& db, const int64_t test_program_id) +{ + model::test_cases_map_builder test_cases; + + sqlite::statement stmt = db.create_statement( + "SELECT name, metadata_id " + "FROM test_cases WHERE test_program_id == :test_program_id"); + stmt.bind(":test_program_id", test_program_id); + while (stmt.step()) { + const std::string name = stmt.safe_column_text("name"); + const int64_t metadata_id = stmt.safe_column_int64("metadata_id"); + + const model::metadata metadata = get_metadata(db, metadata_id); + LD(F("Loaded test case '%s'") % name); + test_cases.add(name, metadata); + } + + return test_cases.build(); +} + + +/// Retrieves a result from the database. +/// +/// \param stmt The statement with the data for the result to load. +/// \param type_column The name of the column containing the type of the result. +/// \param reason_column The name of the column containing the reason for the +/// result, if any. +/// +/// \return The loaded result. +/// +/// \throw integrity_error If the data in the database is invalid. +static model::test_result +parse_result(sqlite::statement& stmt, const char* type_column, + const char* reason_column) +{ + try { + const model::test_result_type type = + store::column_test_result_type(stmt, type_column); + if (type == model::test_result_passed) { + if (stmt.column_type(stmt.column_id(reason_column)) != + sqlite::type_null) + throw store::integrity_error("Result of type 'passed' has a " + "non-NULL reason"); + return model::test_result(type); + } else { + return model::test_result(type, + stmt.safe_column_text(reason_column)); + } + } catch (const sqlite::error& e) { + throw store::integrity_error(e.what()); + } +} + + +} // anonymous namespace + + +/// Loads a specific test program from the database. +/// +/// \param backend_ The store backend we are dealing with. +/// \param id The identifier of the test program to load. +/// +/// \return The instantiated test program. +/// +/// \throw integrity_error If the data read from the database cannot be properly +/// interpreted. +model::test_program_ptr +store::detail::get_test_program(read_backend& backend_, const int64_t id) +{ + sqlite::database& db = backend_.database(); + + model::test_program_ptr test_program; + sqlite::statement stmt = db.create_statement( + "SELECT * FROM test_programs WHERE test_program_id == :id"); + stmt.bind(":id", id); + stmt.step(); + const std::string interface = stmt.safe_column_text("interface"); + test_program.reset(new model::test_program( + interface, + fs::path(stmt.safe_column_text("relative_path")), + fs::path(stmt.safe_column_text("root")), + stmt.safe_column_text("test_suite_name"), + get_metadata(db, stmt.safe_column_int64("metadata_id")), + get_test_cases(db, id))); + const bool more = stmt.step(); + INV(!more); + + LD(F("Loaded test program '%s'") % test_program->relative_path()); + return test_program; +} + + +/// Internal implementation for a results iterator. +struct store::results_iterator::impl : utils::noncopyable { + /// The store backend we are dealing with. + store::read_backend _backend; + + /// The statement to iterate on. + sqlite::statement _stmt; + + /// A cache for the last loaded test program. + optional< std::pair< int64_t, model::test_program_ptr > > + _last_test_program; + + /// Whether the iterator is still valid or not. + bool _valid; + + /// Constructor. + /// + /// \param backend_ The store backend implementation. + impl(store::read_backend& backend_) : + _backend(backend_), + _stmt(backend_.database().create_statement( + "SELECT test_programs.test_program_id, " + " test_programs.interface, " + " test_cases.test_case_id, test_cases.name, " + " test_results.result_type, test_results.result_reason, " + " test_results.start_time, test_results.end_time " + "FROM test_programs " + " JOIN test_cases " + " ON test_programs.test_program_id = test_cases.test_program_id " + " JOIN test_results " + " ON test_cases.test_case_id = test_results.test_case_id " + "ORDER BY test_programs.absolute_path, test_cases.name")) + { + _valid = _stmt.step(); + } +}; + + +/// Constructor. +/// +/// \param pimpl_ The internal implementation details of the iterator. +store::results_iterator::results_iterator( + std::shared_ptr< impl > pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Destructor. +store::results_iterator::~results_iterator(void) +{ +} + + +/// Moves the iterator forward by one result. +/// +/// \return The iterator itself. +store::results_iterator& +store::results_iterator::operator++(void) +{ + _pimpl->_valid = _pimpl->_stmt.step(); + return *this; +} + + +/// Checks whether the iterator is still valid. +/// +/// \return True if there is more elements to iterate on, false otherwise. +store::results_iterator::operator bool(void) const +{ + return _pimpl->_valid; +} + + +/// Gets the test program this result belongs to. +/// +/// \return The representation of a test program. +const model::test_program_ptr +store::results_iterator::test_program(void) const +{ + const int64_t id = _pimpl->_stmt.safe_column_int64("test_program_id"); + if (!_pimpl->_last_test_program || + _pimpl->_last_test_program.get().first != id) + { + const model::test_program_ptr tp = detail::get_test_program( + _pimpl->_backend, id); + _pimpl->_last_test_program = std::make_pair(id, tp); + } + return _pimpl->_last_test_program.get().second; +} + + +/// Gets the name of the test case pointed by the iterator. +/// +/// The caller can look up the test case data by using the find() method on the +/// test program returned by test_program(). +/// +/// \return A test case name, unique within the test program. +std::string +store::results_iterator::test_case_name(void) const +{ + return _pimpl->_stmt.safe_column_text("name"); +} + + +/// Gets the result of the test case pointed by the iterator. +/// +/// \return A test case result. +model::test_result +store::results_iterator::result(void) const +{ + return parse_result(_pimpl->_stmt, "result_type", "result_reason"); +} + + +/// Gets the start time of the test case execution. +/// +/// \return The time when the test started execution. +datetime::timestamp +store::results_iterator::start_time(void) const +{ + return column_timestamp(_pimpl->_stmt, "start_time"); +} + + +/// Gets the end time of the test case execution. +/// +/// \return The time when the test finished execution. +datetime::timestamp +store::results_iterator::end_time(void) const +{ + return column_timestamp(_pimpl->_stmt, "end_time"); +} + + +/// Gets a file from a test case. +/// +/// \param db The database to query the file from. +/// \param test_case_id The identifier of the test case. +/// \param filename The name of the file to be retrieved. +/// +/// \return A textual representation of the file contents. +/// +/// \throw integrity_error If there is any problem in the loaded data or if the +/// file cannot be found. +static std::string +get_test_case_file(sqlite::database& db, const int64_t test_case_id, + const char* filename) +{ + sqlite::statement stmt = db.create_statement( + "SELECT file_id FROM test_case_files " + "WHERE test_case_id == :test_case_id AND file_name == :file_name"); + stmt.bind(":test_case_id", test_case_id); + stmt.bind(":file_name", filename); + if (stmt.step()) + return get_file(db, stmt.safe_column_int64("file_id")); + else + return ""; +} + + +/// Gets the contents of stdout of a test case. +/// +/// \return A textual representation of the stdout contents of the test case. +/// This may of course be empty if the test case didn't print anything. +std::string +store::results_iterator::stdout_contents(void) const +{ + return get_test_case_file(_pimpl->_backend.database(), + _pimpl->_stmt.safe_column_int64("test_case_id"), + "__STDOUT__"); +} + + +/// Gets the contents of stderr of a test case. +/// +/// \return A textual representation of the stderr contents of the test case. +/// This may of course be empty if the test case didn't print anything. +std::string +store::results_iterator::stderr_contents(void) const +{ + return get_test_case_file(_pimpl->_backend.database(), + _pimpl->_stmt.safe_column_int64("test_case_id"), + "__STDERR__"); +} + + +/// Internal implementation for a store read-only transaction. +struct store::read_transaction::impl : utils::noncopyable { + /// The backend instance. + store::read_backend& _backend; + + /// The SQLite database this transaction deals with. + sqlite::database _db; + + /// The backing SQLite transaction. + sqlite::transaction _tx; + + /// Opens a transaction. + /// + /// \param backend_ The backend this transaction is connected to. + impl(read_backend& backend_) : + _backend(backend_), + _db(backend_.database()), + _tx(backend_.database().begin_transaction()) + { + } +}; + + +/// Creates a new read-only transaction. +/// +/// \param backend_ The backend this transaction belongs to. +store::read_transaction::read_transaction(read_backend& backend_) : + _pimpl(new impl(backend_)) +{ +} + + +/// Destructor. +store::read_transaction::~read_transaction(void) +{ +} + + +/// Finishes the transaction. +/// +/// This actually commits the result of the transaction, but because the +/// transaction is read-only, we use a different term to denote that there is no +/// distinction between commit and rollback. +/// +/// \throw error If there is any problem when talking to the database. +void +store::read_transaction::finish(void) +{ + try { + _pimpl->_tx.commit(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Retrieves an context from the database. +/// +/// \return The retrieved context. +/// +/// \throw error If there is a problem loading the context. +model::context +store::read_transaction::get_context(void) +{ + try { + sqlite::statement stmt = _pimpl->_db.create_statement( + "SELECT cwd FROM contexts"); + if (!stmt.step()) + throw error("Error loading context: no data"); + + return model::context(fs::path(stmt.safe_column_text("cwd")), + get_env_vars(_pimpl->_db)); + } catch (const sqlite::error& e) { + throw error(F("Error loading context: %s") % e.what()); + } +} + + +/// Creates a new iterator to scan tests results. +/// +/// \return The constructed iterator. +/// +/// \throw error If there is any problem constructing the iterator. +store::results_iterator +store::read_transaction::get_results(void) +{ + try { + return results_iterator(std::shared_ptr< results_iterator::impl >( + new results_iterator::impl(_pimpl->_backend))); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} diff --git a/store/read_transaction.hpp b/store/read_transaction.hpp new file mode 100644 index 000000000000..7dd20db782eb --- /dev/null +++ b/store/read_transaction.hpp @@ -0,0 +1,120 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/read_transaction.hpp +/// Implementation of read-only transactions on the backend. + +#if !defined(STORE_READ_TRANSACTION_HPP) +#define STORE_READ_TRANSACTION_HPP + +#include "store/read_transaction_fwd.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "model/context_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result_fwd.hpp" +#include "store/read_backend_fwd.hpp" +#include "store/read_transaction_fwd.hpp" +#include "utils/datetime_fwd.hpp" + +namespace store { + + +namespace detail { + + +model::test_program_ptr get_test_program(read_backend&, const int64_t); + + +} // namespace detail + + +/// Iterator for the set of test case results that are part of an action. +/// +/// \todo Note that this is not a "standard" C++ iterator. I have chosen to +/// implement a different interface because it makes things easier to represent +/// an SQL statement state. Rewrite as a proper C++ iterator, inheriting from +/// std::iterator. +class results_iterator { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class read_transaction; + results_iterator(std::shared_ptr< impl >); + +public: + ~results_iterator(void); + + results_iterator& operator++(void); + operator bool(void) const; + + const model::test_program_ptr test_program(void) const; + std::string test_case_name(void) const; + model::test_result result(void) const; + utils::datetime::timestamp start_time(void) const; + utils::datetime::timestamp end_time(void) const; + + std::string stdout_contents(void) const; + std::string stderr_contents(void) const; +}; + + +/// Representation of a read-only transaction. +/// +/// Transactions are the entry place for high-level calls that access the +/// database. +class read_transaction { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class read_backend; + read_transaction(read_backend&); + +public: + ~read_transaction(void); + + void finish(void); + + model::context get_context(void); + results_iterator get_results(void); +}; + + +} // namespace store + +#endif // !defined(STORE_READ_TRANSACTION_HPP) diff --git a/store/read_transaction_fwd.hpp b/store/read_transaction_fwd.hpp new file mode 100644 index 000000000000..4aae92d9825c --- /dev/null +++ b/store/read_transaction_fwd.hpp @@ -0,0 +1,44 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/read_transaction_fwd.hpp +/// Forward declarations for store/read_transaction.hpp + +#if !defined(STORE_READ_TRANSACTION_FWD_HPP) +#define STORE_READ_TRANSACTION_FWD_HPP + +namespace store { + + +class read_transaction; +class results_iterator; + + +} // namespace store + +#endif // !defined(STORE_READ_TRANSACTION_FWD_HPP) diff --git a/store/read_transaction_test.cpp b/store/read_transaction_test.cpp new file mode 100644 index 000000000000..711faa674fbe --- /dev/null +++ b/store/read_transaction_test.cpp @@ -0,0 +1,262 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/read_transaction.hpp" + +#include +#include + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "store/read_backend.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE(get_context__missing); +ATF_TEST_CASE_HEAD(get_context__missing) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_context__missing) +{ + store::write_backend::open_rw(fs::path("test.db")); // Create database. + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + + store::read_transaction tx = backend.start_read(); + ATF_REQUIRE_THROW_RE(store::error, "context: no data", tx.get_context()); +} + + +ATF_TEST_CASE(get_context__invalid_cwd); +ATF_TEST_CASE_HEAD(get_context__invalid_cwd) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_context__invalid_cwd) +{ + { + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + + sqlite::statement stmt = backend.database().create_statement( + "INSERT INTO contexts (cwd) VALUES (:cwd)"); + const char buffer[10] = "foo bar"; + stmt.bind(":cwd", sqlite::blob(buffer, sizeof(buffer))); + stmt.step_without_results(); + } + + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + store::read_transaction tx = backend.start_read(); + ATF_REQUIRE_THROW_RE(store::error, "context: .*cwd.*not a string", + tx.get_context()); +} + + +ATF_TEST_CASE(get_context__invalid_env_vars); +ATF_TEST_CASE_HEAD(get_context__invalid_env_vars) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_context__invalid_env_vars) +{ + { + store::write_backend backend = store::write_backend::open_rw( + fs::path("test-bad-name.db")); + backend.database().exec("INSERT INTO contexts (cwd) " + "VALUES ('/foo/bar')"); + const char buffer[10] = "foo bar"; + + sqlite::statement stmt = backend.database().create_statement( + "INSERT INTO env_vars (var_name, var_value) " + "VALUES (:var_name, 'abc')"); + stmt.bind(":var_name", sqlite::blob(buffer, sizeof(buffer))); + stmt.step_without_results(); + } + { + store::read_backend backend = store::read_backend::open_ro( + fs::path("test-bad-name.db")); + store::read_transaction tx = backend.start_read(); + ATF_REQUIRE_THROW_RE(store::error, "context: .*var_name.*not a string", + tx.get_context()); + } + + { + store::write_backend backend = store::write_backend::open_rw( + fs::path("test-bad-value.db")); + backend.database().exec("INSERT INTO contexts (cwd) " + "VALUES ('/foo/bar')"); + const char buffer[10] = "foo bar"; + + sqlite::statement stmt = backend.database().create_statement( + "INSERT INTO env_vars (var_name, var_value) " + "VALUES ('abc', :var_value)"); + stmt.bind(":var_value", sqlite::blob(buffer, sizeof(buffer))); + stmt.step_without_results(); + } + { + store::read_backend backend = store::read_backend::open_ro( + fs::path("test-bad-value.db")); + store::read_transaction tx = backend.start_read(); + ATF_REQUIRE_THROW_RE(store::error, "context: .*var_value.*not a string", + tx.get_context()); + } +} + + +ATF_TEST_CASE(get_results__none); +ATF_TEST_CASE_HEAD(get_results__none) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_results__none) +{ + store::write_backend::open_rw(fs::path("test.db")); // Create database. + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + store::read_transaction tx = backend.start_read(); + store::results_iterator iter = tx.get_results(); + ATF_REQUIRE(!iter); +} + + +ATF_TEST_CASE(get_results__many); +ATF_TEST_CASE_HEAD(get_results__many) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_results__many) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + + store::write_transaction tx = backend.start_write(); + + const model::context context(fs::path("/foo/bar"), + std::map< std::string, std::string >()); + tx.put_context(context); + + const datetime::timestamp start_time1 = datetime::timestamp::from_values( + 2012, 01, 30, 22, 10, 00, 0); + const datetime::timestamp end_time1 = datetime::timestamp::from_values( + 2012, 01, 30, 22, 15, 30, 1234); + const datetime::timestamp start_time2 = datetime::timestamp::from_values( + 2012, 01, 30, 22, 15, 40, 987); + const datetime::timestamp end_time2 = datetime::timestamp::from_values( + 2012, 01, 30, 22, 16, 0, 0); + + atf::utils::create_file("unused.txt", "unused file\n"); + + const model::test_program test_program_1 = model::test_program_builder( + "plain", fs::path("a/prog1"), fs::path("/the/root"), "suite1") + .add_test_case("main") + .build(); + const model::test_result result_1(model::test_result_passed); + { + const int64_t tp_id = tx.put_test_program(test_program_1); + const int64_t tc_id = tx.put_test_case(test_program_1, "main", tp_id); + atf::utils::create_file("prog1.out", "stdout of prog1\n"); + tx.put_test_case_file("__STDOUT__", fs::path("prog1.out"), tc_id); + tx.put_test_case_file("unused.txt", fs::path("unused.txt"), tc_id); + tx.put_result(result_1, tc_id, start_time1, end_time1); + } + + const model::test_program test_program_2 = model::test_program_builder( + "plain", fs::path("b/prog2"), fs::path("/the/root"), "suite2") + .add_test_case("main") + .build(); + const model::test_result result_2(model::test_result_failed, + "Some text"); + { + const int64_t tp_id = tx.put_test_program(test_program_2); + const int64_t tc_id = tx.put_test_case(test_program_2, "main", tp_id); + atf::utils::create_file("prog2.err", "stderr of prog2\n"); + tx.put_test_case_file("__STDERR__", fs::path("prog2.err"), tc_id); + tx.put_test_case_file("unused.txt", fs::path("unused.txt"), tc_id); + tx.put_result(result_2, tc_id, start_time2, end_time2); + } + + tx.commit(); + backend.close(); + + store::read_backend backend2 = store::read_backend::open_ro( + fs::path("test.db")); + store::read_transaction tx2 = backend2.start_read(); + store::results_iterator iter = tx2.get_results(); + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_1, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ("stdout of prog1\n", iter.stdout_contents()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(result_1, iter.result()); + ATF_REQUIRE_EQ(start_time1, iter.start_time()); + ATF_REQUIRE_EQ(end_time1, iter.end_time()); + ATF_REQUIRE(++iter); + ATF_REQUIRE_EQ(test_program_2, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE_EQ("stderr of prog2\n", iter.stderr_contents()); + ATF_REQUIRE_EQ(result_2, iter.result()); + ATF_REQUIRE_EQ(start_time2, iter.start_time()); + ATF_REQUIRE_EQ(end_time2, iter.end_time()); + ATF_REQUIRE(!++iter); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, get_context__missing); + ATF_ADD_TEST_CASE(tcs, get_context__invalid_cwd); + ATF_ADD_TEST_CASE(tcs, get_context__invalid_env_vars); + + ATF_ADD_TEST_CASE(tcs, get_results__none); + ATF_ADD_TEST_CASE(tcs, get_results__many); +} diff --git a/store/schema_inttest.cpp b/store/schema_inttest.cpp new file mode 100644 index 000000000000..cd528b0c48d8 --- /dev/null +++ b/store/schema_inttest.cpp @@ -0,0 +1,492 @@ +// Copyright 2013 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/migrate.hpp" +#include "store/read_backend.hpp" +#include "store/read_transaction.hpp" +#include "store/write_backend.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/stream.hpp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; +namespace units = utils::units; + + +namespace { + + +/// Gets a data file from the tests directory. +/// +/// We cannot use the srcdir property because the required files are not there +/// when building with an object directory. In those cases, the data files +/// remainin the source directory while the resulting test program is in the +/// object directory, thus having the wrong value for its srcdir property. +/// +/// \param name Basename of the test data file to query. +/// +/// \return The actual path to the requested data file. +static fs::path +testdata_file(const std::string& name) +{ + const fs::path testdatadir(utils::getenv_with_default( + "KYUA_STORETESTDATADIR", KYUA_STORETESTDATADIR)); + return testdatadir / name; +} + + +/// Validates the contents of the action with identifier 1. +/// +/// \param dbpath Path to the database in which to check the action contents. +static void +check_action_1(const fs::path& dbpath) +{ + store::read_backend backend = store::read_backend::open_ro(dbpath); + store::read_transaction transaction = backend.start_read(); + + const fs::path root("/some/root"); + std::map< std::string, std::string > environment; + const model::context context(root, environment); + + ATF_REQUIRE_EQ(context, transaction.get_context()); + + store::results_iterator iter = transaction.get_results(); + ATF_REQUIRE(!iter); +} + + +/// Validates the contents of the action with identifier 2. +/// +/// \param dbpath Path to the database in which to check the action contents. +static void +check_action_2(const fs::path& dbpath) +{ + store::read_backend backend = store::read_backend::open_ro(dbpath); + store::read_transaction transaction = backend.start_read(); + + const fs::path root("/test/suite/root"); + std::map< std::string, std::string > environment; + environment["HOME"] = "/home/test"; + environment["PATH"] = "/bin:/usr/bin"; + const model::context context(root, environment); + + ATF_REQUIRE_EQ(context, transaction.get_context()); + + const model::test_program test_program_1 = model::test_program_builder( + "plain", fs::path("foo_test"), fs::path("/test/suite/root"), + "suite-name") + .add_test_case("main") + .build(); + const model::test_result result_1(model::test_result_passed); + + const model::test_program test_program_2 = model::test_program_builder( + "plain", fs::path("subdir/another_test"), fs::path("/test/suite/root"), + "subsuite-name") + .add_test_case("main", + model::metadata_builder() + .set_timeout(datetime::delta(10, 0)) + .build()) + .set_metadata(model::metadata_builder() + .set_timeout(datetime::delta(10, 0)) + .build()) + .build(); + const model::test_result result_2(model::test_result_failed, + "Exited with code 1"); + + const model::test_program test_program_3 = model::test_program_builder( + "plain", fs::path("subdir/bar_test"), fs::path("/test/suite/root"), + "subsuite-name") + .add_test_case("main") + .build(); + const model::test_result result_3(model::test_result_broken, + "Received signal 1"); + + const model::test_program test_program_4 = model::test_program_builder( + "plain", fs::path("top_test"), fs::path("/test/suite/root"), + "suite-name") + .add_test_case("main") + .build(); + const model::test_result result_4(model::test_result_expected_failure, + "Known bug"); + + const model::test_program test_program_5 = model::test_program_builder( + "plain", fs::path("last_test"), fs::path("/test/suite/root"), + "suite-name") + .add_test_case("main") + .build(); + const model::test_result result_5(model::test_result_skipped, + "Does not apply"); + + store::results_iterator iter = transaction.get_results(); + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_1, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_1, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357643611000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643621000500LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_5, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_5, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357643632000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643638000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_2, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_2, iter.result()); + ATF_REQUIRE_EQ("Test stdout", iter.stdout_contents()); + ATF_REQUIRE_EQ("Test stderr", iter.stderr_contents()); + ATF_REQUIRE_EQ(1357643622001200LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643622900021LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_3, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_3, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357643623500000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643630981932LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_4, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_4, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357643631000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357643631020000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(!iter); +} + + +/// Validates the contents of the action with identifier 3. +/// +/// \param dbpath Path to the database in which to check the action contents. +static void +check_action_3(const fs::path& dbpath) +{ + store::read_backend backend = store::read_backend::open_ro(dbpath); + store::read_transaction transaction = backend.start_read(); + + const fs::path root("/usr/tests"); + std::map< std::string, std::string > environment; + environment["PATH"] = "/bin:/usr/bin"; + const model::context context(root, environment); + + ATF_REQUIRE_EQ(context, transaction.get_context()); + + const model::test_program test_program_6 = model::test_program_builder( + "atf", fs::path("complex_test"), fs::path("/usr/tests"), + "suite-name") + .add_test_case("this_passes") + .add_test_case("this_fails", + model::metadata_builder() + .set_description("Test description") + .set_has_cleanup(true) + .set_required_memory(units::bytes(128)) + .set_required_user("root") + .build()) + .add_test_case("this_skips", + model::metadata_builder() + .add_allowed_architecture("powerpc") + .add_allowed_architecture("x86_64") + .add_allowed_platform("amd64") + .add_allowed_platform("macppc") + .add_required_config("X-foo") + .add_required_config("unprivileged_user") + .add_required_file(fs::path("/the/data/file")) + .add_required_program(fs::path("/bin/ls")) + .add_required_program(fs::path("cp")) + .set_description("Test explanation") + .set_has_cleanup(true) + .set_required_memory(units::bytes(512)) + .set_required_user("unprivileged") + .set_timeout(datetime::delta(600, 0)) + .build()) + .build(); + const model::test_result result_6(model::test_result_passed); + const model::test_result result_7(model::test_result_failed, + "Some reason"); + const model::test_result result_8(model::test_result_skipped, + "Another reason"); + + const model::test_program test_program_7 = model::test_program_builder( + "atf", fs::path("simple_test"), fs::path("/usr/tests"), + "subsuite-name") + .add_test_case("main", + model::metadata_builder() + .set_description("More text") + .set_has_cleanup(true) + .set_required_memory(units::bytes(128)) + .set_required_user("unprivileged") + .build()) + .build(); + const model::test_result result_9(model::test_result_failed, + "Exited with code 1"); + + store::results_iterator iter = transaction.get_results(); + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_6, *iter.test_program()); + ATF_REQUIRE_EQ("this_fails", iter.test_case_name()); + ATF_REQUIRE_EQ(result_7, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357648719000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357648720897182LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_6, *iter.test_program()); + ATF_REQUIRE_EQ("this_passes", iter.test_case_name()); + ATF_REQUIRE_EQ(result_6, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357648712000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357648718000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_6, *iter.test_program()); + ATF_REQUIRE_EQ("this_skips", iter.test_case_name()); + ATF_REQUIRE_EQ(result_8, iter.result()); + ATF_REQUIRE_EQ("Another stdout", iter.stdout_contents()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357648729182013LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357648730000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_7, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_9, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE_EQ("Another stderr", iter.stderr_contents()); + ATF_REQUIRE_EQ(1357648740120000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357648750081700LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(!iter); +} + + +/// Validates the contents of the action with identifier 4. +/// +/// \param dbpath Path to the database in which to check the action contents. +static void +check_action_4(const fs::path& dbpath) +{ + store::read_backend backend = store::read_backend::open_ro(dbpath); + store::read_transaction transaction = backend.start_read(); + + const fs::path root("/usr/tests"); + std::map< std::string, std::string > environment; + environment["LANG"] = "C"; + environment["PATH"] = "/bin:/usr/bin"; + environment["TERM"] = "xterm"; + const model::context context(root, environment); + + ATF_REQUIRE_EQ(context, transaction.get_context()); + + const model::test_program test_program_8 = model::test_program_builder( + "plain", fs::path("subdir/another_test"), fs::path("/usr/tests"), + "subsuite-name") + .add_test_case("main", + model::metadata_builder() + .set_timeout(datetime::delta(10, 0)) + .build()) + .set_metadata(model::metadata_builder() + .set_timeout(datetime::delta(10, 0)) + .build()) + .build(); + const model::test_result result_10(model::test_result_failed, + "Exit failure"); + + const model::test_program test_program_9 = model::test_program_builder( + "atf", fs::path("complex_test"), fs::path("/usr/tests"), + "suite-name") + .add_test_case("this_passes") + .add_test_case("this_fails", + model::metadata_builder() + .set_description("Test description") + .set_required_user("root") + .build()) + .build(); + const model::test_result result_11(model::test_result_passed); + const model::test_result result_12(model::test_result_failed, + "Some reason"); + + store::results_iterator iter = transaction.get_results(); + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_9, *iter.test_program()); + ATF_REQUIRE_EQ("this_fails", iter.test_case_name()); + ATF_REQUIRE_EQ(result_12, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357644397100000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357644399005000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_9, *iter.test_program()); + ATF_REQUIRE_EQ("this_passes", iter.test_case_name()); + ATF_REQUIRE_EQ(result_11, iter.result()); + ATF_REQUIRE(iter.stdout_contents().empty()); + ATF_REQUIRE(iter.stderr_contents().empty()); + ATF_REQUIRE_EQ(1357644396500000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357644397000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(iter); + ATF_REQUIRE_EQ(test_program_8, *iter.test_program()); + ATF_REQUIRE_EQ("main", iter.test_case_name()); + ATF_REQUIRE_EQ(result_10, iter.result()); + ATF_REQUIRE_EQ("Test stdout", iter.stdout_contents()); + ATF_REQUIRE_EQ("Test stderr", iter.stderr_contents()); + ATF_REQUIRE_EQ(1357644395000000LL, iter.start_time().to_microseconds()); + ATF_REQUIRE_EQ(1357644396000000LL, iter.end_time().to_microseconds()); + + ++iter; + ATF_REQUIRE(!iter); +} + + +} // anonymous namespace + + +#define CURRENT_SCHEMA_TEST(dataset) \ + ATF_TEST_CASE(current_schema_ ##dataset); \ + ATF_TEST_CASE_HEAD(current_schema_ ##dataset) \ + { \ + logging::set_inmemory(); \ + const std::string required_files = \ + store::detail::schema_file().str() + " " + \ + testdata_file("testdata_v3_" #dataset ".sql").str(); \ + set_md_var("require.files", required_files); \ + } \ + ATF_TEST_CASE_BODY(current_schema_ ##dataset) \ + { \ + const fs::path testpath("test.db"); \ + \ + sqlite::database db = sqlite::database::open( \ + testpath, sqlite::open_readwrite | sqlite::open_create); \ + db.exec(utils::read_file(store::detail::schema_file())); \ + db.exec(utils::read_file(testdata_file(\ + "testdata_v3_" #dataset ".sql"))); \ + db.close(); \ + \ + check_action_ ## dataset (testpath); \ + } +CURRENT_SCHEMA_TEST(1); +CURRENT_SCHEMA_TEST(2); +CURRENT_SCHEMA_TEST(3); +CURRENT_SCHEMA_TEST(4); + + +#define MIGRATE_SCHEMA_TEST(from_version) \ + ATF_TEST_CASE(migrate_schema__from_v ##from_version); \ + ATF_TEST_CASE_HEAD(migrate_schema__from_v ##from_version) \ + { \ + logging::set_inmemory(); \ + \ + const char* schema = "schema_v" #from_version ".sql"; \ + const char* testdata = "testdata_v" #from_version ".sql"; \ + \ + std::string required_files = \ + testdata_file(schema).str() + " " + testdata_file(testdata).str(); \ + for (int i = from_version; i < store::detail::current_schema_version; \ + ++i) \ + required_files += " " + store::detail::migration_file( \ + i, i + 1).str(); \ + \ + set_md_var("require.files", required_files); \ + } \ + ATF_TEST_CASE_BODY(migrate_schema__from_v ##from_version) \ + { \ + const char* schema = "schema_v" #from_version ".sql"; \ + const char* testdata = "testdata_v" #from_version ".sql"; \ + \ + const fs::path testpath("test.db"); \ + \ + sqlite::database db = sqlite::database::open( \ + testpath, sqlite::open_readwrite | sqlite::open_create); \ + db.exec(utils::read_file(testdata_file(schema))); \ + db.exec(utils::read_file(testdata_file(testdata))); \ + db.close(); \ + \ + store::migrate_schema(fs::path("test.db")); \ + \ + check_action_2(fs::path(".kyua/store/" \ + "results.test_suite_root.20130108-111331-000000.db")); \ + check_action_3(fs::path(".kyua/store/" \ + "results.usr_tests.20130108-123832-000000.db")); \ + check_action_4(fs::path(".kyua/store/" \ + "results.usr_tests.20130108-112635-000000.db")); \ + } +MIGRATE_SCHEMA_TEST(1); +MIGRATE_SCHEMA_TEST(2); + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, current_schema_1); + ATF_ADD_TEST_CASE(tcs, current_schema_2); + ATF_ADD_TEST_CASE(tcs, current_schema_3); + ATF_ADD_TEST_CASE(tcs, current_schema_4); + + ATF_ADD_TEST_CASE(tcs, migrate_schema__from_v1); + ATF_ADD_TEST_CASE(tcs, migrate_schema__from_v2); +} diff --git a/store/schema_v1.sql b/store/schema_v1.sql new file mode 100644 index 000000000000..fbc7355bcd85 --- /dev/null +++ b/store/schema_v1.sql @@ -0,0 +1,314 @@ +-- Copyright 2011 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/schema_v1.sql +-- Definition of the database schema. +-- +-- The whole contents of this file are wrapped in a transaction. We want +-- to ensure that the initial contents of the database (the table layout as +-- well as any predefined values) are written atomically to simplify error +-- handling in our code. + + +BEGIN TRANSACTION; + + +-- ------------------------------------------------------------------------- +-- Metadata. +-- ------------------------------------------------------------------------- + + +-- Database-wide properties. +-- +-- Rows in this table are immutable: modifying the metadata implies writing +-- a new record with a larger timestamp value, and never updating previous +-- records. When extracting data from this table, the only "valid" row is +-- the one with the highest timestamp. All the other rows are meaningless. +-- +-- In other words, this table keeps the history of the database metadata. +-- The only reason for doing this is for debugging purposes. It may come +-- in handy to know when a particular database-wide operation happened if +-- it turns out that the database got corrupted. +CREATE TABLE metadata ( + timestamp TIMESTAMP PRIMARY KEY CHECK (timestamp >= 0), + schema_version INTEGER NOT NULL CHECK (schema_version >= 1) +); + + +-- ------------------------------------------------------------------------- +-- Contexts. +-- ------------------------------------------------------------------------- + + +-- Execution contexts. +-- +-- A context represents the execution environment of a particular action. +-- Because every action is invoked by the user, the context may have +-- changed. We record such information for information and debugging +-- purposes. +CREATE TABLE contexts ( + context_id INTEGER PRIMARY KEY AUTOINCREMENT, + cwd TEXT NOT NULL + + -- TODO(jmmv): Record the run-time configuration. +); + + +-- Environment variables of a context. +CREATE TABLE env_vars ( + context_id INTEGER REFERENCES contexts, + var_name TEXT NOT NULL, + var_value TEXT NOT NULL, + + PRIMARY KEY (context_id, var_name) +); + + +-- ------------------------------------------------------------------------- +-- Actions. +-- ------------------------------------------------------------------------- + + +-- Representation of user-initiated actions. +-- +-- An action is an operation initiated by the user. At the moment, the +-- only operation Kyua supports is the "test" operation (in the future we +-- should be able to store, e.g. build logs). To keep things simple the +-- database schema is restricted to represent one single action. +CREATE TABLE actions ( + action_id INTEGER PRIMARY KEY AUTOINCREMENT, + context_id INTEGER REFERENCES contexts +); + + +-- ------------------------------------------------------------------------- +-- Test suites. +-- +-- The tables in this section represent all the components that form a test +-- suite. This includes data about the test suite itself (test programs +-- and test cases), and also the data about particular runs (test results). +-- +-- As you will notice, every object belongs to a particular action, has a +-- unique identifier and there is no attempt to deduplicate data. This +-- comes from the fact that a test suite is not "stable" over time: i.e. on +-- each execution of the test suite, test programs and test cases may have +-- come and gone. This has the interesting result of making the +-- distinction of a test case and a test result a pure syntactic +-- difference, because there is always a 1:1 relation. +-- +-- The code that performs the processing of the actions is the component in +-- charge of finding correlations between test programs and test cases +-- across different actions. +-- ------------------------------------------------------------------------- + + +-- Representation of a test program. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_programs ( + test_program_id INTEGER PRIMARY KEY AUTOINCREMENT, + action_id INTEGER REFERENCES actions, + + -- The absolute path to the test program. This should not be necessary + -- because it is basically the concatenation of root and relative_path. + -- However, this allows us to very easily search for test programs + -- regardless of where they were executed from. (I.e. different + -- combinations of root + relative_path can map to the same absolute path). + absolute_path NOT NULL, + + -- The path to the root of the test suite (where the Kyuafile lives). + root TEXT NOT NULL, + + -- The path to the test program, relative to the root. + relative_path NOT NULL, + + -- Name of the test suite the test program belongs to. + test_suite_name TEXT NOT NULL, + + -- The name of the test program interface. + -- + -- Note that this indicates both the interface for the test program and + -- its test cases. See below for the corresponding detail tables. + interface TEXT NOT NULL +); + + +-- Representation of a test case. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_cases ( + test_case_id INTEGER PRIMARY KEY AUTOINCREMENT, + test_program_id INTEGER REFERENCES test_programs, + name TEXT NOT NULL +); + + +-- Representation of test case results. +-- +-- Note that there is a 1:1 relation between test cases and their results. +-- This is a result of storing the information of a test case on every +-- single action. +CREATE TABLE test_results ( + test_case_id INTEGER PRIMARY KEY REFERENCES test_cases, + result_type TEXT NOT NULL, + result_reason TEXT, + + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL +); + + +-- Collection of output files of the test case. +CREATE TABLE test_case_files ( + test_case_id INTEGER NOT NULL REFERENCES test_cases, + + -- The raw name of the file. + -- + -- The special names '__STDOUT__' and '__STDERR__' are reserved to hold + -- the stdout and stderr of the test case, respectively. If any of + -- these are empty, there will be no corresponding entry in this table + -- (hence why we do not allow NULLs in these fields). + file_name TEXT NOT NULL, + + -- Pointer to the file itself. + file_id INTEGER NOT NULL REFERENCES files, + + PRIMARY KEY (test_case_id, file_name) +); + + +-- ------------------------------------------------------------------------- +-- Detail tables for the 'atf' test interface. +-- ------------------------------------------------------------------------- + + +-- Properties specific to 'atf' test cases. +-- +-- This table contains the representation of singly-valued properties such +-- as 'timeout'. Properties that can have more than one (textual) value +-- are stored in the atf_test_cases_multivalues table. +-- +-- Note that all properties can be NULL because test cases are not required +-- to define them. +CREATE TABLE atf_test_cases ( + test_case_id INTEGER PRIMARY KEY REFERENCES test_cases, + + -- Free-form description of the text case. + description TEXT, + + -- Either 'true' or 'false', indicating whether the test case has a + -- cleanup routine or not. + has_cleanup TEXT, + + -- The timeout for the test case in microseconds. + timeout INTEGER, + + -- The amount of physical memory required by the test case. + required_memory INTEGER, + + -- Either 'root' or 'unprivileged', indicating the privileges required by + -- the test case. + required_user TEXT +); + + +-- Representation of test case properties that have more than one value. +-- +-- While we could store the flattened values of the properties as provided +-- by the test case itself, we choose to store the processed, split +-- representation. This allows us to perform queries about the test cases +-- directly on the database without doing text processing; for example, +-- "get all test cases that require /bin/ls". +CREATE TABLE atf_test_cases_multivalues ( + test_case_id INTEGER REFERENCES test_cases, + + -- The name of the property; for example, 'require.progs'. + property_name TEXT NOT NULL, + + -- One of the values of the property. + property_value TEXT NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Detail tables for the 'plain' test interface. +-- ------------------------------------------------------------------------- + + +-- Properties specific to 'plain' test programs. +CREATE TABLE plain_test_programs ( + test_program_id INTEGER PRIMARY KEY REFERENCES test_programs, + + -- The timeout for the test cases in this test program. While this + -- setting has a default value for test programs, we explicitly record + -- the information here. The "default value" used when the test + -- program was run might change over time, so we want to know what it + -- was exactly when this was run. + timeout INTEGER NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Verbatim files. +-- ------------------------------------------------------------------------- + + +-- Copies of files or logs generated during testing. +-- +-- TODO(jmmv): This will probably grow to unmanageable sizes. We should add a +-- hash to the file contents and use that as the primary key instead. +CREATE TABLE files ( + file_id INTEGER PRIMARY KEY, + + contents BLOB NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Initialization of values. +-- ------------------------------------------------------------------------- + + +-- Create a new metadata record. +-- +-- For every new database, we want to ensure that the metadata is valid if +-- the database creation (i.e. the whole transaction) succeeded. +-- +-- If you modify the value of the schema version in this statement, you +-- will also have to modify the version encoded in the backend module. +INSERT INTO metadata (timestamp, schema_version) + VALUES (strftime('%s', 'now'), 1); + + +COMMIT TRANSACTION; diff --git a/store/schema_v2.sql b/store/schema_v2.sql new file mode 100644 index 000000000000..48bd1727f91b --- /dev/null +++ b/store/schema_v2.sql @@ -0,0 +1,293 @@ +-- Copyright 2012 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/schema_v2.sql +-- Definition of the database schema. +-- +-- The whole contents of this file are wrapped in a transaction. We want +-- to ensure that the initial contents of the database (the table layout as +-- well as any predefined values) are written atomically to simplify error +-- handling in our code. + + +BEGIN TRANSACTION; + + +-- ------------------------------------------------------------------------- +-- Metadata. +-- ------------------------------------------------------------------------- + + +-- Database-wide properties. +-- +-- Rows in this table are immutable: modifying the metadata implies writing +-- a new record with a new schema_version greater than all existing +-- records, and never updating previous records. When extracting data from +-- this table, the only "valid" row is the one with the highest +-- scheam_version. All the other rows are meaningless and only exist for +-- historical purposes. +-- +-- In other words, this table keeps the history of the database metadata. +-- The only reason for doing this is for debugging purposes. It may come +-- in handy to know when a particular database-wide operation happened if +-- it turns out that the database got corrupted. +CREATE TABLE metadata ( + schema_version INTEGER PRIMARY KEY CHECK (schema_version >= 1), + timestamp TIMESTAMP NOT NULL CHECK (timestamp >= 0) +); + + +-- ------------------------------------------------------------------------- +-- Contexts. +-- ------------------------------------------------------------------------- + + +-- Execution contexts. +-- +-- A context represents the execution environment of a particular action. +-- Because every action is invoked by the user, the context may have +-- changed. We record such information for information and debugging +-- purposes. +CREATE TABLE contexts ( + context_id INTEGER PRIMARY KEY AUTOINCREMENT, + cwd TEXT NOT NULL + + -- TODO(jmmv): Record the run-time configuration. +); + + +-- Environment variables of a context. +CREATE TABLE env_vars ( + context_id INTEGER REFERENCES contexts, + var_name TEXT NOT NULL, + var_value TEXT NOT NULL, + + PRIMARY KEY (context_id, var_name) +); + + +-- ------------------------------------------------------------------------- +-- Actions. +-- ------------------------------------------------------------------------- + + +-- Representation of user-initiated actions. +-- +-- An action is an operation initiated by the user. At the moment, the +-- only operation Kyua supports is the "test" operation (in the future we +-- should be able to store, e.g. build logs). To keep things simple the +-- database schema is restricted to represent one single action. +CREATE TABLE actions ( + action_id INTEGER PRIMARY KEY AUTOINCREMENT, + context_id INTEGER REFERENCES contexts +); + + +-- ------------------------------------------------------------------------- +-- Test suites. +-- +-- The tables in this section represent all the components that form a test +-- suite. This includes data about the test suite itself (test programs +-- and test cases), and also the data about particular runs (test results). +-- +-- As you will notice, every object belongs to a particular action, has a +-- unique identifier and there is no attempt to deduplicate data. This +-- comes from the fact that a test suite is not "stable" over time: i.e. on +-- each execution of the test suite, test programs and test cases may have +-- come and gone. This has the interesting result of making the +-- distinction of a test case and a test result a pure syntactic +-- difference, because there is always a 1:1 relation. +-- +-- The code that performs the processing of the actions is the component in +-- charge of finding correlations between test programs and test cases +-- across different actions. +-- ------------------------------------------------------------------------- + + +-- Representation of the metadata objects. +-- +-- The way this table works is like this: every time we record a metadata +-- object, we calculate what its identifier should be as the last rowid of +-- the table. All properties of that metadata object thus receive the same +-- identifier. +CREATE TABLE metadatas ( + metadata_id INTEGER NOT NULL, + + -- The name of the property. + property_name TEXT NOT NULL, + + -- One of the values of the property. + property_value TEXT, + + PRIMARY KEY (metadata_id, property_name) +); + + +-- Optimize the loading of the metadata of any single entity. +-- +-- The metadata_id column of the metadatas table is not enough to act as a +-- primary key, yet we need to locate entries in the metadatas table solely by +-- their identifier. +-- +-- TODO(jmmv): I think this index is useless given that the primary key in the +-- metadatas table includes the metadata_id as the first component. Need to +-- verify this and drop the index or this comment appropriately. +CREATE INDEX index_metadatas_by_id + ON metadatas (metadata_id); + + +-- Representation of a test program. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_programs ( + test_program_id INTEGER PRIMARY KEY AUTOINCREMENT, + action_id INTEGER REFERENCES actions, + + -- The absolute path to the test program. This should not be necessary + -- because it is basically the concatenation of root and relative_path. + -- However, this allows us to very easily search for test programs + -- regardless of where they were executed from. (I.e. different + -- combinations of root + relative_path can map to the same absolute path). + absolute_path TEXT NOT NULL, + + -- The path to the root of the test suite (where the Kyuafile lives). + root TEXT NOT NULL, + + -- The path to the test program, relative to the root. + relative_path TEXT NOT NULL, + + -- Name of the test suite the test program belongs to. + test_suite_name TEXT NOT NULL, + + -- Reference to the various rows of metadatas. + metadata_id INTEGER, + + -- The name of the test program interface. + -- + -- Note that this indicates both the interface for the test program and + -- its test cases. See below for the corresponding detail tables. + interface TEXT NOT NULL +); + + +-- Optimize the lookup of test programs by the action they belong to. +CREATE INDEX index_test_programs_by_action_id + ON test_programs (action_id); + + +-- Representation of a test case. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_cases ( + test_case_id INTEGER PRIMARY KEY AUTOINCREMENT, + test_program_id INTEGER REFERENCES test_programs, + name TEXT NOT NULL, + + -- Reference to the various rows of metadatas. + metadata_id INTEGER +); + + +-- Optimize the loading of all test cases that are part of a test program. +CREATE INDEX index_test_cases_by_test_programs_id + ON test_cases (test_program_id); + + +-- Representation of test case results. +-- +-- Note that there is a 1:1 relation between test cases and their results. +-- This is a result of storing the information of a test case on every +-- single action. +CREATE TABLE test_results ( + test_case_id INTEGER PRIMARY KEY REFERENCES test_cases, + result_type TEXT NOT NULL, + result_reason TEXT, + + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL +); + + +-- Collection of output files of the test case. +CREATE TABLE test_case_files ( + test_case_id INTEGER NOT NULL REFERENCES test_cases, + + -- The raw name of the file. + -- + -- The special names '__STDOUT__' and '__STDERR__' are reserved to hold + -- the stdout and stderr of the test case, respectively. If any of + -- these are empty, there will be no corresponding entry in this table + -- (hence why we do not allow NULLs in these fields). + file_name TEXT NOT NULL, + + -- Pointer to the file itself. + file_id INTEGER NOT NULL REFERENCES files, + + PRIMARY KEY (test_case_id, file_name) +); + + +-- ------------------------------------------------------------------------- +-- Verbatim files. +-- ------------------------------------------------------------------------- + + +-- Copies of files or logs generated during testing. +-- +-- TODO(jmmv): This will probably grow to unmanageable sizes. We should add a +-- hash to the file contents and use that as the primary key instead. +CREATE TABLE files ( + file_id INTEGER PRIMARY KEY, + + contents BLOB NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Initialization of values. +-- ------------------------------------------------------------------------- + + +-- Create a new metadata record. +-- +-- For every new database, we want to ensure that the metadata is valid if +-- the database creation (i.e. the whole transaction) succeeded. +-- +-- If you modify the value of the schema version in this statement, you +-- will also have to modify the version encoded in the backend module. +INSERT INTO metadata (timestamp, schema_version) + VALUES (strftime('%s', 'now'), 2); + + +COMMIT TRANSACTION; diff --git a/store/schema_v3.sql b/store/schema_v3.sql new file mode 100644 index 000000000000..26e8359e1994 --- /dev/null +++ b/store/schema_v3.sql @@ -0,0 +1,255 @@ +-- Copyright 2012 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/schema_v3.sql +-- Definition of the database schema. +-- +-- The whole contents of this file are wrapped in a transaction. We want +-- to ensure that the initial contents of the database (the table layout as +-- well as any predefined values) are written atomically to simplify error +-- handling in our code. + + +BEGIN TRANSACTION; + + +-- ------------------------------------------------------------------------- +-- Metadata. +-- ------------------------------------------------------------------------- + + +-- Database-wide properties. +-- +-- Rows in this table are immutable: modifying the metadata implies writing +-- a new record with a new schema_version greater than all existing +-- records, and never updating previous records. When extracting data from +-- this table, the only "valid" row is the one with the highest +-- scheam_version. All the other rows are meaningless and only exist for +-- historical purposes. +-- +-- In other words, this table keeps the history of the database metadata. +-- The only reason for doing this is for debugging purposes. It may come +-- in handy to know when a particular database-wide operation happened if +-- it turns out that the database got corrupted. +CREATE TABLE metadata ( + schema_version INTEGER PRIMARY KEY CHECK (schema_version >= 1), + timestamp TIMESTAMP NOT NULL CHECK (timestamp >= 0) +); + + +-- ------------------------------------------------------------------------- +-- Contexts. +-- ------------------------------------------------------------------------- + + +-- Execution contexts. +-- +-- A context represents the execution environment of the test run. +-- We record such information for information and debugging purposes. +CREATE TABLE contexts ( + cwd TEXT NOT NULL + + -- TODO(jmmv): Record the run-time configuration. +); + + +-- Environment variables of a context. +CREATE TABLE env_vars ( + var_name TEXT PRIMARY KEY, + var_value TEXT NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Test suites. +-- +-- The tables in this section represent all the components that form a test +-- suite. This includes data about the test suite itself (test programs +-- and test cases), and also the data about particular runs (test results). +-- +-- As you will notice, every object has a unique identifier and there is no +-- attempt to deduplicate data. This has the interesting result of making +-- the distinction of a test case and a test result a pure syntactic +-- difference, because there is always a 1:1 relation. +-- ------------------------------------------------------------------------- + + +-- Representation of the metadata objects. +-- +-- The way this table works is like this: every time we record a metadata +-- object, we calculate what its identifier should be as the last rowid of +-- the table. All properties of that metadata object thus receive the same +-- identifier. +CREATE TABLE metadatas ( + metadata_id INTEGER NOT NULL, + + -- The name of the property. + property_name TEXT NOT NULL, + + -- One of the values of the property. + property_value TEXT, + + PRIMARY KEY (metadata_id, property_name) +); + + +-- Optimize the loading of the metadata of any single entity. +-- +-- The metadata_id column of the metadatas table is not enough to act as a +-- primary key, yet we need to locate entries in the metadatas table solely by +-- their identifier. +-- +-- TODO(jmmv): I think this index is useless given that the primary key in the +-- metadatas table includes the metadata_id as the first component. Need to +-- verify this and drop the index or this comment appropriately. +CREATE INDEX index_metadatas_by_id + ON metadatas (metadata_id); + + +-- Representation of a test program. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_programs ( + test_program_id INTEGER PRIMARY KEY AUTOINCREMENT, + + -- The absolute path to the test program. This should not be necessary + -- because it is basically the concatenation of root and relative_path. + -- However, this allows us to very easily search for test programs + -- regardless of where they were executed from. (I.e. different + -- combinations of root + relative_path can map to the same absolute path). + absolute_path TEXT NOT NULL, + + -- The path to the root of the test suite (where the Kyuafile lives). + root TEXT NOT NULL, + + -- The path to the test program, relative to the root. + relative_path TEXT NOT NULL, + + -- Name of the test suite the test program belongs to. + test_suite_name TEXT NOT NULL, + + -- Reference to the various rows of metadatas. + metadata_id INTEGER, + + -- The name of the test program interface. + -- + -- Note that this indicates both the interface for the test program and + -- its test cases. See below for the corresponding detail tables. + interface TEXT NOT NULL +); + + +-- Representation of a test case. +-- +-- At the moment, there are no substantial differences between the +-- different interfaces, so we can simplify the design by with having a +-- single table representing all test caes. We may need to revisit this in +-- the future. +CREATE TABLE test_cases ( + test_case_id INTEGER PRIMARY KEY AUTOINCREMENT, + test_program_id INTEGER REFERENCES test_programs, + name TEXT NOT NULL, + + -- Reference to the various rows of metadatas. + metadata_id INTEGER +); + + +-- Optimize the loading of all test cases that are part of a test program. +CREATE INDEX index_test_cases_by_test_programs_id + ON test_cases (test_program_id); + + +-- Representation of test case results. +-- +-- Note that there is a 1:1 relation between test cases and their results. +CREATE TABLE test_results ( + test_case_id INTEGER PRIMARY KEY REFERENCES test_cases, + result_type TEXT NOT NULL, + result_reason TEXT, + + start_time TIMESTAMP NOT NULL, + end_time TIMESTAMP NOT NULL +); + + +-- Collection of output files of the test case. +CREATE TABLE test_case_files ( + test_case_id INTEGER NOT NULL REFERENCES test_cases, + + -- The raw name of the file. + -- + -- The special names '__STDOUT__' and '__STDERR__' are reserved to hold + -- the stdout and stderr of the test case, respectively. If any of + -- these are empty, there will be no corresponding entry in this table + -- (hence why we do not allow NULLs in these fields). + file_name TEXT NOT NULL, + + -- Pointer to the file itself. + file_id INTEGER NOT NULL REFERENCES files, + + PRIMARY KEY (test_case_id, file_name) +); + + +-- ------------------------------------------------------------------------- +-- Verbatim files. +-- ------------------------------------------------------------------------- + + +-- Copies of files or logs generated during testing. +-- +-- TODO(jmmv): This will probably grow to unmanageable sizes. We should add a +-- hash to the file contents and use that as the primary key instead. +CREATE TABLE files ( + file_id INTEGER PRIMARY KEY, + + contents BLOB NOT NULL +); + + +-- ------------------------------------------------------------------------- +-- Initialization of values. +-- ------------------------------------------------------------------------- + + +-- Create a new metadata record. +-- +-- For every new database, we want to ensure that the metadata is valid if +-- the database creation (i.e. the whole transaction) succeeded. +-- +-- If you modify the value of the schema version in this statement, you +-- will also have to modify the version encoded in the backend module. +INSERT INTO metadata (timestamp, schema_version) + VALUES (strftime('%s', 'now'), 3); + + +COMMIT TRANSACTION; diff --git a/store/testdata_v1.sql b/store/testdata_v1.sql new file mode 100644 index 000000000000..75c4d439ac96 --- /dev/null +++ b/store/testdata_v1.sql @@ -0,0 +1,330 @@ +-- Copyright 2013 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/testdata_v1.sql +-- Populates a v1 database with some test data. + + +BEGIN TRANSACTION; + + +-- +-- Action 1: Empty context and no test programs nor test cases. +-- + + +-- context_id 1 +INSERT INTO contexts (context_id, cwd) VALUES (1, '/some/root'); + +-- action_id 1 +INSERT INTO actions (action_id, context_id) VALUES (1, 1); + + +-- +-- Action 2: Plain test programs only. +-- +-- This action contains 5 test programs, each with one test case, and each +-- reporting one of all possible result types. +-- + + +-- context_id 2 +INSERT INTO contexts (context_id, cwd) VALUES (2, '/test/suite/root'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (2, 'HOME', '/home/test'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (2, 'PATH', '/bin:/usr/bin'); + +-- action_id 2 +INSERT INTO actions (action_id, context_id) VALUES (2, 2); + +-- test_program_id 1 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (1, 2, '/test/suite/root/foo_test', '/test/suite/root', + 'foo_test', 'suite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (1, 300000000); + +-- test_case_id 1 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (1, 1, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (1, 'passed', NULL, 1357643611000000, 1357643621000500); + +-- test_program_id 2 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (2, 2, '/test/suite/root/subdir/another_test', '/test/suite/root', + 'subdir/another_test', 'subsuite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (2, 10000000); + +-- test_case_id 2 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (2, 2, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (2, 'failed', 'Exited with code 1', + 1357643622001200, 1357643622900021); + +-- file_id 1 +INSERT INTO files (file_id, contents) VALUES (1, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDOUT__', 1); + +-- file_id 2 +INSERT INTO files (file_id, contents) VALUES (2, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDERR__', 2); + +-- test_program_id 3 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (3, 2, '/test/suite/root/subdir/bar_test', '/test/suite/root', + 'subdir/bar_test', 'subsuite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (3, 300000000); + +-- test_case_id 3 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (3, 3, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (3, 'broken', 'Received signal 1', + 1357643623500000, 1357643630981932); + +-- test_program_id 4 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (4, 2, '/test/suite/root/top_test', '/test/suite/root', + 'top_test', 'suite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (4, 300000000); + +-- test_case_id 4 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (4, 4, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (4, 'expected_failure', 'Known bug', + 1357643631000000, 1357643631020000); + +-- test_program_id 5 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (5, 2, '/test/suite/root/last_test', '/test/suite/root', + 'last_test', 'suite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (5, 300000000); + +-- test_case_id 5 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (5, 5, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (5, 'skipped', 'Does not apply', 1357643632000000, 1357643638000000); + + +-- +-- Action 3: ATF test programs only. +-- + + +-- context_id 3 +INSERT INTO contexts (context_id, cwd) VALUES (3, '/usr/tests'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (3, 'PATH', '/bin:/usr/bin'); + +-- action_id 3 +INSERT INTO actions (action_id, context_id) VALUES (3, 3); + +-- test_program_id 6 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (6, 3, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 'atf'); + +-- test_case_id 6, passed, no optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (6, 6, 'this_passes'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (6, 'passed', NULL, 1357648712000000, 1357648718000000); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (6, NULL, 'false', 300000000, 0, NULL); + +-- test_case_id 7, failed, optional non-multivalue metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (7, 6, 'this_fails'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (7, 'failed', 'Some reason', 1357648719000000, 1357648720897182); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (7, 'Test description', 'true', 300000000, 128, 'root'); + +-- test_case_id 8, skipped, all optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (8, 6, 'this_skips'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (8, 'skipped', 'Another reason', 1357648729182013, 1357648730000000); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (8, 'Test explanation', 'true', 600000000, 512, 'unprivileged'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.arch', 'x86_64'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.arch', 'powerpc'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.machine', 'amd64'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.machine', 'macppc'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.config', 'unprivileged_user'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.config', 'X-foo'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.files', '/the/data/file'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.progs', 'cp'); +INSERT INTO atf_test_cases_multivalues (test_case_id, property_name, + property_value) + VALUES (8, 'require.progs', '/bin/ls'); + +-- file_id 3 +INSERT INTO files (file_id, contents) + VALUES (3, x'416e6f74686572207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (8, '__STDOUT__', 3); + +-- test_program_id 7 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (7, 3, '/usr/tests/simple_test', '/usr/tests', + 'simple_test', 'subsuite-name', 'atf'); + +-- test_case_id 9 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (9, 7, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (9, 'failed', 'Exited with code 1', + 1357648740120000, 1357648750081700); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (9, 'More text', 'true', 300000000, 128, 'unprivileged'); + +-- file_id 4 +INSERT INTO files (file_id, contents) + VALUES (4, x'416e6f7468657220737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (9, '__STDERR__', 4); + + +-- +-- Action 4: Mixture of test programs. +-- + + +-- context_id 4 +INSERT INTO contexts (context_id, cwd) VALUES (4, '/usr/tests'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'LANG', 'C'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'PATH', '/bin:/usr/bin'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'TERM', 'xterm'); + +-- action_id 4 +INSERT INTO actions (action_id, context_id) VALUES (4, 4); + +-- test_program_id 8 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (8, 4, '/usr/tests/subdir/another_test', '/usr/tests', + 'subdir/another_test', 'subsuite-name', 'plain'); +INSERT INTO plain_test_programs (test_program_id, timeout) + VALUES (8, 10000000); + +-- test_case_id 10 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (10, 8, 'main'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (10, 'failed', 'Exit failure', 1357644395000000, 1357644396000000); + +-- file_id 5 +INSERT INTO files (file_id, contents) VALUES (5, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDOUT__', 5); + +-- file_id 6 +INSERT INTO files (file_id, contents) VALUES (6, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDERR__', 6); + +-- test_program_id 9 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, interface) + VALUES (9, 4, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 'atf'); + +-- test_case_id 11 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (11, 9, 'this_passes'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (11, 'passed', NULL, 1357644396500000, 1357644397000000); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (11, NULL, 'false', 300000000, 0, NULL); + +-- test_case_id 12 +INSERT INTO test_cases (test_case_id, test_program_id, name) + VALUES (12, 9, 'this_fails'); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (12, 'failed', 'Some reason', 1357644397100000, 1357644399005000); +INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout, + required_memory, required_user) + VALUES (12, 'Test description', 'false', 300000000, 0, 'root'); + + +COMMIT TRANSACTION; diff --git a/store/testdata_v2.sql b/store/testdata_v2.sql new file mode 100644 index 000000000000..838da4c25956 --- /dev/null +++ b/store/testdata_v2.sql @@ -0,0 +1,462 @@ +-- Copyright 2013 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/testdata_v2.sql +-- Populates a v2 database with some test data. + + +BEGIN TRANSACTION; + + +-- +-- Action 1: Empty context and no test programs nor test cases. +-- + + +-- context_id 1 +INSERT INTO contexts (context_id, cwd) VALUES (1, '/some/root'); + +-- action_id 1 +INSERT INTO actions (action_id, context_id) VALUES (1, 1); + + +-- +-- Action 2: Plain test programs only. +-- +-- This action contains 5 test programs, each with one test case, and each +-- reporting one of all possible result types. +-- + + +-- context_id 2 +INSERT INTO contexts (context_id, cwd) VALUES (2, '/test/suite/root'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (2, 'HOME', '/home/test'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (2, 'PATH', '/bin:/usr/bin'); + +-- action_id 2 +INSERT INTO actions (action_id, context_id) VALUES (2, 2); + +-- metadata_id 1 +INSERT INTO metadatas VALUES (1, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (1, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (1, 'description', ''); +INSERT INTO metadatas VALUES (1, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (1, 'required_configs', ''); +INSERT INTO metadatas VALUES (1, 'required_files', ''); +INSERT INTO metadatas VALUES (1, 'required_memory', '0'); +INSERT INTO metadatas VALUES (1, 'required_programs', ''); +INSERT INTO metadatas VALUES (1, 'required_user', ''); +INSERT INTO metadatas VALUES (1, 'timeout', '300'); + +-- test_program_id 1 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (1, 2, '/test/suite/root/foo_test', '/test/suite/root', + 'foo_test', 'suite-name', 1, 'plain'); + +-- test_case_id 1 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (1, 1, 'main', 1); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (1, 'passed', NULL, 1357643611000000, 1357643621000500); + +-- metadata_id 2 +INSERT INTO metadatas VALUES (2, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (2, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (2, 'description', ''); +INSERT INTO metadatas VALUES (2, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (2, 'required_configs', ''); +INSERT INTO metadatas VALUES (2, 'required_files', ''); +INSERT INTO metadatas VALUES (2, 'required_memory', '0'); +INSERT INTO metadatas VALUES (2, 'required_programs', ''); +INSERT INTO metadatas VALUES (2, 'required_user', ''); +INSERT INTO metadatas VALUES (2, 'timeout', '10'); + +-- test_program_id 2 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (2, 2, '/test/suite/root/subdir/another_test', '/test/suite/root', + 'subdir/another_test', 'subsuite-name', 2, 'plain'); + +-- test_case_id 2 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (2, 2, 'main', 2); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (2, 'failed', 'Exited with code 1', + 1357643622001200, 1357643622900021); + +-- file_id 1 +INSERT INTO files (file_id, contents) VALUES (1, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDOUT__', 1); + +-- file_id 2 +INSERT INTO files (file_id, contents) VALUES (2, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDERR__', 2); + +-- metadata_id 3 +INSERT INTO metadatas VALUES (3, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (3, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (3, 'description', ''); +INSERT INTO metadatas VALUES (3, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (3, 'required_configs', ''); +INSERT INTO metadatas VALUES (3, 'required_files', ''); +INSERT INTO metadatas VALUES (3, 'required_memory', '0'); +INSERT INTO metadatas VALUES (3, 'required_programs', ''); +INSERT INTO metadatas VALUES (3, 'required_user', ''); +INSERT INTO metadatas VALUES (3, 'timeout', '300'); + +-- test_program_id 3 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (3, 2, '/test/suite/root/subdir/bar_test', '/test/suite/root', + 'subdir/bar_test', 'subsuite-name', 3, 'plain'); + +-- test_case_id 3 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (3, 3, 'main', 3); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (3, 'broken', 'Received signal 1', + 1357643623500000, 1357643630981932); + +-- metadata_id 4 +INSERT INTO metadatas VALUES (4, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (4, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (4, 'description', ''); +INSERT INTO metadatas VALUES (4, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (4, 'required_configs', ''); +INSERT INTO metadatas VALUES (4, 'required_files', ''); +INSERT INTO metadatas VALUES (4, 'required_memory', '0'); +INSERT INTO metadatas VALUES (4, 'required_programs', ''); +INSERT INTO metadatas VALUES (4, 'required_user', ''); +INSERT INTO metadatas VALUES (4, 'timeout', '300'); + +-- test_program_id 4 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (4, 2, '/test/suite/root/top_test', '/test/suite/root', + 'top_test', 'suite-name', 4, 'plain'); + +-- test_case_id 4 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (4, 4, 'main', 4); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (4, 'expected_failure', 'Known bug', + 1357643631000000, 1357643631020000); + +-- metadata_id 5 +INSERT INTO metadatas VALUES (5, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (5, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (5, 'description', ''); +INSERT INTO metadatas VALUES (5, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (5, 'required_configs', ''); +INSERT INTO metadatas VALUES (5, 'required_files', ''); +INSERT INTO metadatas VALUES (5, 'required_memory', '0'); +INSERT INTO metadatas VALUES (5, 'required_programs', ''); +INSERT INTO metadatas VALUES (5, 'required_user', ''); +INSERT INTO metadatas VALUES (5, 'timeout', '300'); + +-- test_program_id 5 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (5, 2, '/test/suite/root/last_test', '/test/suite/root', + 'last_test', 'suite-name', 5, 'plain'); + +-- test_case_id 5 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (5, 5, 'main', 5); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (5, 'skipped', 'Does not apply', 1357643632000000, 1357643638000000); + + +-- +-- Action 3: ATF test programs only. +-- + + +-- context_id 3 +INSERT INTO contexts (context_id, cwd) VALUES (3, '/usr/tests'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (3, 'PATH', '/bin:/usr/bin'); + +-- action_id 3 +INSERT INTO actions (action_id, context_id) VALUES (3, 3); + +-- metadata_id 6 +INSERT INTO metadatas VALUES (6, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (6, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (6, 'description', ''); +INSERT INTO metadatas VALUES (6, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (6, 'required_configs', ''); +INSERT INTO metadatas VALUES (6, 'required_files', ''); +INSERT INTO metadatas VALUES (6, 'required_memory', '0'); +INSERT INTO metadatas VALUES (6, 'required_programs', ''); +INSERT INTO metadatas VALUES (6, 'required_user', ''); +INSERT INTO metadatas VALUES (6, 'timeout', '300'); + +-- test_program_id 6 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (6, 3, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 6, 'atf'); + +-- metadata_id 7 +INSERT INTO metadatas VALUES (7, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (7, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (7, 'description', ''); +INSERT INTO metadatas VALUES (7, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (7, 'required_configs', ''); +INSERT INTO metadatas VALUES (7, 'required_files', ''); +INSERT INTO metadatas VALUES (7, 'required_memory', '0'); +INSERT INTO metadatas VALUES (7, 'required_programs', ''); +INSERT INTO metadatas VALUES (7, 'required_user', ''); +INSERT INTO metadatas VALUES (7, 'timeout', '300'); + +-- test_case_id 6, passed, no optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (6, 6, 'this_passes', 7); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (6, 'passed', NULL, 1357648712000000, 1357648718000000); + +-- metadata_id 8 +INSERT INTO metadatas VALUES (8, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (8, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (8, 'description', 'Test description'); +INSERT INTO metadatas VALUES (8, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (8, 'required_configs', ''); +INSERT INTO metadatas VALUES (8, 'required_files', ''); +INSERT INTO metadatas VALUES (8, 'required_memory', '128'); +INSERT INTO metadatas VALUES (8, 'required_programs', ''); +INSERT INTO metadatas VALUES (8, 'required_user', 'root'); +INSERT INTO metadatas VALUES (8, 'timeout', '300'); + +-- test_case_id 7, failed, optional non-multivalue metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (7, 6, 'this_fails', 8); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (7, 'failed', 'Some reason', 1357648719000000, 1357648720897182); + +-- metadata_id 9 +INSERT INTO metadatas VALUES (9, 'allowed_architectures', 'powerpc x86_64'); +INSERT INTO metadatas VALUES (9, 'allowed_platforms', 'amd64 macppc'); +INSERT INTO metadatas VALUES (9, 'description', 'Test explanation'); +INSERT INTO metadatas VALUES (9, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (9, 'required_configs', 'unprivileged_user X-foo'); +INSERT INTO metadatas VALUES (9, 'required_files', '/the/data/file'); +INSERT INTO metadatas VALUES (9, 'required_memory', '512'); +INSERT INTO metadatas VALUES (9, 'required_programs', 'cp /bin/ls'); +INSERT INTO metadatas VALUES (9, 'required_user', 'unprivileged'); +INSERT INTO metadatas VALUES (9, 'timeout', '600'); + +-- test_case_id 8, skipped, all optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (8, 6, 'this_skips', 9); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (8, 'skipped', 'Another reason', 1357648729182013, 1357648730000000); + +-- file_id 3 +INSERT INTO files (file_id, contents) + VALUES (3, x'416e6f74686572207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (8, '__STDOUT__', 3); + +-- metadata_id 10 +INSERT INTO metadatas VALUES (10, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (10, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (10, 'description', ''); +INSERT INTO metadatas VALUES (10, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (10, 'required_configs', ''); +INSERT INTO metadatas VALUES (10, 'required_files', ''); +INSERT INTO metadatas VALUES (10, 'required_memory', '0'); +INSERT INTO metadatas VALUES (10, 'required_programs', ''); +INSERT INTO metadatas VALUES (10, 'required_user', ''); +INSERT INTO metadatas VALUES (10, 'timeout', '300'); + +-- test_program_id 7 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (7, 3, '/usr/tests/simple_test', '/usr/tests', + 'simple_test', 'subsuite-name', 10, 'atf'); + +-- metadata_id 11 +INSERT INTO metadatas VALUES (11, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (11, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (11, 'description', 'More text'); +INSERT INTO metadatas VALUES (11, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (11, 'required_configs', ''); +INSERT INTO metadatas VALUES (11, 'required_files', ''); +INSERT INTO metadatas VALUES (11, 'required_memory', '128'); +INSERT INTO metadatas VALUES (11, 'required_programs', ''); +INSERT INTO metadatas VALUES (11, 'required_user', 'unprivileged'); +INSERT INTO metadatas VALUES (11, 'timeout', '300'); + +-- test_case_id 9 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (9, 7, 'main', 11); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (9, 'failed', 'Exited with code 1', + 1357648740120000, 1357648750081700); + +-- file_id 4 +INSERT INTO files (file_id, contents) + VALUES (4, x'416e6f7468657220737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (9, '__STDERR__', 4); + + +-- +-- Action 4: Mixture of test programs. +-- + + +-- context_id 4 +INSERT INTO contexts (context_id, cwd) VALUES (4, '/usr/tests'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'LANG', 'C'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'PATH', '/bin:/usr/bin'); +INSERT INTO env_vars (context_id, var_name, var_value) + VALUES (4, 'TERM', 'xterm'); + +-- action_id 4 +INSERT INTO actions (action_id, context_id) VALUES (4, 4); + +-- metadata_id 12 +INSERT INTO metadatas VALUES (12, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (12, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (12, 'description', ''); +INSERT INTO metadatas VALUES (12, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (12, 'required_configs', ''); +INSERT INTO metadatas VALUES (12, 'required_files', ''); +INSERT INTO metadatas VALUES (12, 'required_memory', '0'); +INSERT INTO metadatas VALUES (12, 'required_programs', ''); +INSERT INTO metadatas VALUES (12, 'required_user', ''); +INSERT INTO metadatas VALUES (12, 'timeout', '10'); + +-- test_program_id 8 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (8, 4, '/usr/tests/subdir/another_test', '/usr/tests', + 'subdir/another_test', 'subsuite-name', 12, 'plain'); + +-- test_case_id 10 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (10, 8, 'main', 12); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (10, 'failed', 'Exit failure', 1357644395000000, 1357644396000000); + +-- file_id 5 +INSERT INTO files (file_id, contents) VALUES (5, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDOUT__', 5); + +-- file_id 6 +INSERT INTO files (file_id, contents) VALUES (6, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDERR__', 6); + +-- metadata_id 13 +INSERT INTO metadatas VALUES (13, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (13, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (13, 'description', ''); +INSERT INTO metadatas VALUES (13, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (13, 'required_configs', ''); +INSERT INTO metadatas VALUES (13, 'required_files', ''); +INSERT INTO metadatas VALUES (13, 'required_memory', '0'); +INSERT INTO metadatas VALUES (13, 'required_programs', ''); +INSERT INTO metadatas VALUES (13, 'required_user', ''); +INSERT INTO metadatas VALUES (13, 'timeout', '300'); + +-- test_program_id 9 +INSERT INTO test_programs (test_program_id, action_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (9, 4, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 14, 'atf'); + +-- metadata_id 15 +INSERT INTO metadatas VALUES (15, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (15, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (15, 'description', ''); +INSERT INTO metadatas VALUES (15, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (15, 'required_configs', ''); +INSERT INTO metadatas VALUES (15, 'required_files', ''); +INSERT INTO metadatas VALUES (15, 'required_memory', '0'); +INSERT INTO metadatas VALUES (15, 'required_programs', ''); +INSERT INTO metadatas VALUES (15, 'required_user', ''); +INSERT INTO metadatas VALUES (15, 'timeout', '300'); + +-- test_case_id 11 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (11, 9, 'this_passes', 15); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (11, 'passed', NULL, 1357644396500000, 1357644397000000); + +-- metadata_id 16 +INSERT INTO metadatas VALUES (16, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (16, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (16, 'description', 'Test description'); +INSERT INTO metadatas VALUES (16, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (16, 'required_configs', ''); +INSERT INTO metadatas VALUES (16, 'required_files', ''); +INSERT INTO metadatas VALUES (16, 'required_memory', '0'); +INSERT INTO metadatas VALUES (16, 'required_programs', ''); +INSERT INTO metadatas VALUES (16, 'required_user', 'root'); +INSERT INTO metadatas VALUES (16, 'timeout', '300'); + +-- test_case_id 12 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (12, 9, 'this_fails', 16); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (12, 'failed', 'Some reason', 1357644397100000, 1357644399005000); + + +COMMIT TRANSACTION; diff --git a/store/testdata_v3_1.sql b/store/testdata_v3_1.sql new file mode 100644 index 000000000000..9715db490ba0 --- /dev/null +++ b/store/testdata_v3_1.sql @@ -0,0 +1,42 @@ +-- Copyright 2014 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/testdata_v3.sql +-- Populates a v3 database with some test data. +-- +-- Empty context and no test programs nor test cases. + + +BEGIN TRANSACTION; + + +-- context +INSERT INTO contexts (cwd) VALUES ('/some/root'); + + +COMMIT TRANSACTION; diff --git a/store/testdata_v3_2.sql b/store/testdata_v3_2.sql new file mode 100644 index 000000000000..0ef42a328c7c --- /dev/null +++ b/store/testdata_v3_2.sql @@ -0,0 +1,190 @@ +-- Copyright 2014 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/testdata_v3.sql +-- Populates a v3 database with some test data. +-- +-- This contains 5 test programs, each with one test case, and each +-- reporting one of all possible result types. + + +BEGIN TRANSACTION; + + +-- context +INSERT INTO contexts (cwd) VALUES ('/test/suite/root'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('HOME', '/home/test'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('PATH', '/bin:/usr/bin'); + +-- metadata_id 1 +INSERT INTO metadatas VALUES (1, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (1, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (1, 'description', ''); +INSERT INTO metadatas VALUES (1, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (1, 'required_configs', ''); +INSERT INTO metadatas VALUES (1, 'required_files', ''); +INSERT INTO metadatas VALUES (1, 'required_memory', '0'); +INSERT INTO metadatas VALUES (1, 'required_programs', ''); +INSERT INTO metadatas VALUES (1, 'required_user', ''); +INSERT INTO metadatas VALUES (1, 'timeout', '300'); + +-- test_program_id 1 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (1, '/test/suite/root/foo_test', '/test/suite/root', + 'foo_test', 'suite-name', 1, 'plain'); + +-- test_case_id 1 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (1, 1, 'main', 1); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (1, 'passed', NULL, 1357643611000000, 1357643621000500); + +-- metadata_id 2 +INSERT INTO metadatas VALUES (2, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (2, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (2, 'description', ''); +INSERT INTO metadatas VALUES (2, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (2, 'required_configs', ''); +INSERT INTO metadatas VALUES (2, 'required_files', ''); +INSERT INTO metadatas VALUES (2, 'required_memory', '0'); +INSERT INTO metadatas VALUES (2, 'required_programs', ''); +INSERT INTO metadatas VALUES (2, 'required_user', ''); +INSERT INTO metadatas VALUES (2, 'timeout', '10'); + +-- test_program_id 2 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (2, '/test/suite/root/subdir/another_test', '/test/suite/root', + 'subdir/another_test', 'subsuite-name', 2, 'plain'); + +-- test_case_id 2 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (2, 2, 'main', 2); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (2, 'failed', 'Exited with code 1', + 1357643622001200, 1357643622900021); + +-- file_id 1 +INSERT INTO files (file_id, contents) VALUES (1, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDOUT__', 1); + +-- file_id 2 +INSERT INTO files (file_id, contents) VALUES (2, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (2, '__STDERR__', 2); + +-- metadata_id 3 +INSERT INTO metadatas VALUES (3, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (3, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (3, 'description', ''); +INSERT INTO metadatas VALUES (3, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (3, 'required_configs', ''); +INSERT INTO metadatas VALUES (3, 'required_files', ''); +INSERT INTO metadatas VALUES (3, 'required_memory', '0'); +INSERT INTO metadatas VALUES (3, 'required_programs', ''); +INSERT INTO metadatas VALUES (3, 'required_user', ''); +INSERT INTO metadatas VALUES (3, 'timeout', '300'); + +-- test_program_id 3 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (3, '/test/suite/root/subdir/bar_test', '/test/suite/root', + 'subdir/bar_test', 'subsuite-name', 3, 'plain'); + +-- test_case_id 3 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (3, 3, 'main', 3); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (3, 'broken', 'Received signal 1', + 1357643623500000, 1357643630981932); + +-- metadata_id 4 +INSERT INTO metadatas VALUES (4, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (4, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (4, 'description', ''); +INSERT INTO metadatas VALUES (4, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (4, 'required_configs', ''); +INSERT INTO metadatas VALUES (4, 'required_files', ''); +INSERT INTO metadatas VALUES (4, 'required_memory', '0'); +INSERT INTO metadatas VALUES (4, 'required_programs', ''); +INSERT INTO metadatas VALUES (4, 'required_user', ''); +INSERT INTO metadatas VALUES (4, 'timeout', '300'); + +-- test_program_id 4 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (4, '/test/suite/root/top_test', '/test/suite/root', + 'top_test', 'suite-name', 4, 'plain'); + +-- test_case_id 4 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (4, 4, 'main', 4); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (4, 'expected_failure', 'Known bug', + 1357643631000000, 1357643631020000); + +-- metadata_id 5 +INSERT INTO metadatas VALUES (5, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (5, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (5, 'description', ''); +INSERT INTO metadatas VALUES (5, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (5, 'required_configs', ''); +INSERT INTO metadatas VALUES (5, 'required_files', ''); +INSERT INTO metadatas VALUES (5, 'required_memory', '0'); +INSERT INTO metadatas VALUES (5, 'required_programs', ''); +INSERT INTO metadatas VALUES (5, 'required_user', ''); +INSERT INTO metadatas VALUES (5, 'timeout', '300'); + +-- test_program_id 5 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (5, '/test/suite/root/last_test', '/test/suite/root', + 'last_test', 'suite-name', 5, 'plain'); + +-- test_case_id 5 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (5, 5, 'main', 5); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (5, 'skipped', 'Does not apply', 1357643632000000, 1357643638000000); + + +COMMIT TRANSACTION; diff --git a/store/testdata_v3_3.sql b/store/testdata_v3_3.sql new file mode 100644 index 000000000000..80d5a6b9a6e2 --- /dev/null +++ b/store/testdata_v3_3.sql @@ -0,0 +1,171 @@ +-- Copyright 2014 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/testdata_v3.sql +-- Populates a v3 database with some test data. +-- +-- ATF test programs only. + + +BEGIN TRANSACTION; + + +-- context +INSERT INTO contexts (cwd) VALUES ('/usr/tests'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('PATH', '/bin:/usr/bin'); + +-- metadata_id 6 +INSERT INTO metadatas VALUES (6, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (6, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (6, 'description', ''); +INSERT INTO metadatas VALUES (6, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (6, 'required_configs', ''); +INSERT INTO metadatas VALUES (6, 'required_files', ''); +INSERT INTO metadatas VALUES (6, 'required_memory', '0'); +INSERT INTO metadatas VALUES (6, 'required_programs', ''); +INSERT INTO metadatas VALUES (6, 'required_user', ''); +INSERT INTO metadatas VALUES (6, 'timeout', '300'); + +-- test_program_id 6 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (6, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 6, 'atf'); + +-- metadata_id 7 +INSERT INTO metadatas VALUES (7, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (7, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (7, 'description', ''); +INSERT INTO metadatas VALUES (7, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (7, 'required_configs', ''); +INSERT INTO metadatas VALUES (7, 'required_files', ''); +INSERT INTO metadatas VALUES (7, 'required_memory', '0'); +INSERT INTO metadatas VALUES (7, 'required_programs', ''); +INSERT INTO metadatas VALUES (7, 'required_user', ''); +INSERT INTO metadatas VALUES (7, 'timeout', '300'); + +-- test_case_id 6, passed, no optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (6, 6, 'this_passes', 7); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (6, 'passed', NULL, 1357648712000000, 1357648718000000); + +-- metadata_id 8 +INSERT INTO metadatas VALUES (8, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (8, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (8, 'description', 'Test description'); +INSERT INTO metadatas VALUES (8, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (8, 'required_configs', ''); +INSERT INTO metadatas VALUES (8, 'required_files', ''); +INSERT INTO metadatas VALUES (8, 'required_memory', '128'); +INSERT INTO metadatas VALUES (8, 'required_programs', ''); +INSERT INTO metadatas VALUES (8, 'required_user', 'root'); +INSERT INTO metadatas VALUES (8, 'timeout', '300'); + +-- test_case_id 7, failed, optional non-multivalue metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (7, 6, 'this_fails', 8); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (7, 'failed', 'Some reason', 1357648719000000, 1357648720897182); + +-- metadata_id 9 +INSERT INTO metadatas VALUES (9, 'allowed_architectures', 'powerpc x86_64'); +INSERT INTO metadatas VALUES (9, 'allowed_platforms', 'amd64 macppc'); +INSERT INTO metadatas VALUES (9, 'description', 'Test explanation'); +INSERT INTO metadatas VALUES (9, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (9, 'required_configs', 'unprivileged_user X-foo'); +INSERT INTO metadatas VALUES (9, 'required_files', '/the/data/file'); +INSERT INTO metadatas VALUES (9, 'required_memory', '512'); +INSERT INTO metadatas VALUES (9, 'required_programs', 'cp /bin/ls'); +INSERT INTO metadatas VALUES (9, 'required_user', 'unprivileged'); +INSERT INTO metadatas VALUES (9, 'timeout', '600'); + +-- test_case_id 8, skipped, all optional metadata. +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (8, 6, 'this_skips', 9); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (8, 'skipped', 'Another reason', 1357648729182013, 1357648730000000); + +-- file_id 3 +INSERT INTO files (file_id, contents) + VALUES (3, x'416e6f74686572207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (8, '__STDOUT__', 3); + +-- metadata_id 10 +INSERT INTO metadatas VALUES (10, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (10, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (10, 'description', ''); +INSERT INTO metadatas VALUES (10, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (10, 'required_configs', ''); +INSERT INTO metadatas VALUES (10, 'required_files', ''); +INSERT INTO metadatas VALUES (10, 'required_memory', '0'); +INSERT INTO metadatas VALUES (10, 'required_programs', ''); +INSERT INTO metadatas VALUES (10, 'required_user', ''); +INSERT INTO metadatas VALUES (10, 'timeout', '300'); + +-- test_program_id 7 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (7, '/usr/tests/simple_test', '/usr/tests', + 'simple_test', 'subsuite-name', 10, 'atf'); + +-- metadata_id 11 +INSERT INTO metadatas VALUES (11, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (11, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (11, 'description', 'More text'); +INSERT INTO metadatas VALUES (11, 'has_cleanup', 'true'); +INSERT INTO metadatas VALUES (11, 'required_configs', ''); +INSERT INTO metadatas VALUES (11, 'required_files', ''); +INSERT INTO metadatas VALUES (11, 'required_memory', '128'); +INSERT INTO metadatas VALUES (11, 'required_programs', ''); +INSERT INTO metadatas VALUES (11, 'required_user', 'unprivileged'); +INSERT INTO metadatas VALUES (11, 'timeout', '300'); + +-- test_case_id 9 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (9, 7, 'main', 11); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (9, 'failed', 'Exited with code 1', + 1357648740120000, 1357648750081700); + +-- file_id 4 +INSERT INTO files (file_id, contents) + VALUES (4, x'416e6f7468657220737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (9, '__STDERR__', 4); + + +COMMIT TRANSACTION; diff --git a/store/testdata_v3_4.sql b/store/testdata_v3_4.sql new file mode 100644 index 000000000000..1007bc7adac4 --- /dev/null +++ b/store/testdata_v3_4.sql @@ -0,0 +1,141 @@ +-- Copyright 2014 The Kyua Authors. +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- * Neither the name of Google Inc. nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- \file store/testdata_v3.sql +-- Populates a v3 database with some test data. +-- +-- Mixture of test programs. + + +BEGIN TRANSACTION; + + +-- context +INSERT INTO contexts (cwd) VALUES ('/usr/tests'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('LANG', 'C'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('PATH', '/bin:/usr/bin'); +INSERT INTO env_vars (var_name, var_value) + VALUES ('TERM', 'xterm'); + +-- metadata_id 12 +INSERT INTO metadatas VALUES (12, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (12, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (12, 'description', ''); +INSERT INTO metadatas VALUES (12, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (12, 'required_configs', ''); +INSERT INTO metadatas VALUES (12, 'required_files', ''); +INSERT INTO metadatas VALUES (12, 'required_memory', '0'); +INSERT INTO metadatas VALUES (12, 'required_programs', ''); +INSERT INTO metadatas VALUES (12, 'required_user', ''); +INSERT INTO metadatas VALUES (12, 'timeout', '10'); + +-- test_program_id 8 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (8, '/usr/tests/subdir/another_test', '/usr/tests', + 'subdir/another_test', 'subsuite-name', 12, 'plain'); + +-- test_case_id 10 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (10, 8, 'main', 12); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (10, 'failed', 'Exit failure', 1357644395000000, 1357644396000000); + +-- file_id 5 +INSERT INTO files (file_id, contents) VALUES (5, x'54657374207374646f7574'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDOUT__', 5); + +-- file_id 6 +INSERT INTO files (file_id, contents) VALUES (6, x'5465737420737464657272'); +INSERT INTO test_case_files (test_case_id, file_name, file_id) + VALUES (10, '__STDERR__', 6); + +-- metadata_id 13 +INSERT INTO metadatas VALUES (13, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (13, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (13, 'description', ''); +INSERT INTO metadatas VALUES (13, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (13, 'required_configs', ''); +INSERT INTO metadatas VALUES (13, 'required_files', ''); +INSERT INTO metadatas VALUES (13, 'required_memory', '0'); +INSERT INTO metadatas VALUES (13, 'required_programs', ''); +INSERT INTO metadatas VALUES (13, 'required_user', ''); +INSERT INTO metadatas VALUES (13, 'timeout', '300'); + +-- test_program_id 9 +INSERT INTO test_programs (test_program_id, absolute_path, root, + relative_path, test_suite_name, metadata_id, + interface) + VALUES (9, '/usr/tests/complex_test', '/usr/tests', + 'complex_test', 'suite-name', 14, 'atf'); + +-- metadata_id 15 +INSERT INTO metadatas VALUES (15, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (15, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (15, 'description', ''); +INSERT INTO metadatas VALUES (15, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (15, 'required_configs', ''); +INSERT INTO metadatas VALUES (15, 'required_files', ''); +INSERT INTO metadatas VALUES (15, 'required_memory', '0'); +INSERT INTO metadatas VALUES (15, 'required_programs', ''); +INSERT INTO metadatas VALUES (15, 'required_user', ''); +INSERT INTO metadatas VALUES (15, 'timeout', '300'); + +-- test_case_id 11 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (11, 9, 'this_passes', 15); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (11, 'passed', NULL, 1357644396500000, 1357644397000000); + +-- metadata_id 16 +INSERT INTO metadatas VALUES (16, 'allowed_architectures', ''); +INSERT INTO metadatas VALUES (16, 'allowed_platforms', ''); +INSERT INTO metadatas VALUES (16, 'description', 'Test description'); +INSERT INTO metadatas VALUES (16, 'has_cleanup', 'false'); +INSERT INTO metadatas VALUES (16, 'required_configs', ''); +INSERT INTO metadatas VALUES (16, 'required_files', ''); +INSERT INTO metadatas VALUES (16, 'required_memory', '0'); +INSERT INTO metadatas VALUES (16, 'required_programs', ''); +INSERT INTO metadatas VALUES (16, 'required_user', 'root'); +INSERT INTO metadatas VALUES (16, 'timeout', '300'); + +-- test_case_id 12 +INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id) + VALUES (12, 9, 'this_fails', 16); +INSERT INTO test_results (test_case_id, result_type, result_reason, start_time, + end_time) + VALUES (12, 'failed', 'Some reason', 1357644397100000, 1357644399005000); + + +COMMIT TRANSACTION; diff --git a/store/transaction_test.cpp b/store/transaction_test.cpp new file mode 100644 index 000000000000..62db8bf1ffbe --- /dev/null +++ b/store/transaction_test.cpp @@ -0,0 +1,170 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "store/read_backend.hpp" +#include "store/read_transaction.hpp" +#include "store/write_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/datetime.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/units.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace units = utils::units; + + +namespace { + + +/// Puts and gets a context and validates the results. +/// +/// \param exp_context The context to save and restore. +static void +check_get_put_context(const model::context& exp_context) +{ + const fs::path test_db("test.db"); + + if (fs::exists(test_db)) + fs::unlink(test_db); + + { + store::write_backend backend = store::write_backend::open_rw(test_db); + store::write_transaction tx = backend.start_write(); + tx.put_context(exp_context); + tx.commit(); + } + { + store::read_backend backend = store::read_backend::open_ro(test_db); + store::read_transaction tx = backend.start_read(); + model::context context = tx.get_context(); + tx.finish(); + + ATF_REQUIRE(exp_context == context); + } +} + + +} // anonymous namespace + + +ATF_TEST_CASE(get_put_context__ok); +ATF_TEST_CASE_HEAD(get_put_context__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_put_context__ok) +{ + std::map< std::string, std::string > env1; + env1["A1"] = "foo"; + env1["A2"] = "bar"; + std::map< std::string, std::string > env2; + check_get_put_context(model::context(fs::path("/foo/bar"), env1)); + check_get_put_context(model::context(fs::path("/foo/bar"), env1)); + check_get_put_context(model::context(fs::path("/foo/baz"), env2)); +} + + +ATF_TEST_CASE(get_put_test_case__ok); +ATF_TEST_CASE_HEAD(get_put_test_case__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(get_put_test_case__ok) +{ + const model::metadata md2 = model::metadata_builder() + .add_allowed_architecture("powerpc") + .add_allowed_architecture("x86_64") + .add_allowed_platform("amd64") + .add_allowed_platform("macppc") + .add_custom("user1", "value1") + .add_custom("user2", "value2") + .add_required_config("var1") + .add_required_config("var2") + .add_required_config("var3") + .add_required_file(fs::path("/file1/yes")) + .add_required_file(fs::path("/file2/foo")) + .add_required_program(fs::path("/bin/ls")) + .add_required_program(fs::path("cp")) + .set_description("The description") + .set_has_cleanup(true) + .set_required_memory(units::bytes::parse("1k")) + .set_required_user("root") + .set_timeout(datetime::delta(520, 0)) + .build(); + + const model::test_program test_program = model::test_program_builder( + "atf", fs::path("the/binary"), fs::path("/some/root"), "the-suite") + .add_test_case("tc1") + .add_test_case("tc2", md2) + .build(); + + int64_t test_program_id; + { + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + + store::write_transaction tx = backend.start_write(); + test_program_id = tx.put_test_program(test_program); + tx.put_test_case(test_program, "tc1", test_program_id); + tx.put_test_case(test_program, "tc2", test_program_id); + tx.commit(); + } + + store::read_backend backend = store::read_backend::open_ro( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + + store::read_transaction tx = backend.start_read(); + const model::test_program_ptr loaded_test_program = + store::detail::get_test_program(backend, test_program_id); + ATF_REQUIRE(test_program == *loaded_test_program); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, get_put_context__ok); + + ATF_ADD_TEST_CASE(tcs, get_put_test_case__ok); +} diff --git a/store/write_backend.cpp b/store/write_backend.cpp new file mode 100644 index 000000000000..7a3eb167f88f --- /dev/null +++ b/store/write_backend.cpp @@ -0,0 +1,208 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/write_backend.hpp" + +#include + +#include "store/exceptions.hpp" +#include "store/metadata.hpp" +#include "store/read_backend.hpp" +#include "store/write_transaction.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + + +/// The current schema version. +/// +/// Any new database gets this schema version. Existing databases with an older +/// schema version must be first migrated to the current schema with +/// migrate_schema() before they can be used. +/// +/// This must be kept in sync with the value in the corresponding schema_vX.sql +/// file, where X matches this version number. +/// +/// This variable is not const to allow tests to modify it. No other code +/// should change its value. +int store::detail::current_schema_version = 3; + + +namespace { + + +/// Checks if a database is empty (i.e. if it is new). +/// +/// \param db The database to check. +/// +/// \return True if the database is empty. +static bool +empty_database(sqlite::database& db) +{ + sqlite::statement stmt = db.create_statement("SELECT * FROM sqlite_master"); + return !stmt.step(); +} + + +} // anonymous namespace + + +/// Calculates the path to the schema file for the database. +/// +/// \return The path to the installed schema_vX.sql file that matches the +/// current_schema_version. +fs::path +store::detail::schema_file(void) +{ + return fs::path(utils::getenv_with_default("KYUA_STOREDIR", KYUA_STOREDIR)) + / (F("schema_v%s.sql") % current_schema_version); +} + + +/// Initializes an empty database. +/// +/// \param db The database to initialize. +/// +/// \return The metadata record written into the new database. +/// +/// \throw store::error If there is a problem initializing the database. +store::metadata +store::detail::initialize(sqlite::database& db) +{ + PRE(empty_database(db)); + + const fs::path schema = schema_file(); + + LI(F("Populating new database with schema from %s") % schema); + try { + db.exec(utils::read_file(schema)); + + const metadata metadata = metadata::fetch_latest(db); + LI(F("New metadata entry %s") % metadata.timestamp()); + if (metadata.schema_version() != detail::current_schema_version) { + UNREACHABLE_MSG(F("current_schema_version is out of sync with " + "%s") % schema); + } + return metadata; + } catch (const store::integrity_error& e) { + // Could be raised by metadata::fetch_latest. + UNREACHABLE_MSG("Inconsistent code while creating a database"); + } catch (const sqlite::error& e) { + throw error(F("Failed to initialize database: %s") % e.what()); + } catch (const std::runtime_error& e) { + throw error(F("Cannot read database schema '%s'") % schema); + } +} + + +/// Internal implementation for the backend. +struct store::write_backend::impl : utils::noncopyable { + /// The SQLite database this backend talks to. + sqlite::database database; + + /// Constructor. + /// + /// \param database_ The SQLite database instance. + impl(sqlite::database& database_) : database(database_) + { + } +}; + + +/// Constructs a new backend. +/// +/// \param pimpl_ The internal data. +store::write_backend::write_backend(impl* pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Destructor. +store::write_backend::~write_backend(void) +{ +} + + +/// Opens a database in read-write mode and creates it if necessary. +/// +/// \param file The database file to be opened. +/// +/// \return The backend representation. +/// +/// \throw store::error If there is any problem opening or creating +/// the database. +store::write_backend +store::write_backend::open_rw(const fs::path& file) +{ + sqlite::database db = detail::open_and_setup( + file, sqlite::open_readwrite | sqlite::open_create); + if (!empty_database(db)) + throw error(F("%s already exists and is not empty; cannot open " + "for write") % file); + detail::initialize(db); + return write_backend(new impl(db)); +} + + +/// Closes the SQLite database. +void +store::write_backend::close(void) +{ + _pimpl->database.close(); +} + + +/// Gets the connection to the SQLite database. +/// +/// \return A database connection. +sqlite::database& +store::write_backend::database(void) +{ + return _pimpl->database; +} + + +/// Opens a write-only transaction. +/// +/// \return A new transaction. +store::write_transaction +store::write_backend::start_write(void) +{ + return write_transaction(*this); +} diff --git a/store/write_backend.hpp b/store/write_backend.hpp new file mode 100644 index 000000000000..a1d46f1450c0 --- /dev/null +++ b/store/write_backend.hpp @@ -0,0 +1,81 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/write_backend.hpp +/// Interface to the backend database for write-only operations. + +#if !defined(STORE_WRITE_BACKEND_HPP) +#define STORE_WRITE_BACKEND_HPP + +#include "store/write_backend_fwd.hpp" + +#include + +#include "store/metadata_fwd.hpp" +#include "store/write_transaction_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/sqlite/database_fwd.hpp" + +namespace store { + + +namespace detail { + + +utils::fs::path schema_file(void); +metadata initialize(utils::sqlite::database&); + + +} // anonymous namespace + + +/// Public interface to the database store for write-only operations. +class write_backend { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class metadata; + + write_backend(impl*); + +public: + ~write_backend(void); + + static write_backend open_rw(const utils::fs::path&); + void close(void); + + utils::sqlite::database& database(void); + write_transaction start_write(void); +}; + + +} // namespace store + +#endif // !defined(STORE_WRITE_BACKEND_HPP) diff --git a/store/write_backend_fwd.hpp b/store/write_backend_fwd.hpp new file mode 100644 index 000000000000..8f2ea12d25cb --- /dev/null +++ b/store/write_backend_fwd.hpp @@ -0,0 +1,52 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/write_backend_fwd.hpp +/// Forward declarations for store/write_backend.hpp + +#if !defined(STORE_WRITE_BACKEND_FWD_HPP) +#define STORE_WRITE_BACKEND_FWD_HPP + +namespace store { + + +namespace detail { + + +extern int current_schema_version; + + +} // namespace detail + + +class write_backend; + + +} // namespace store + +#endif // !defined(STORE_WRITE_BACKEND_FWD_HPP) diff --git a/store/write_backend_test.cpp b/store/write_backend_test.cpp new file mode 100644 index 000000000000..a1052154aaae --- /dev/null +++ b/store/write_backend_test.cpp @@ -0,0 +1,204 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/write_backend.hpp" + +#include + +#include "store/exceptions.hpp" +#include "store/metadata.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE(detail__initialize__ok); +ATF_TEST_CASE_HEAD(detail__initialize__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(detail__initialize__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + const datetime::timestamp before = datetime::timestamp::now(); + const store::metadata md = store::detail::initialize(db); + const datetime::timestamp after = datetime::timestamp::now(); + + ATF_REQUIRE(md.timestamp() >= before.to_seconds()); + ATF_REQUIRE(md.timestamp() <= after.to_microseconds()); + ATF_REQUIRE_EQ(store::detail::current_schema_version, md.schema_version()); + + // Query some known tables to ensure they were created. + db.exec("SELECT * FROM metadata"); + + // And now query some know values. + sqlite::statement stmt = db.create_statement( + "SELECT COUNT(*) FROM metadata"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(1, stmt.column_int(0)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__initialize__missing_schema); +ATF_TEST_CASE_BODY(detail__initialize__missing_schema) +{ + utils::setenv("KYUA_STOREDIR", "/non-existent"); + store::detail::current_schema_version = 712; + + sqlite::database db = sqlite::database::in_memory(); + ATF_REQUIRE_THROW_RE(store::error, + "Cannot read.*'/non-existent/schema_v712.sql'", + store::detail::initialize(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__initialize__sqlite_error); +ATF_TEST_CASE_BODY(detail__initialize__sqlite_error) +{ + utils::setenv("KYUA_STOREDIR", "."); + store::detail::current_schema_version = 712; + + atf::utils::create_file("schema_v712.sql", "foo_bar_baz;\n"); + + sqlite::database db = sqlite::database::in_memory(); + ATF_REQUIRE_THROW_RE(store::error, "Failed to initialize.*:.*foo_bar_baz", + store::detail::initialize(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__schema_file__builtin); +ATF_TEST_CASE_BODY(detail__schema_file__builtin) +{ + utils::unsetenv("KYUA_STOREDIR"); + ATF_REQUIRE_EQ(fs::path(KYUA_STOREDIR) / "schema_v3.sql", + store::detail::schema_file()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__schema_file__overriden); +ATF_TEST_CASE_BODY(detail__schema_file__overriden) +{ + utils::setenv("KYUA_STOREDIR", "/tmp/test"); + store::detail::current_schema_version = 123; + ATF_REQUIRE_EQ(fs::path("/tmp/test/schema_v123.sql"), + store::detail::schema_file()); +} + + +ATF_TEST_CASE(write_backend__open_rw__ok_if_empty); +ATF_TEST_CASE_HEAD(write_backend__open_rw__ok_if_empty) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(write_backend__open_rw__ok_if_empty) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + } + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); +} + + +ATF_TEST_CASE(write_backend__open_rw__error_if_not_empty); +ATF_TEST_CASE_HEAD(write_backend__open_rw__error_if_not_empty) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(write_backend__open_rw__error_if_not_empty) +{ + { + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + store::detail::initialize(db); + } + ATF_REQUIRE_THROW_RE(store::error, "test.db already exists", + store::write_backend::open_rw(fs::path("test.db"))); +} + + +ATF_TEST_CASE(write_backend__open_rw__create_missing); +ATF_TEST_CASE_HEAD(write_backend__open_rw__create_missing) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(write_backend__open_rw__create_missing) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); +} + + +ATF_TEST_CASE(write_backend__close); +ATF_TEST_CASE_HEAD(write_backend__close) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(write_backend__close) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("SELECT * FROM metadata"); + backend.close(); + ATF_REQUIRE_THROW(utils::sqlite::error, + backend.database().exec("SELECT * FROM metadata")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, detail__initialize__ok); + ATF_ADD_TEST_CASE(tcs, detail__initialize__missing_schema); + ATF_ADD_TEST_CASE(tcs, detail__initialize__sqlite_error); + + ATF_ADD_TEST_CASE(tcs, detail__schema_file__builtin); + ATF_ADD_TEST_CASE(tcs, detail__schema_file__overriden); + + ATF_ADD_TEST_CASE(tcs, write_backend__open_rw__ok_if_empty); + ATF_ADD_TEST_CASE(tcs, write_backend__open_rw__error_if_not_empty); + ATF_ADD_TEST_CASE(tcs, write_backend__open_rw__create_missing); + ATF_ADD_TEST_CASE(tcs, write_backend__close); +} diff --git a/store/write_transaction.cpp b/store/write_transaction.cpp new file mode 100644 index 000000000000..134a13a30494 --- /dev/null +++ b/store/write_transaction.cpp @@ -0,0 +1,440 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/write_transaction.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "model/types.hpp" +#include "store/dbtypes.hpp" +#include "store/exceptions.hpp" +#include "store/write_backend.hpp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" +#include "utils/sqlite/transaction.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Stores the environment variables of a context. +/// +/// \param db The SQLite database. +/// \param env The environment variables to store. +/// +/// \throw sqlite::error If there is a problem storing the variables. +static void +put_env_vars(sqlite::database& db, + const std::map< std::string, std::string >& env) +{ + sqlite::statement stmt = db.create_statement( + "INSERT INTO env_vars (var_name, var_value) " + "VALUES (:var_name, :var_value)"); + for (std::map< std::string, std::string >::const_iterator iter = + env.begin(); iter != env.end(); iter++) { + stmt.bind(":var_name", (*iter).first); + stmt.bind(":var_value", (*iter).second); + stmt.step_without_results(); + stmt.reset(); + } +} + + +/// Calculates the last rowid of a table. +/// +/// \param db The SQLite database. +/// \param table Name of the table. +/// +/// \return The last rowid; 0 if the table is empty. +static int64_t +last_rowid(sqlite::database& db, const std::string& table) +{ + sqlite::statement stmt = db.create_statement( + F("SELECT MAX(ROWID) AS max_rowid FROM %s") % table); + stmt.step(); + if (stmt.column_type(0) == sqlite::type_null) { + return 0; + } else { + INV(stmt.column_type(0) == sqlite::type_integer); + return stmt.column_int64(0); + } +} + + +/// Stores a metadata object. +/// +/// \param db The database into which to store the information. +/// \param md The metadata to store. +/// +/// \return The identifier of the new metadata object. +static int64_t +put_metadata(sqlite::database& db, const model::metadata& md) +{ + const model::properties_map props = md.to_properties(); + + const int64_t metadata_id = last_rowid(db, "metadatas"); + + sqlite::statement stmt = db.create_statement( + "INSERT INTO metadatas (metadata_id, property_name, property_value) " + "VALUES (:metadata_id, :property_name, :property_value)"); + stmt.bind(":metadata_id", metadata_id); + + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + stmt.bind(":property_name", (*iter).first); + stmt.bind(":property_value", (*iter).second); + stmt.step_without_results(); + stmt.reset(); + } + + return metadata_id; +} + + +/// Stores an arbitrary file into the database as a BLOB. +/// +/// \param db The database into which to store the file. +/// \param path Path to the file to be stored. +/// +/// \return The identifier of the stored file, or none if the file was empty. +/// +/// \throw sqlite::error If there are problems writing to the database. +static optional< int64_t > +put_file(sqlite::database& db, const fs::path& path) +{ + std::ifstream input(path.c_str()); + if (!input) + throw store::error(F("Cannot open file %s") % path); + + try { + if (utils::stream_length(input) == 0) + return none; + } catch (const std::runtime_error& e) { + // Skipping empty files is an optimization. If we fail to calculate the + // size of the file, just ignore the problem. If there are real issues + // with the file, the read below will fail anyway. + LD(F("Cannot determine if file is empty: %s") % e.what()); + } + + // TODO(jmmv): This will probably cause an unreasonable amount of memory + // consumption if we decide to store arbitrary files in the database (other + // than stdout or stderr). Should this happen, we need to investigate a + // better way to feel blobs into SQLite. + const std::string contents = utils::read_stream(input); + + sqlite::statement stmt = db.create_statement( + "INSERT INTO files (contents) VALUES (:contents)"); + stmt.bind(":contents", sqlite::blob(contents.c_str(), contents.length())); + stmt.step_without_results(); + + return optional< int64_t >(db.last_insert_rowid()); +} + + +} // anonymous namespace + + +/// Internal implementation for a store write-only transaction. +struct store::write_transaction::impl : utils::noncopyable { + /// The backend instance. + store::write_backend& _backend; + + /// The SQLite database this transaction deals with. + sqlite::database _db; + + /// The backing SQLite transaction. + sqlite::transaction _tx; + + /// Opens a transaction. + /// + /// \param backend_ The backend this transaction is connected to. + impl(write_backend& backend_) : + _backend(backend_), + _db(backend_.database()), + _tx(backend_.database().begin_transaction()) + { + } +}; + + +/// Creates a new write-only transaction. +/// +/// \param backend_ The backend this transaction belongs to. +store::write_transaction::write_transaction(write_backend& backend_) : + _pimpl(new impl(backend_)) +{ +} + + +/// Destructor. +store::write_transaction::~write_transaction(void) +{ +} + + +/// Commits the transaction. +/// +/// \throw error If there is any problem when talking to the database. +void +store::write_transaction::commit(void) +{ + try { + _pimpl->_tx.commit(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Rolls the transaction back. +/// +/// \throw error If there is any problem when talking to the database. +void +store::write_transaction::rollback(void) +{ + try { + _pimpl->_tx.rollback(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Puts a context into the database. +/// +/// \pre The context has not been put yet. +/// \post The context is stored into the database with a new identifier. +/// +/// \param context The context to put. +/// +/// \throw error If there is any problem when talking to the database. +void +store::write_transaction::put_context(const model::context& context) +{ + try { + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO contexts (cwd) VALUES (:cwd)"); + stmt.bind(":cwd", context.cwd().str()); + stmt.step_without_results(); + + put_env_vars(_pimpl->_db, context.env()); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Puts a test program into the database. +/// +/// \pre The test program has not been put yet. +/// \post The test program is stored into the database with a new identifier. +/// +/// \param test_program The test program to put. +/// +/// \return The identifier of the inserted test program. +/// +/// \throw error If there is any problem when talking to the database. +int64_t +store::write_transaction::put_test_program( + const model::test_program& test_program) +{ + try { + const int64_t metadata_id = put_metadata( + _pimpl->_db, test_program.get_metadata()); + + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO test_programs (absolute_path, " + " root, relative_path, test_suite_name, " + " metadata_id, interface) " + "VALUES (:absolute_path, :root, :relative_path, " + " :test_suite_name, :metadata_id, :interface)"); + stmt.bind(":absolute_path", test_program.absolute_path().str()); + // TODO(jmmv): The root is not necessarily absolute. We need to ensure + // that we can recover the absolute path of the test program. Maybe we + // need to change the test_program to always ensure root is absolute? + stmt.bind(":root", test_program.root().str()); + stmt.bind(":relative_path", test_program.relative_path().str()); + stmt.bind(":test_suite_name", test_program.test_suite_name()); + stmt.bind(":metadata_id", metadata_id); + stmt.bind(":interface", test_program.interface_name()); + stmt.step_without_results(); + return _pimpl->_db.last_insert_rowid(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Puts a test case into the database. +/// +/// \pre The test case has not been put yet. +/// \post The test case is stored into the database with a new identifier. +/// +/// \param test_program The program containing the test case to be stored. +/// \param test_case_name The name of the test case to put. +/// \param test_program_id The test program this test case belongs to. +/// +/// \return The identifier of the inserted test case. +/// +/// \throw error If there is any problem when talking to the database. +int64_t +store::write_transaction::put_test_case(const model::test_program& test_program, + const std::string& test_case_name, + const int64_t test_program_id) +{ + const model::test_case& test_case = test_program.find(test_case_name); + + try { + const int64_t metadata_id = put_metadata( + _pimpl->_db, test_case.get_raw_metadata()); + + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO test_cases (test_program_id, name, metadata_id) " + "VALUES (:test_program_id, :name, :metadata_id)"); + stmt.bind(":test_program_id", test_program_id); + stmt.bind(":name", test_case.name()); + stmt.bind(":metadata_id", metadata_id); + stmt.step_without_results(); + return _pimpl->_db.last_insert_rowid(); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Stores a file generated by a test case into the database as a BLOB. +/// +/// \param name The name of the file to store in the database. This needs to be +/// unique per test case. The caller is free to decide what names to use +/// for which files. For example, it might make sense to always call +/// __STDOUT__ the stdout of the test case so that it is easy to locate. +/// \param path The path to the file to be stored. +/// \param test_case_id The identifier of the test case this file belongs to. +/// +/// \return The identifier of the stored file, or none if the file was empty. +/// +/// \throw store::error If there are problems writing to the database. +optional< int64_t > +store::write_transaction::put_test_case_file(const std::string& name, + const fs::path& path, + const int64_t test_case_id) +{ + LD(F("Storing %s (%s) of test case %s") % name % path % test_case_id); + try { + const optional< int64_t > file_id = put_file(_pimpl->_db, path); + if (!file_id) { + LD("Not storing empty file"); + return none; + } + + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO test_case_files (test_case_id, file_name, file_id) " + "VALUES (:test_case_id, :file_name, :file_id)"); + stmt.bind(":test_case_id", test_case_id); + stmt.bind(":file_name", name); + stmt.bind(":file_id", file_id.get()); + stmt.step_without_results(); + + return optional< int64_t >(_pimpl->_db.last_insert_rowid()); + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} + + +/// Puts a result into the database. +/// +/// \pre The result has not been put yet. +/// \post The result is stored into the database with a new identifier. +/// +/// \param result The result to put. +/// \param test_case_id The test case this result corresponds to. +/// \param start_time The time when the test started to run. +/// \param end_time The time when the test finished running. +/// +/// \return The identifier of the inserted result. +/// +/// \throw error If there is any problem when talking to the database. +int64_t +store::write_transaction::put_result(const model::test_result& result, + const int64_t test_case_id, + const datetime::timestamp& start_time, + const datetime::timestamp& end_time) +{ + try { + sqlite::statement stmt = _pimpl->_db.create_statement( + "INSERT INTO test_results (test_case_id, result_type, " + " result_reason, start_time, " + " end_time) " + "VALUES (:test_case_id, :result_type, :result_reason, " + " :start_time, :end_time)"); + stmt.bind(":test_case_id", test_case_id); + + store::bind_test_result_type(stmt, ":result_type", result.type()); + if (result.reason().empty()) + stmt.bind(":result_reason", sqlite::null()); + else + stmt.bind(":result_reason", result.reason()); + + store::bind_timestamp(stmt, ":start_time", start_time); + store::bind_timestamp(stmt, ":end_time", end_time); + + stmt.step_without_results(); + const int64_t result_id = _pimpl->_db.last_insert_rowid(); + + return result_id; + } catch (const sqlite::error& e) { + throw error(e.what()); + } +} diff --git a/store/write_transaction.hpp b/store/write_transaction.hpp new file mode 100644 index 000000000000..5c73d20af788 --- /dev/null +++ b/store/write_transaction.hpp @@ -0,0 +1,89 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/write_transaction.hpp +/// Implementation of write-only transactions on the backend. + +#if !defined(STORE_WRITE_TRANSACTION_HPP) +#define STORE_WRITE_TRANSACTION_HPP + +#include "store/write_transaction_fwd.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "model/context_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result_fwd.hpp" +#include "store/write_backend_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace store { + + +/// Representation of a write-only transaction. +/// +/// Transactions are the entry place for high-level calls that access the +/// database. +class write_transaction { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class write_backend; + write_transaction(write_backend&); + +public: + ~write_transaction(void); + + void commit(void); + void rollback(void); + + void put_context(const model::context&); + int64_t put_test_program(const model::test_program&); + int64_t put_test_case(const model::test_program&, const std::string&, + const int64_t); + utils::optional< int64_t > put_test_case_file(const std::string&, + const utils::fs::path&, + const int64_t); + int64_t put_result(const model::test_result&, const int64_t, + const utils::datetime::timestamp&, + const utils::datetime::timestamp&); +}; + + +} // namespace store + +#endif // !defined(STORE_WRITE_TRANSACTION_HPP) diff --git a/store/write_transaction_fwd.hpp b/store/write_transaction_fwd.hpp new file mode 100644 index 000000000000..1d2357a52dbe --- /dev/null +++ b/store/write_transaction_fwd.hpp @@ -0,0 +1,43 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file store/write_transaction_fwd.hpp +/// Forward declarations for store/write_transaction.hpp + +#if !defined(STORE_WRITE_TRANSACTION_FWD_HPP) +#define STORE_WRITE_TRANSACTION_FWD_HPP + +namespace store { + + +class write_transaction; + + +} // namespace store + +#endif // !defined(STORE_WRITE_TRANSACTION_FWD_HPP) diff --git a/store/write_transaction_test.cpp b/store/write_transaction_test.cpp new file mode 100644 index 000000000000..984e328dcdae --- /dev/null +++ b/store/write_transaction_test.cpp @@ -0,0 +1,416 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "store/write_transaction.hpp" + +#include +#include +#include + +#include + +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/exceptions.hpp" +#include "store/write_backend.hpp" +#include "utils/datetime.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace sqlite = utils::sqlite; + +using utils::optional; + + +namespace { + + +/// Performs a test for a working put_result +/// +/// \param result The result object to put. +/// \param result_type The textual name of the result to expect in the +/// database. +/// \param exp_reason The reason to expect in the database. This is separate +/// from the result parameter so that we can handle passed() here as well. +/// Just provide NULL in this case. +static void +do_put_result_ok_test(const model::test_result& result, + const char* result_type, const char* exp_reason) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + const datetime::timestamp start_time = datetime::timestamp::from_values( + 2012, 01, 30, 22, 10, 00, 0); + const datetime::timestamp end_time = datetime::timestamp::from_values( + 2012, 01, 30, 22, 15, 30, 123456); + tx.put_result(result, 312, start_time, end_time); + tx.commit(); + + sqlite::statement stmt = backend.database().create_statement( + "SELECT test_case_id, result_type, result_reason " + "FROM test_results"); + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(312, stmt.column_int64(0)); + ATF_REQUIRE_EQ(result_type, stmt.column_text(1)); + if (exp_reason != NULL) + ATF_REQUIRE_EQ(exp_reason, stmt.column_text(2)); + else + ATF_REQUIRE(stmt.column_type(2) == sqlite::type_null); + ATF_REQUIRE(!stmt.step()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE(commit__ok); +ATF_TEST_CASE_HEAD(commit__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(commit__ok) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + backend.database().exec("CREATE TABLE a (b INTEGER PRIMARY KEY)"); + backend.database().exec("SELECT * FROM a"); + tx.commit(); + backend.database().exec("SELECT * FROM a"); +} + + +ATF_TEST_CASE(commit__fail); +ATF_TEST_CASE_HEAD(commit__fail) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(commit__fail) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + const model::context context(fs::path("/foo/bar"), + std::map< std::string, std::string >()); + { + store::write_transaction tx = backend.start_write(); + tx.put_context(context); + backend.database().exec( + "CREATE TABLE foo (" + "a REFERENCES env_vars(var_name) DEFERRABLE INITIALLY DEFERRED)"); + backend.database().exec("INSERT INTO foo VALUES (\"WHAT\")"); + ATF_REQUIRE_THROW(store::error, tx.commit()); + } + // If the code attempts to maintain any state regarding the already-put + // objects and the commit does not clean up correctly, this would fail in + // some manner. + store::write_transaction tx = backend.start_write(); + tx.put_context(context); + tx.commit(); +} + + +ATF_TEST_CASE(rollback__ok); +ATF_TEST_CASE_HEAD(rollback__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(rollback__ok) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + backend.database().exec("CREATE TABLE a_table (b INTEGER PRIMARY KEY)"); + backend.database().exec("SELECT * FROM a_table"); + tx.rollback(); + ATF_REQUIRE_THROW_RE(sqlite::error, "a_table", + backend.database().exec("SELECT * FROM a_table")); +} + + +ATF_TEST_CASE(put_test_program__ok); +ATF_TEST_CASE_HEAD(put_test_program__ok) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_program__ok) +{ + const model::metadata md = model::metadata_builder() + .add_custom("var1", "value1") + .add_custom("var2", "value2") + .build(); + const model::test_program test_program( + "mock", fs::path("the/binary"), fs::path("/some//root"), + "the-suite", md, model::test_cases_map()); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + const int64_t test_program_id = tx.put_test_program(test_program); + tx.commit(); + + { + sqlite::statement stmt = backend.database().create_statement( + "SELECT * FROM test_programs"); + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(test_program_id, + stmt.safe_column_int64("test_program_id")); + ATF_REQUIRE_EQ("/some/root/the/binary", + stmt.safe_column_text("absolute_path")); + ATF_REQUIRE_EQ("/some/root", stmt.safe_column_text("root")); + ATF_REQUIRE_EQ("the/binary", stmt.safe_column_text("relative_path")); + ATF_REQUIRE_EQ("the-suite", stmt.safe_column_text("test_suite_name")); + ATF_REQUIRE(!stmt.step()); + } +} + + +ATF_TEST_CASE(put_test_case__fail); +ATF_TEST_CASE_HEAD(put_test_case__fail) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_case__fail) +{ + const model::test_program test_program = model::test_program_builder( + "plain", fs::path("the/binary"), fs::path("/some/root"), "the-suite") + .add_test_case("main") + .build(); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + ATF_REQUIRE_THROW(store::error, tx.put_test_case(test_program, "main", -1)); + tx.commit(); +} + + +ATF_TEST_CASE(put_test_case_file__empty); +ATF_TEST_CASE_HEAD(put_test_case_file__empty) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_case_file__empty) +{ + atf::utils::create_file("input.txt", ""); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + const optional< int64_t > file_id = tx.put_test_case_file( + "my-file", fs::path("input.txt"), 123L); + tx.commit(); + ATF_REQUIRE(!file_id); + + sqlite::statement stmt = backend.database().create_statement( + "SELECT * FROM test_case_files NATURAL JOIN files"); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE(put_test_case_file__some); +ATF_TEST_CASE_HEAD(put_test_case_file__some) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_case_file__some) +{ + const char contents[] = "This is a test!"; + + atf::utils::create_file("input.txt", contents); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + const optional< int64_t > file_id = tx.put_test_case_file( + "my-file", fs::path("input.txt"), 123L); + tx.commit(); + ATF_REQUIRE(file_id); + + sqlite::statement stmt = backend.database().create_statement( + "SELECT * FROM test_case_files NATURAL JOIN files"); + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(123L, stmt.safe_column_int64("test_case_id")); + ATF_REQUIRE_EQ("my-file", stmt.safe_column_text("file_name")); + const sqlite::blob blob = stmt.safe_column_blob("contents"); + ATF_REQUIRE(std::strlen(contents) == static_cast< std::size_t >(blob.size)); + ATF_REQUIRE(std::memcmp(contents, blob.memory, blob.size) == 0); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE(put_test_case_file__fail); +ATF_TEST_CASE_HEAD(put_test_case_file__fail) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_test_case_file__fail) +{ + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + backend.database().exec("PRAGMA foreign_keys = OFF"); + store::write_transaction tx = backend.start_write(); + ATF_REQUIRE_THROW(store::error, + tx.put_test_case_file("foo", fs::path("missing"), 1L)); + tx.commit(); + + sqlite::statement stmt = backend.database().create_statement( + "SELECT * FROM test_case_files NATURAL JOIN files"); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE(put_result__ok__broken); +ATF_TEST_CASE_HEAD(put_result__ok__broken) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__broken) +{ + const model::test_result result(model::test_result_broken, "a b cd"); + do_put_result_ok_test(result, "broken", "a b cd"); +} + + +ATF_TEST_CASE(put_result__ok__expected_failure); +ATF_TEST_CASE_HEAD(put_result__ok__expected_failure) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__expected_failure) +{ + const model::test_result result(model::test_result_expected_failure, + "a b cd"); + do_put_result_ok_test(result, "expected_failure", "a b cd"); +} + + +ATF_TEST_CASE(put_result__ok__failed); +ATF_TEST_CASE_HEAD(put_result__ok__failed) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__failed) +{ + const model::test_result result(model::test_result_failed, "a b cd"); + do_put_result_ok_test(result, "failed", "a b cd"); +} + + +ATF_TEST_CASE(put_result__ok__passed); +ATF_TEST_CASE_HEAD(put_result__ok__passed) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__passed) +{ + const model::test_result result(model::test_result_passed); + do_put_result_ok_test(result, "passed", NULL); +} + + +ATF_TEST_CASE(put_result__ok__skipped); +ATF_TEST_CASE_HEAD(put_result__ok__skipped) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__ok__skipped) +{ + const model::test_result result(model::test_result_skipped, "a b cd"); + do_put_result_ok_test(result, "skipped", "a b cd"); +} + + +ATF_TEST_CASE(put_result__fail); +ATF_TEST_CASE_HEAD(put_result__fail) +{ + logging::set_inmemory(); + set_md_var("require.files", store::detail::schema_file().c_str()); +} +ATF_TEST_CASE_BODY(put_result__fail) +{ + const model::test_result result(model::test_result_broken, "foo"); + + store::write_backend backend = store::write_backend::open_rw( + fs::path("test.db")); + store::write_transaction tx = backend.start_write(); + const datetime::timestamp zero = datetime::timestamp::from_microseconds(0); + ATF_REQUIRE_THROW(store::error, tx.put_result(result, -1, zero, zero)); + tx.commit(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, commit__ok); + ATF_ADD_TEST_CASE(tcs, commit__fail); + ATF_ADD_TEST_CASE(tcs, rollback__ok); + + ATF_ADD_TEST_CASE(tcs, put_test_program__ok); + ATF_ADD_TEST_CASE(tcs, put_test_case__fail); + ATF_ADD_TEST_CASE(tcs, put_test_case_file__empty); + ATF_ADD_TEST_CASE(tcs, put_test_case_file__some); + ATF_ADD_TEST_CASE(tcs, put_test_case_file__fail); + + ATF_ADD_TEST_CASE(tcs, put_result__ok__broken); + ATF_ADD_TEST_CASE(tcs, put_result__ok__expected_failure); + ATF_ADD_TEST_CASE(tcs, put_result__ok__failed); + ATF_ADD_TEST_CASE(tcs, put_result__ok__passed); + ATF_ADD_TEST_CASE(tcs, put_result__ok__skipped); + ATF_ADD_TEST_CASE(tcs, put_result__fail); +} diff --git a/utils/.gitignore b/utils/.gitignore new file mode 100644 index 000000000000..b33d720f27a4 --- /dev/null +++ b/utils/.gitignore @@ -0,0 +1,2 @@ +defs.hpp +stacktrace_helper diff --git a/utils/Kyuafile b/utils/Kyuafile new file mode 100644 index 000000000000..042ad77a3fe4 --- /dev/null +++ b/utils/Kyuafile @@ -0,0 +1,24 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="auto_array_test"} +atf_test_program{name="datetime_test"} +atf_test_program{name="env_test"} +atf_test_program{name="memory_test"} +atf_test_program{name="optional_test"} +atf_test_program{name="passwd_test"} +atf_test_program{name="sanity_test"} +atf_test_program{name="stacktrace_test"} +atf_test_program{name="stream_test"} +atf_test_program{name="units_test"} + +include("cmdline/Kyuafile") +include("config/Kyuafile") +include("format/Kyuafile") +include("fs/Kyuafile") +include("logging/Kyuafile") +include("process/Kyuafile") +include("signals/Kyuafile") +include("sqlite/Kyuafile") +include("text/Kyuafile") diff --git a/utils/Makefile.am.inc b/utils/Makefile.am.inc new file mode 100644 index 000000000000..d6690bdbecde --- /dev/null +++ b/utils/Makefile.am.inc @@ -0,0 +1,133 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +UTILS_CFLAGS = +UTILS_LIBS = libutils.a + +noinst_LIBRARIES += libutils.a +libutils_a_CPPFLAGS = -DGDB=\"$(GDB)\" +libutils_a_SOURCES = utils/auto_array.hpp +libutils_a_SOURCES += utils/auto_array.ipp +libutils_a_SOURCES += utils/auto_array_fwd.hpp +libutils_a_SOURCES += utils/datetime.cpp +libutils_a_SOURCES += utils/datetime.hpp +libutils_a_SOURCES += utils/datetime_fwd.hpp +libutils_a_SOURCES += utils/env.hpp +libutils_a_SOURCES += utils/env.cpp +libutils_a_SOURCES += utils/memory.hpp +libutils_a_SOURCES += utils/memory.cpp +libutils_a_SOURCES += utils/noncopyable.hpp +libutils_a_SOURCES += utils/optional.hpp +libutils_a_SOURCES += utils/optional_fwd.hpp +libutils_a_SOURCES += utils/optional.ipp +libutils_a_SOURCES += utils/passwd.cpp +libutils_a_SOURCES += utils/passwd.hpp +libutils_a_SOURCES += utils/passwd_fwd.hpp +libutils_a_SOURCES += utils/sanity.cpp +libutils_a_SOURCES += utils/sanity.hpp +libutils_a_SOURCES += utils/sanity_fwd.hpp +libutils_a_SOURCES += utils/stacktrace.cpp +libutils_a_SOURCES += utils/stacktrace.hpp +libutils_a_SOURCES += utils/stream.cpp +libutils_a_SOURCES += utils/stream.hpp +libutils_a_SOURCES += utils/units.cpp +libutils_a_SOURCES += utils/units.hpp +libutils_a_SOURCES += utils/units_fwd.hpp +nodist_libutils_a_SOURCES = utils/defs.hpp + +EXTRA_DIST += utils/test_utils.ipp + +if WITH_ATF +tests_utilsdir = $(pkgtestsdir)/utils + +tests_utils_DATA = utils/Kyuafile +EXTRA_DIST += $(tests_utils_DATA) + +tests_utils_PROGRAMS = utils/auto_array_test +utils_auto_array_test_SOURCES = utils/auto_array_test.cpp +utils_auto_array_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_auto_array_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/datetime_test +utils_datetime_test_SOURCES = utils/datetime_test.cpp +utils_datetime_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_datetime_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/env_test +utils_env_test_SOURCES = utils/env_test.cpp +utils_env_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_env_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/memory_test +utils_memory_test_SOURCES = utils/memory_test.cpp +utils_memory_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_memory_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/optional_test +utils_optional_test_SOURCES = utils/optional_test.cpp +utils_optional_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_optional_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/passwd_test +utils_passwd_test_SOURCES = utils/passwd_test.cpp +utils_passwd_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_passwd_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/sanity_test +utils_sanity_test_SOURCES = utils/sanity_test.cpp +utils_sanity_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sanity_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/stacktrace_helper +utils_stacktrace_helper_SOURCES = utils/stacktrace_helper.cpp + +tests_utils_PROGRAMS += utils/stacktrace_test +utils_stacktrace_test_SOURCES = utils/stacktrace_test.cpp +utils_stacktrace_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_stacktrace_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/stream_test +utils_stream_test_SOURCES = utils/stream_test.cpp +utils_stream_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_stream_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_PROGRAMS += utils/units_test +utils_units_test_SOURCES = utils/units_test.cpp +utils_units_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_units_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif + +include utils/cmdline/Makefile.am.inc +include utils/config/Makefile.am.inc +include utils/format/Makefile.am.inc +include utils/fs/Makefile.am.inc +include utils/logging/Makefile.am.inc +include utils/process/Makefile.am.inc +include utils/signals/Makefile.am.inc +include utils/sqlite/Makefile.am.inc +include utils/text/Makefile.am.inc diff --git a/utils/auto_array.hpp b/utils/auto_array.hpp new file mode 100644 index 000000000000..0cc3d0e0afd5 --- /dev/null +++ b/utils/auto_array.hpp @@ -0,0 +1,102 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/auto_array.hpp +/// Provides the utils::auto_array class. +/// +/// The class is provided as a separate module on its own to minimize +/// header-inclusion side-effects. + +#if !defined(UTILS_AUTO_ARRAY_HPP) +#define UTILS_AUTO_ARRAY_HPP + +#include "utils/auto_array_fwd.hpp" + +#include + +namespace utils { + + +namespace detail { + + +/// Wrapper class to provide reference semantics for utils::auto_array. +/// +/// This class is internally used, for example, to allow returning a +/// utils::auto_array from a function. +template< class T > +class auto_array_ref { + /// Internal pointer to the dynamically-allocated array. + T* _ptr; + + template< class > friend class utils::auto_array; + +public: + explicit auto_array_ref(T*); +}; + + +} // namespace detail + + +/// A simple smart pointer for arrays providing strict ownership semantics. +/// +/// This class is the counterpart of std::auto_ptr for arrays. The semantics of +/// the API of this class are the same as those of std::auto_ptr. +/// +/// The wrapped pointer must be NULL or must have been allocated using operator +/// new[]. +template< class T > +class auto_array { + /// Internal pointer to the dynamically-allocated array. + T* _ptr; + +public: + auto_array(T* = NULL) throw(); + auto_array(auto_array< T >&) throw(); + auto_array(detail::auto_array_ref< T >) throw(); + ~auto_array(void) throw(); + + T* get(void) throw(); + const T* get(void) const throw(); + + T* release(void) throw(); + void reset(T* = NULL) throw(); + + auto_array< T >& operator=(auto_array< T >&) throw(); + auto_array< T >& operator=(detail::auto_array_ref< T >) throw(); + T& operator[](int) throw(); + const T& operator[](int) const throw(); + operator detail::auto_array_ref< T >(void) throw(); +}; + + +} // namespace utils + + +#endif // !defined(UTILS_AUTO_ARRAY_HPP) diff --git a/utils/auto_array.ipp b/utils/auto_array.ipp new file mode 100644 index 000000000000..fd29311def8c --- /dev/null +++ b/utils/auto_array.ipp @@ -0,0 +1,227 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_AUTO_ARRAY_IPP) +#define UTILS_AUTO_ARRAY_IPP + +#include "utils/auto_array.hpp" + +namespace utils { + + +namespace detail { + + +/// Constructs a new auto_array_ref from a pointer. +/// +/// \param ptr The pointer to wrap. +template< class T > inline +auto_array_ref< T >::auto_array_ref(T* ptr) : + _ptr(ptr) +{ +} + + +} // namespace detail + + +/// Constructs a new auto_array from a given pointer. +/// +/// This grabs ownership of the pointer unless it is NULL. +/// +/// \param ptr The pointer to wrap. If not NULL, the memory pointed to must +/// have been allocated with operator new[]. +template< class T > inline +auto_array< T >::auto_array(T* ptr) throw() : + _ptr(ptr) +{ +} + + +/// Constructs a copy of an auto_array. +/// +/// \param ptr The pointer to copy from. This pointer is invalidated and the +/// new copy grabs ownership of the object pointed to. +template< class T > inline +auto_array< T >::auto_array(auto_array< T >& ptr) throw() : + _ptr(ptr.release()) +{ +} + + +/// Constructs a new auto_array form a reference. +/// +/// Internal function used to construct a new auto_array from an object +/// returned, for example, from a function. +/// +/// \param ref The reference. +template< class T > inline +auto_array< T >::auto_array(detail::auto_array_ref< T > ref) throw() : + _ptr(ref._ptr) +{ +} + + +/// Destructor for auto_array objects. +template< class T > inline +auto_array< T >::~auto_array(void) throw() +{ + if (_ptr != NULL) + delete [] _ptr; +} + + +/// Gets the value of the wrapped pointer without releasing ownership. +/// +/// \return The raw mutable pointer. +template< class T > inline +T* +auto_array< T >::get(void) throw() +{ + return _ptr; +} + + +/// Gets the value of the wrapped pointer without releasing ownership. +/// +/// \return The raw immutable pointer. +template< class T > inline +const T* +auto_array< T >::get(void) const throw() +{ + return _ptr; +} + + +/// Gets the value of the wrapped pointer and releases ownership. +/// +/// \return The raw mutable pointer. +template< class T > inline +T* +auto_array< T >::release(void) throw() +{ + T* ptr = _ptr; + _ptr = NULL; + return ptr; +} + + +/// Changes the value of the wrapped pointer. +/// +/// If the auto_array was pointing to an array, such array is released and the +/// wrapped pointer is replaced with the new pointer provided. +/// +/// \param ptr The pointer to use as a replacement; may be NULL. +template< class T > inline +void +auto_array< T >::reset(T* ptr) throw() +{ + if (_ptr != NULL) + delete [] _ptr; + _ptr = ptr; +} + + +/// Assignment operator. +/// +/// \param ptr The object to copy from. This is invalidated after the copy. +/// \return A reference to the auto_array object itself. +template< class T > inline +auto_array< T >& +auto_array< T >::operator=(auto_array< T >& ptr) throw() +{ + reset(ptr.release()); + return *this; +} + + +/// Internal assignment operator for function returns. +/// +/// \param ref The reference object to copy from. +/// \return A reference to the auto_array object itself. +template< class T > inline +auto_array< T >& +auto_array< T >::operator=(detail::auto_array_ref< T > ref) throw() +{ + if (_ptr != ref._ptr) { + delete [] _ptr; + _ptr = ref._ptr; + } + return *this; +} + + +/// Subscript operator to access the array by position. +/// +/// This does not perform any bounds checking, in particular because auto_array +/// does not know the size of the arrays pointed to by it. +/// +/// \param pos The position to access, indexed from zero. +/// +/// \return A mutable reference to the element at the specified position. +template< class T > inline +T& +auto_array< T >::operator[](int pos) throw() +{ + return _ptr[pos]; +} + + +/// Subscript operator to access the array by position. +/// +/// This does not perform any bounds checking, in particular because auto_array +/// does not know the size of the arrays pointed to by it. +/// +/// \param pos The position to access, indexed from zero. +/// +/// \return An immutable reference to the element at the specified position. +template< class T > inline +const T& +auto_array< T >::operator[](int pos) const throw() +{ + return _ptr[pos]; +} + + +/// Internal conversion to a reference wrapper. +/// +/// This is used internally to support returning auto_array objects from +/// functions. The auto_array is invalidated when used. +/// +/// \return A new detail::auto_array_ref object holding the pointer. +template< class T > inline +auto_array< T >::operator detail::auto_array_ref< T >(void) throw() +{ + return detail::auto_array_ref< T >(release()); +} + + +} // namespace utils + + +#endif // !defined(UTILS_AUTO_ARRAY_IPP) diff --git a/utils/auto_array_fwd.hpp b/utils/auto_array_fwd.hpp new file mode 100644 index 000000000000..e1522a25bf7d --- /dev/null +++ b/utils/auto_array_fwd.hpp @@ -0,0 +1,43 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/auto_array_fwd.hpp +/// Forward declarations for utils/auto_array.hpp + +#if !defined(UTILS_AUTO_ARRAY_FWD_HPP) +#define UTILS_AUTO_ARRAY_FWD_HPP + +namespace utils { + + +template< class > class auto_array; + + +} // namespace utils + +#endif // !defined(UTILS_AUTO_ARRAY_FWD_HPP) diff --git a/utils/auto_array_test.cpp b/utils/auto_array_test.cpp new file mode 100644 index 000000000000..041eb65863ba --- /dev/null +++ b/utils/auto_array_test.cpp @@ -0,0 +1,312 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/auto_array.ipp" + +extern "C" { +#include +} + +#include + +#include + +#include "utils/defs.hpp" + +using utils::auto_array; + + +namespace { + + +/// Mock class to capture calls to the new and delete operators. +class test_array { +public: + /// User-settable cookie to disambiguate instances of this class. + int m_value; + + /// The current balance of existing test_array instances. + static ssize_t m_nblocks; + + /// Captures invalid calls to new on an array. + /// + /// \return Nothing; this always fails the test case. + void* + operator new(const size_t /* size */) + { + ATF_FAIL("New called but should have been new[]"); + return new int(5); + } + + /// Obtains memory for a new instance and increments m_nblocks. + /// + /// \param size The amount of memory to allocate, in bytes. + /// + /// \return A pointer to the allocated memory. + /// + /// \throw std::bad_alloc If the memory cannot be allocated. + void* + operator new[](const size_t size) + { + void* mem = ::operator new(size); + m_nblocks++; + std::cout << "Allocated 'test_array' object " << mem << "\n"; + return mem; + } + + /// Captures invalid calls to delete on an array. + /// + /// \return Nothing; this always fails the test case. + void + operator delete(void* /* mem */) + { + ATF_FAIL("Delete called but should have been delete[]"); + } + + /// Deletes a previously allocated array and decrements m_nblocks. + /// + /// \param mem The pointer to the memory to be deleted. + void + operator delete[](void* mem) + { + std::cout << "Releasing 'test_array' object " << mem << "\n"; + if (m_nblocks == 0) + ATF_FAIL("Unbalanced delete[]"); + m_nblocks--; + ::operator delete(mem); + } +}; + + +ssize_t test_array::m_nblocks = 0; + + +} // anonymous namespace + + +ATF_TEST_CASE(scope); +ATF_TEST_CASE_HEAD(scope) +{ + set_md_var("descr", "Tests the automatic scope handling in the " + "auto_array smart pointer class"); +} +ATF_TEST_CASE_BODY(scope) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(copy); +ATF_TEST_CASE_HEAD(copy) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' copy " + "constructor"); +} +ATF_TEST_CASE_BODY(copy) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t1(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + + { + auto_array< test_array > t2(t1); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(copy_ref); +ATF_TEST_CASE_HEAD(copy_ref) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' copy " + "constructor through the auxiliary ref object"); +} +ATF_TEST_CASE_BODY(copy_ref) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t1(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + + { + auto_array< test_array > t2 = t1; + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(get); +ATF_TEST_CASE_HEAD(get) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' get " + "method"); +} +ATF_TEST_CASE_BODY(get) +{ + test_array* ta = new test_array[10]; + auto_array< test_array > t(ta); + ATF_REQUIRE_EQ(t.get(), ta); +} + + +ATF_TEST_CASE(release); +ATF_TEST_CASE_HEAD(release) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' release " + "method"); +} +ATF_TEST_CASE_BODY(release) +{ + test_array* ta1 = new test_array[10]; + { + auto_array< test_array > t(ta1); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + test_array* ta2 = t.release(); + ATF_REQUIRE_EQ(ta2, ta1); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + delete [] ta1; +} + + +ATF_TEST_CASE(reset); +ATF_TEST_CASE_HEAD(reset) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' reset " + "method"); +} +ATF_TEST_CASE_BODY(reset) +{ + test_array* ta1 = new test_array[10]; + test_array* ta2 = new test_array[10]; + ATF_REQUIRE_EQ(test_array::m_nblocks, 2); + + { + auto_array< test_array > t(ta1); + ATF_REQUIRE_EQ(test_array::m_nblocks, 2); + t.reset(ta2); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + t.reset(); + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(assign); +ATF_TEST_CASE_HEAD(assign) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' " + "assignment operator"); +} +ATF_TEST_CASE_BODY(assign) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t1(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + + { + auto_array< test_array > t2; + t2 = t1; + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(assign_ref); +ATF_TEST_CASE_HEAD(assign_ref) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' " + "assignment operator through the auxiliary ref " + "object"); +} +ATF_TEST_CASE_BODY(assign_ref) +{ + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + { + auto_array< test_array > t1(new test_array[10]); + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + + { + auto_array< test_array > t2; + t2 = t1; + ATF_REQUIRE_EQ(test_array::m_nblocks, 1); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); + } + ATF_REQUIRE_EQ(test_array::m_nblocks, 0); +} + + +ATF_TEST_CASE(access); +ATF_TEST_CASE_HEAD(access) +{ + set_md_var("descr", "Tests the auto_array smart pointer class' access " + "operator"); +} +ATF_TEST_CASE_BODY(access) +{ + auto_array< test_array > t(new test_array[10]); + + for (int i = 0; i < 10; i++) + t[i].m_value = i * 2; + + for (int i = 0; i < 10; i++) + ATF_REQUIRE_EQ(t[i].m_value, i * 2); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, scope); + ATF_ADD_TEST_CASE(tcs, copy); + ATF_ADD_TEST_CASE(tcs, copy_ref); + ATF_ADD_TEST_CASE(tcs, get); + ATF_ADD_TEST_CASE(tcs, release); + ATF_ADD_TEST_CASE(tcs, reset); + ATF_ADD_TEST_CASE(tcs, assign); + ATF_ADD_TEST_CASE(tcs, assign_ref); + ATF_ADD_TEST_CASE(tcs, access); +} diff --git a/utils/cmdline/Kyuafile b/utils/cmdline/Kyuafile new file mode 100644 index 000000000000..d5e6f7122b07 --- /dev/null +++ b/utils/cmdline/Kyuafile @@ -0,0 +1,11 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="base_command_test"} +atf_test_program{name="commands_map_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="globals_test"} +atf_test_program{name="options_test"} +atf_test_program{name="parser_test"} +atf_test_program{name="ui_test"} diff --git a/utils/cmdline/Makefile.am.inc b/utils/cmdline/Makefile.am.inc new file mode 100644 index 000000000000..65081cbeafee --- /dev/null +++ b/utils/cmdline/Makefile.am.inc @@ -0,0 +1,96 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +libutils_a_SOURCES += utils/cmdline/base_command.cpp +libutils_a_SOURCES += utils/cmdline/base_command.hpp +libutils_a_SOURCES += utils/cmdline/base_command_fwd.hpp +libutils_a_SOURCES += utils/cmdline/base_command.ipp +libutils_a_SOURCES += utils/cmdline/commands_map.hpp +libutils_a_SOURCES += utils/cmdline/commands_map_fwd.hpp +libutils_a_SOURCES += utils/cmdline/commands_map.ipp +libutils_a_SOURCES += utils/cmdline/exceptions.cpp +libutils_a_SOURCES += utils/cmdline/exceptions.hpp +libutils_a_SOURCES += utils/cmdline/globals.cpp +libutils_a_SOURCES += utils/cmdline/globals.hpp +libutils_a_SOURCES += utils/cmdline/options.cpp +libutils_a_SOURCES += utils/cmdline/options.hpp +libutils_a_SOURCES += utils/cmdline/options_fwd.hpp +libutils_a_SOURCES += utils/cmdline/parser.cpp +libutils_a_SOURCES += utils/cmdline/parser.hpp +libutils_a_SOURCES += utils/cmdline/parser_fwd.hpp +libutils_a_SOURCES += utils/cmdline/parser.ipp +libutils_a_SOURCES += utils/cmdline/ui.cpp +libutils_a_SOURCES += utils/cmdline/ui.hpp +libutils_a_SOURCES += utils/cmdline/ui_fwd.hpp +# The following two files are only supposed to be used from test code. They +# should not be bundled into libutils.a, but doing so simplifies the build +# significantly. +libutils_a_SOURCES += utils/cmdline/ui_mock.hpp +libutils_a_SOURCES += utils/cmdline/ui_mock.cpp + +if WITH_ATF +tests_utils_cmdlinedir = $(pkgtestsdir)/utils/cmdline + +tests_utils_cmdline_DATA = utils/cmdline/Kyuafile +EXTRA_DIST += $(tests_utils_cmdline_DATA) + +tests_utils_cmdline_PROGRAMS = utils/cmdline/base_command_test +utils_cmdline_base_command_test_SOURCES = utils/cmdline/base_command_test.cpp +utils_cmdline_base_command_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_base_command_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/commands_map_test +utils_cmdline_commands_map_test_SOURCES = utils/cmdline/commands_map_test.cpp +utils_cmdline_commands_map_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_commands_map_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/exceptions_test +utils_cmdline_exceptions_test_SOURCES = utils/cmdline/exceptions_test.cpp +utils_cmdline_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/globals_test +utils_cmdline_globals_test_SOURCES = utils/cmdline/globals_test.cpp +utils_cmdline_globals_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_globals_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/options_test +utils_cmdline_options_test_SOURCES = utils/cmdline/options_test.cpp +utils_cmdline_options_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_options_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/parser_test +utils_cmdline_parser_test_SOURCES = utils/cmdline/parser_test.cpp +utils_cmdline_parser_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_parser_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_cmdline_PROGRAMS += utils/cmdline/ui_test +utils_cmdline_ui_test_SOURCES = utils/cmdline/ui_test.cpp +utils_cmdline_ui_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_cmdline_ui_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/cmdline/base_command.cpp b/utils/cmdline/base_command.cpp new file mode 100644 index 000000000000..837ded9cffab --- /dev/null +++ b/utils/cmdline/base_command.cpp @@ -0,0 +1,201 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/base_command.hpp" + +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + + +/// Creates a new command. +/// +/// \param name_ The name of the command. Must be unique within the context of +/// a program and have no spaces. +/// \param arg_list_ A textual description of the arguments received by the +/// command. May be empty. +/// \param min_args_ The minimum number of arguments required by the command. +/// \param max_args_ The maximum number of arguments required by the command. +/// -1 means infinity. +/// \param short_description_ A description of the purpose of the command. +cmdline::command_proto::command_proto(const std::string& name_, + const std::string& arg_list_, + const int min_args_, + const int max_args_, + const std::string& short_description_) : + _name(name_), + _arg_list(arg_list_), + _min_args(min_args_), + _max_args(max_args_), + _short_description(short_description_) +{ + PRE(name_.find(' ') == std::string::npos); + PRE(max_args_ == -1 || min_args_ <= max_args_); +} + + +/// Destructor for a command. +cmdline::command_proto::~command_proto(void) +{ + for (options_vector::const_iterator iter = _options.begin(); + iter != _options.end(); iter++) + delete *iter; +} + + +/// Internal method to register a dynamically-allocated option. +/// +/// Always use add_option() from subclasses to add options. +/// +/// \param option_ The option to add. Must have been dynamically allocated. +/// This grabs ownership of the pointer, which is released when the command +/// is destroyed. +void +cmdline::command_proto::add_option_ptr(const cmdline::base_option* option_) +{ + try { + _options.push_back(option_); + } catch (...) { + delete option_; + throw; + } +} + + +/// Processes the command line based on the command description. +/// +/// \param args The raw command line to be processed. +/// +/// \return An object containing the list of options and free arguments found in +/// args. +/// +/// \throw cmdline::usage_error If there is a problem processing the command +/// line. This error is caused by invalid input from the user. +cmdline::parsed_cmdline +cmdline::command_proto::parse_cmdline(const cmdline::args_vector& args) const +{ + PRE(name() == args[0]); + const parsed_cmdline cmdline = cmdline::parse(args, options()); + + const int argc = cmdline.arguments().size(); + if (argc < _min_args) + throw usage_error("Not enough arguments"); + if (_max_args != -1 && argc > _max_args) + throw usage_error("Too many arguments"); + + return cmdline; +} + + +/// Gets the name of the command. +/// +/// \return The command name. +const std::string& +cmdline::command_proto::name(void) const +{ + return _name; +} + + +/// Gets the textual representation of the arguments list. +/// +/// \return The description of the arguments list. +const std::string& +cmdline::command_proto::arg_list(void) const +{ + return _arg_list; +} + + +/// Gets the description of the purpose of the command. +/// +/// \return The description of the command. +const std::string& +cmdline::command_proto::short_description(void) const +{ + return _short_description; +} + + +/// Gets the definition of the options accepted by the command. +/// +/// \return The list of options. +const cmdline::options_vector& +cmdline::command_proto::options(void) const +{ + return _options; +} + + +/// Creates a new command. +/// +/// \param name_ The name of the command. Must be unique within the context of +/// a program and have no spaces. +/// \param arg_list_ A textual description of the arguments received by the +/// command. May be empty. +/// \param min_args_ The minimum number of arguments required by the command. +/// \param max_args_ The maximum number of arguments required by the command. +/// -1 means infinity. +/// \param short_description_ A description of the purpose of the command. +cmdline::base_command_no_data::base_command_no_data( + const std::string& name_, + const std::string& arg_list_, + const int min_args_, + const int max_args_, + const std::string& short_description_) : + command_proto(name_, arg_list_, min_args_, max_args_, short_description_) +{ +} + + +/// Entry point for the command. +/// +/// This delegates execution to the run() abstract function after the command +/// line provided in args has been parsed. +/// +/// If this function returns, the command is assumed to have been executed +/// successfully. Any error must be reported by means of exceptions. +/// +/// \param ui Object to interact with the I/O of the command. The command must +/// always use this object to write to stdout and stderr. +/// \param args The command line passed to the command broken by word, which +/// includes options and arguments. +/// +/// \return The exit code that the program has to return. 0 on success, some +/// other value on error. +/// \throw usage_error If args is invalid (i.e. if the options are mispecified +/// or if the arguments are invalid). +int +cmdline::base_command_no_data::main(cmdline::ui* ui, + const cmdline::args_vector& args) +{ + return run(ui, parse_cmdline(args)); +} diff --git a/utils/cmdline/base_command.hpp b/utils/cmdline/base_command.hpp new file mode 100644 index 000000000000..819dfe98dad3 --- /dev/null +++ b/utils/cmdline/base_command.hpp @@ -0,0 +1,162 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/base_command.hpp +/// Provides the utils::cmdline::base_command class. + +#if !defined(UTILS_CMDLINE_BASE_COMMAND_HPP) +#define UTILS_CMDLINE_BASE_COMMAND_HPP + +#include "utils/cmdline/base_command_fwd.hpp" + +#include + +#include "utils/cmdline/options_fwd.hpp" +#include "utils/cmdline/parser_fwd.hpp" +#include "utils/cmdline/ui_fwd.hpp" +#include "utils/noncopyable.hpp" + +namespace utils { +namespace cmdline { + + +/// Prototype class for the implementation of subcommands of a program. +/// +/// Use the subclasses of command_proto defined in this module instead of +/// command_proto itself as base classes for your application-specific +/// commands. +class command_proto : noncopyable { + /// The user-visible name of the command. + const std::string _name; + + /// Textual description of the command arguments. + const std::string _arg_list; + + /// The minimum number of required arguments. + const int _min_args; + + /// The maximum number of allowed arguments; -1 for infinity. + const int _max_args; + + /// A textual description of the command. + const std::string _short_description; + + /// Collection of command-specific options. + options_vector _options; + + void add_option_ptr(const base_option*); + +protected: + template< typename Option > void add_option(const Option&); + parsed_cmdline parse_cmdline(const args_vector&) const; + +public: + command_proto(const std::string&, const std::string&, const int, const int, + const std::string&); + virtual ~command_proto(void); + + const std::string& name(void) const; + const std::string& arg_list(void) const; + const std::string& short_description(void) const; + const options_vector& options(void) const; +}; + + +/// Unparametrized base subcommand for a program. +/// +/// Use this class to define subcommands for your program that do not need any +/// information passed in from the main command-line dispatcher other than the +/// command-line arguments. +class base_command_no_data : public command_proto { + /// Main code of the command. + /// + /// This is called from main() after the command line has been processed and + /// validated. + /// + /// \param ui Object to interact with the I/O of the command. The command + /// must always use this object to write to stdout and stderr. + /// \param cmdline The parsed command line, containing the values of any + /// given options and arguments. + /// + /// \return The exit code that the program has to return. 0 on success, + /// some other value on error. + /// + /// \throw std::runtime_error Any errors detected during the execution of + /// the command are reported by means of exceptions. + virtual int run(ui* ui, const parsed_cmdline& cmdline) = 0; + +public: + base_command_no_data(const std::string&, const std::string&, const int, + const int, const std::string&); + + int main(ui*, const args_vector&); +}; + + +/// Parametrized base subcommand for a program. +/// +/// Use this class to define subcommands for your program that need some kind of +/// runtime information passed in from the main command-line dispatcher. +/// +/// \param Data The type of the object passed to the subcommand at runtime. +/// This is useful, for example, to pass around the runtime configuration of the +/// program. +template< typename Data > +class base_command : public command_proto { + /// Main code of the command. + /// + /// This is called from main() after the command line has been processed and + /// validated. + /// + /// \param ui Object to interact with the I/O of the command. The command + /// must always use this object to write to stdout and stderr. + /// \param cmdline The parsed command line, containing the values of any + /// given options and arguments. + /// \param data An instance of the runtime data passed from main(). + /// + /// \return The exit code that the program has to return. 0 on success, + /// some other value on error. + /// + /// \throw std::runtime_error Any errors detected during the execution of + /// the command are reported by means of exceptions. + virtual int run(ui* ui, const parsed_cmdline& cmdline, + const Data& data) = 0; + +public: + base_command(const std::string&, const std::string&, const int, const int, + const std::string&); + + int main(ui*, const args_vector&, const Data&); +}; + + +} // namespace cmdline +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_BASE_COMMAND_HPP) diff --git a/utils/cmdline/base_command.ipp b/utils/cmdline/base_command.ipp new file mode 100644 index 000000000000..5696637085d7 --- /dev/null +++ b/utils/cmdline/base_command.ipp @@ -0,0 +1,104 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_CMDLINE_BASE_COMMAND_IPP) +#define UTILS_CMDLINE_BASE_COMMAND_IPP + +#include "utils/cmdline/base_command.hpp" + + +namespace utils { +namespace cmdline { + + +/// Adds an option to the command. +/// +/// This is to be called from the constructor of the subclass that implements +/// the command. +/// +/// \param option_ The option to add. +template< typename Option > +void +command_proto::add_option(const Option& option_) +{ + add_option_ptr(new Option(option_)); +} + + +/// Creates a new command. +/// +/// \param name_ The name of the command. Must be unique within the context of +/// a program and have no spaces. +/// \param arg_list_ A textual description of the arguments received by the +/// command. May be empty. +/// \param min_args_ The minimum number of arguments required by the command. +/// \param max_args_ The maximum number of arguments required by the command. +/// -1 means infinity. +/// \param short_description_ A description of the purpose of the command. +template< typename Data > +base_command< Data >::base_command(const std::string& name_, + const std::string& arg_list_, + const int min_args_, + const int max_args_, + const std::string& short_description_) : + command_proto(name_, arg_list_, min_args_, max_args_, short_description_) +{ +} + + +/// Entry point for the command. +/// +/// This delegates execution to the run() abstract function after the command +/// line provided in args has been parsed. +/// +/// If this function returns, the command is assumed to have been executed +/// successfully. Any error must be reported by means of exceptions. +/// +/// \param ui Object to interact with the I/O of the command. The command must +/// always use this object to write to stdout and stderr. +/// \param args The command line passed to the command broken by word, which +/// includes options and arguments. +/// \param data An opaque data structure to pass to the run method. +/// +/// \return The exit code that the program has to return. 0 on success, some +/// other value on error. +/// \throw usage_error If args is invalid (i.e. if the options are mispecified +/// or if the arguments are invalid). +template< typename Data > +int +base_command< Data >::main(ui* ui, const args_vector& args, const Data& data) +{ + return run(ui, parse_cmdline(args), data); +} + + +} // namespace cli +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_BASE_COMMAND_IPP) diff --git a/utils/cmdline/base_command_fwd.hpp b/utils/cmdline/base_command_fwd.hpp new file mode 100644 index 000000000000..c94db1ae2d05 --- /dev/null +++ b/utils/cmdline/base_command_fwd.hpp @@ -0,0 +1,47 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/base_command_fwd.hpp +/// Forward declarations for utils/cmdline/base_command.hpp + +#if !defined(UTILS_CMDLINE_BASE_COMMAND_FWD_HPP) +#define UTILS_CMDLINE_BASE_COMMAND_FWD_HPP + +namespace utils { +namespace cmdline { + + +class command_proto; +class base_command_no_data; +template< typename > class base_command; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_BASE_COMMAND_FWD_HPP) diff --git a/utils/cmdline/base_command_test.cpp b/utils/cmdline/base_command_test.cpp new file mode 100644 index 000000000000..20df8ea49512 --- /dev/null +++ b/utils/cmdline/base_command_test.cpp @@ -0,0 +1,295 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/base_command.ipp" + +#include + +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/defs.hpp" + +namespace cmdline = utils::cmdline; + + +namespace { + + +/// Mock command to test the cmdline::base_command base class. +/// +/// \param Data The type of the opaque data object passed to main(). +/// \param ExpectedData The value run() will expect to find in the Data object +/// passed to main(). +template< typename Data, Data ExpectedData > +class mock_cmd : public cmdline::base_command< Data > { +public: + /// Indicates if run() has been called already and executed correctly. + bool executed; + + /// Contains the argument of --the_string after run() is executed. + std::string optvalue; + + /// Constructs a new mock command. + mock_cmd(void) : + cmdline::base_command< Data >("mock", "arg1 [arg2 [arg3]]", 1, 3, + "Command for testing."), + executed(false) + { + this->add_option(cmdline::string_option("the_string", "Test option", + "arg")); + } + + /// Executes the command. + /// + /// \param cmdline Representation of the command line to the subcommand. + /// \param data Arbitrary data cookie passed to the command. + /// + /// \return A hardcoded number for testing purposes. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& cmdline, const Data& data) + { + if (cmdline.has_option("the_string")) + optvalue = cmdline.get_option< cmdline::string_option >( + "the_string"); + ATF_REQUIRE_EQ(ExpectedData, data); + executed = true; + return 1234; + } +}; + + +/// Mock command to test the cmdline::base_command_no_data base class. +class mock_cmd_no_data : public cmdline::base_command_no_data { +public: + /// Indicates if run() has been called already and executed correctly. + bool executed; + + /// Contains the argument of --the_string after run() is executed. + std::string optvalue; + + /// Constructs a new mock command. + mock_cmd_no_data(void) : + cmdline::base_command_no_data("mock", "arg1 [arg2 [arg3]]", 1, 3, + "Command for testing."), + executed(false) + { + add_option(cmdline::string_option("the_string", "Test option", "arg")); + } + + /// Executes the command. + /// + /// \param cmdline Representation of the command line to the subcommand. + /// + /// \return A hardcoded number for testing purposes. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& cmdline) + { + if (cmdline.has_option("the_string")) + optvalue = cmdline.get_option< cmdline::string_option >( + "the_string"); + executed = true; + return 1234; + } +}; + + +/// Implementation of a command to get access to parse_cmdline(). +class parse_cmdline_portal : public cmdline::command_proto { +public: + /// Constructs a new mock command. + parse_cmdline_portal(void) : + cmdline::command_proto("portal", "arg1 [arg2 [arg3]]", 1, 3, + "Command for testing.") + { + this->add_option(cmdline::string_option("the_string", "Test option", + "arg")); + } + + /// Delegator for the internal parse_cmdline() method. + /// + /// \param args The input arguments to be parsed. + /// + /// \return The parsed command line, split in options and arguments. + cmdline::parsed_cmdline + operator()(const cmdline::args_vector& args) const + { + return parse_cmdline(args); + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(command_proto__parse_cmdline__ok); +ATF_TEST_CASE_BODY(command_proto__parse_cmdline__ok) +{ + cmdline::args_vector args; + args.push_back("portal"); + args.push_back("--the_string=foo bar"); + args.push_back("one arg"); + args.push_back("another arg"); + (void)parse_cmdline_portal()(args); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(command_proto__parse_cmdline__parse_fail); +ATF_TEST_CASE_BODY(command_proto__parse_cmdline__parse_fail) +{ + cmdline::args_vector args; + args.push_back("portal"); + args.push_back("--foo-bar"); + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Unknown.*foo-bar", + (void)parse_cmdline_portal()(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(command_proto__parse_cmdline__args_invalid); +ATF_TEST_CASE_BODY(command_proto__parse_cmdline__args_invalid) +{ + cmdline::args_vector args; + args.push_back("portal"); + + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Not enough arguments", + (void)parse_cmdline_portal()(args)); + + args.push_back("1"); + args.push_back("2"); + args.push_back("3"); + args.push_back("4"); + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Too many arguments", + (void)parse_cmdline_portal()(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command__getters); +ATF_TEST_CASE_BODY(base_command__getters) +{ + mock_cmd< int, 584 > cmd; + ATF_REQUIRE_EQ("mock", cmd.name()); + ATF_REQUIRE_EQ("arg1 [arg2 [arg3]]", cmd.arg_list()); + ATF_REQUIRE_EQ("Command for testing.", cmd.short_description()); + ATF_REQUIRE_EQ(1, cmd.options().size()); + ATF_REQUIRE_EQ("the_string", cmd.options()[0]->long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command__main__ok) +ATF_TEST_CASE_BODY(base_command__main__ok) +{ + mock_cmd< int, 584 > cmd; + + cmdline::ui_mock ui; + cmdline::args_vector args; + args.push_back("mock"); + args.push_back("--the_string=foo bar"); + args.push_back("one arg"); + args.push_back("another arg"); + ATF_REQUIRE_EQ(1234, cmd.main(&ui, args, 584)); + ATF_REQUIRE(cmd.executed); + ATF_REQUIRE_EQ("foo bar", cmd.optvalue); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command__main__parse_cmdline_fail) +ATF_TEST_CASE_BODY(base_command__main__parse_cmdline_fail) +{ + mock_cmd< int, 584 > cmd; + + cmdline::ui_mock ui; + cmdline::args_vector args; + args.push_back("mock"); + args.push_back("--foo-bar"); + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Unknown.*foo-bar", + cmd.main(&ui, args, 584)); + ATF_REQUIRE(!cmd.executed); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command_no_data__getters); +ATF_TEST_CASE_BODY(base_command_no_data__getters) +{ + mock_cmd_no_data cmd; + ATF_REQUIRE_EQ("mock", cmd.name()); + ATF_REQUIRE_EQ("arg1 [arg2 [arg3]]", cmd.arg_list()); + ATF_REQUIRE_EQ("Command for testing.", cmd.short_description()); + ATF_REQUIRE_EQ(1, cmd.options().size()); + ATF_REQUIRE_EQ("the_string", cmd.options()[0]->long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command_no_data__main__ok) +ATF_TEST_CASE_BODY(base_command_no_data__main__ok) +{ + mock_cmd_no_data cmd; + + cmdline::ui_mock ui; + cmdline::args_vector args; + args.push_back("mock"); + args.push_back("--the_string=foo bar"); + args.push_back("one arg"); + args.push_back("another arg"); + ATF_REQUIRE_EQ(1234, cmd.main(&ui, args)); + ATF_REQUIRE(cmd.executed); + ATF_REQUIRE_EQ("foo bar", cmd.optvalue); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_command_no_data__main__parse_cmdline_fail) +ATF_TEST_CASE_BODY(base_command_no_data__main__parse_cmdline_fail) +{ + mock_cmd_no_data cmd; + + cmdline::ui_mock ui; + cmdline::args_vector args; + args.push_back("mock"); + args.push_back("--foo-bar"); + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Unknown.*foo-bar", + cmd.main(&ui, args)); + ATF_REQUIRE(!cmd.executed); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, command_proto__parse_cmdline__ok); + ATF_ADD_TEST_CASE(tcs, command_proto__parse_cmdline__parse_fail); + ATF_ADD_TEST_CASE(tcs, command_proto__parse_cmdline__args_invalid); + + ATF_ADD_TEST_CASE(tcs, base_command__getters); + ATF_ADD_TEST_CASE(tcs, base_command__main__ok); + ATF_ADD_TEST_CASE(tcs, base_command__main__parse_cmdline_fail); + + ATF_ADD_TEST_CASE(tcs, base_command_no_data__getters); + ATF_ADD_TEST_CASE(tcs, base_command_no_data__main__ok); + ATF_ADD_TEST_CASE(tcs, base_command_no_data__main__parse_cmdline_fail); +} diff --git a/utils/cmdline/commands_map.hpp b/utils/cmdline/commands_map.hpp new file mode 100644 index 000000000000..5378a6f2c471 --- /dev/null +++ b/utils/cmdline/commands_map.hpp @@ -0,0 +1,96 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/commands_map.hpp +/// Maintains a collection of dynamically-instantiated commands. +/// +/// Commands need to be dynamically-instantiated because they are often +/// complex data structures. Instantiating them as static variables causes +/// problems with the order of construction of globals. The commands_map class +/// provided by this module provides a mechanism to maintain these instantiated +/// objects. + +#if !defined(UTILS_CMDLINE_COMMANDS_MAP_HPP) +#define UTILS_CMDLINE_COMMANDS_MAP_HPP + +#include "utils/cmdline/commands_map_fwd.hpp" + +#include +#include +#include +#include + +#include "utils/noncopyable.hpp" + + +namespace utils { +namespace cmdline { + + +/// Collection of dynamically-instantiated commands. +template< typename BaseCommand > +class commands_map : noncopyable { + /// Map of command names to their implementations. + typedef std::map< std::string, BaseCommand* > impl_map; + + /// Map of category names to the command names they contain. + typedef std::map< std::string, std::set< std::string > > categories_map; + + /// Collection of all available commands. + impl_map _commands; + + /// Collection of defined categories and their commands. + categories_map _categories; + +public: + commands_map(void); + ~commands_map(void); + + /// Scoped, strictly-owned pointer to a command from this map. + typedef typename std::auto_ptr< BaseCommand > command_ptr; + void insert(command_ptr, const std::string& = ""); + void insert(BaseCommand*, const std::string& = ""); + + /// Type for a constant iterator. + typedef typename categories_map::const_iterator const_iterator; + + bool empty(void) const; + + const_iterator begin(void) const; + const_iterator end(void) const; + + BaseCommand* find(const std::string&); + const BaseCommand* find(const std::string&) const; +}; + + +} // namespace cmdline +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_BASE_COMMAND_HPP) diff --git a/utils/cmdline/commands_map.ipp b/utils/cmdline/commands_map.ipp new file mode 100644 index 000000000000..8be87ab3b5cc --- /dev/null +++ b/utils/cmdline/commands_map.ipp @@ -0,0 +1,161 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/commands_map.hpp" +#include "utils/sanity.hpp" + + +namespace utils { + + +/// Constructs an empty set of commands. +template< typename BaseCommand > +cmdline::commands_map< BaseCommand >::commands_map(void) +{ +} + + +/// Destroys a set of commands. +/// +/// This releases the dynamically-instantiated objects. +template< typename BaseCommand > +cmdline::commands_map< BaseCommand >::~commands_map(void) +{ + for (typename impl_map::iterator iter = _commands.begin(); + iter != _commands.end(); iter++) + delete (*iter).second; +} + + +/// Inserts a new command into the map. +/// +/// \param command The command to insert. This must have been dynamically +/// allocated with new. The call grabs ownership of the command, or the +/// command is freed if the call fails. +/// \param category The category this command belongs to. Defaults to the empty +/// string, which indicates that the command has not be categorized. +template< typename BaseCommand > +void +cmdline::commands_map< BaseCommand >::insert(command_ptr command, + const std::string& category) +{ + INV(_commands.find(command->name()) == _commands.end()); + BaseCommand* ptr = command.release(); + INV(ptr != NULL); + _commands[ptr->name()] = ptr; + _categories[category].insert(ptr->name()); +} + + +/// Inserts a new command into the map. +/// +/// This grabs ownership of the pointer, so it is ONLY safe to use with the +/// following idiom: insert(new foo()). +/// +/// \param command The command to insert. This must have been dynamically +/// allocated with new. The call grabs ownership of the command, or the +/// command is freed if the call fails. +/// \param category The category this command belongs to. Defaults to the empty +/// string, which indicates that the command has not be categorized. +template< typename BaseCommand > +void +cmdline::commands_map< BaseCommand >::insert(BaseCommand* command, + const std::string& category) +{ + insert(command_ptr(command), category); +} + + +/// Checks whether the list of commands is empty. +/// +/// \return True if there are no commands in this map. +template< typename BaseCommand > +bool +cmdline::commands_map< BaseCommand >::empty(void) const +{ + return _commands.empty(); +} + + +/// Returns a constant iterator to the beginning of the categories mapping. +/// +/// \return A map (string -> BaseCommand*) iterator. +template< typename BaseCommand > +typename cmdline::commands_map< BaseCommand >::const_iterator +cmdline::commands_map< BaseCommand >::begin(void) const +{ + return _categories.begin(); +} + + +/// Returns a constant iterator to the end of the categories mapping. +/// +/// \return A map (string -> BaseCommand*) iterator. +template< typename BaseCommand > +typename cmdline::commands_map< BaseCommand >::const_iterator +cmdline::commands_map< BaseCommand >::end(void) const +{ + return _categories.end(); +} + + +/// Finds a command by name; mutable version. +/// +/// \param name The name of the command to locate. +/// +/// \return The command itself or NULL if it does not exist. +template< typename BaseCommand > +BaseCommand* +cmdline::commands_map< BaseCommand >::find(const std::string& name) +{ + typename impl_map::iterator iter = _commands.find(name); + if (iter == _commands.end()) + return NULL; + else + return (*iter).second; +} + + +/// Finds a command by name; constant version. +/// +/// \param name The name of the command to locate. +/// +/// \return The command itself or NULL if it does not exist. +template< typename BaseCommand > +const BaseCommand* +cmdline::commands_map< BaseCommand >::find(const std::string& name) const +{ + typename impl_map::const_iterator iter = _commands.find(name); + if (iter == _commands.end()) + return NULL; + else + return (*iter).second; +} + + +} // namespace utils diff --git a/utils/cmdline/commands_map_fwd.hpp b/utils/cmdline/commands_map_fwd.hpp new file mode 100644 index 000000000000..a81a852790da --- /dev/null +++ b/utils/cmdline/commands_map_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/commands_map_fwd.hpp +/// Forward declarations for utils/cmdline/commands_map.hpp + +#if !defined(UTILS_CMDLINE_COMMANDS_MAP_FWD_HPP) +#define UTILS_CMDLINE_COMMANDS_MAP_FWD_HPP + +namespace utils { +namespace cmdline { + + +template< typename > class commands_map; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_COMMANDS_MAP_FWD_HPP) diff --git a/utils/cmdline/commands_map_test.cpp b/utils/cmdline/commands_map_test.cpp new file mode 100644 index 000000000000..47a7404f64fb --- /dev/null +++ b/utils/cmdline/commands_map_test.cpp @@ -0,0 +1,140 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/commands_map.ipp" + +#include + +#include "utils/cmdline/base_command.hpp" +#include "utils/defs.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + + +namespace { + + +/// Fake command to validate the behavior of commands_map. +/// +/// Note that this command does not do anything. It is only intended to provide +/// a specific class that can be inserted into commands_map instances and check +/// that it can be located properly. +class mock_cmd : public cmdline::base_command_no_data { +public: + /// Constructor for the mock command. + /// + /// \param mock_name The name of the command. All other settings are set to + /// irrelevant values. + mock_cmd(const char* mock_name) : + cmdline::base_command_no_data(mock_name, "", 0, 0, + "Command for testing.") + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function is never called. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */) + { + UNREACHABLE; + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(empty); +ATF_TEST_CASE_BODY(empty) +{ + cmdline::commands_map< cmdline::base_command_no_data > commands; + ATF_REQUIRE(commands.empty()); + ATF_REQUIRE(commands.begin() == commands.end()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some); +ATF_TEST_CASE_BODY(some) +{ + cmdline::commands_map< cmdline::base_command_no_data > commands; + cmdline::base_command_no_data* cmd1 = new mock_cmd("cmd1"); + commands.insert(cmd1); + cmdline::base_command_no_data* cmd2 = new mock_cmd("cmd2"); + commands.insert(cmd2, "foo"); + + ATF_REQUIRE(!commands.empty()); + + cmdline::commands_map< cmdline::base_command_no_data >::const_iterator + iter = commands.begin(); + ATF_REQUIRE_EQ("", (*iter).first); + ATF_REQUIRE_EQ(1, (*iter).second.size()); + ATF_REQUIRE_EQ("cmd1", *(*iter).second.begin()); + + ++iter; + ATF_REQUIRE_EQ("foo", (*iter).first); + ATF_REQUIRE_EQ(1, (*iter).second.size()); + ATF_REQUIRE_EQ("cmd2", *(*iter).second.begin()); + + ATF_REQUIRE(++iter == commands.end()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find__match); +ATF_TEST_CASE_BODY(find__match) +{ + cmdline::commands_map< cmdline::base_command_no_data > commands; + cmdline::base_command_no_data* cmd1 = new mock_cmd("cmd1"); + commands.insert(cmd1); + cmdline::base_command_no_data* cmd2 = new mock_cmd("cmd2"); + commands.insert(cmd2); + + ATF_REQUIRE(cmd1 == commands.find("cmd1")); + ATF_REQUIRE(cmd2 == commands.find("cmd2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find__nomatch); +ATF_TEST_CASE_BODY(find__nomatch) +{ + cmdline::commands_map< cmdline::base_command_no_data > commands; + commands.insert(new mock_cmd("cmd1")); + + ATF_REQUIRE(NULL == commands.find("cmd2")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, empty); + ATF_ADD_TEST_CASE(tcs, some); + ATF_ADD_TEST_CASE(tcs, find__match); + ATF_ADD_TEST_CASE(tcs, find__nomatch); +} diff --git a/utils/cmdline/exceptions.cpp b/utils/cmdline/exceptions.cpp new file mode 100644 index 000000000000..fa9ba2218a7f --- /dev/null +++ b/utils/cmdline/exceptions.cpp @@ -0,0 +1,175 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/exceptions.hpp" + +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + + +#define VALIDATE_OPTION_NAME(option) PRE_MSG( \ + (option.length() == 2 && (option[0] == '-' && option[1] != '-')) || \ + (option.length() > 2 && (option[0] == '-' && option[1] == '-')), \ + F("The option name %s must be fully specified") % option); + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +cmdline::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +cmdline::error::~error(void) throw() +{ +} + + +/// Constructs a new usage_error. +/// +/// \param message The reason behind the usage error. +cmdline::usage_error::usage_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +cmdline::usage_error::~usage_error(void) throw() +{ +} + + +/// Constructs a new missing_option_argument_error. +/// +/// \param option_ The option for which no argument was provided. The option +/// name must be fully specified (with - or -- in front). +cmdline::missing_option_argument_error::missing_option_argument_error( + const std::string& option_) : + usage_error(F("Missing required argument for option %s") % option_), + _option(option_) +{ + VALIDATE_OPTION_NAME(option_); +} + + +/// Destructor for the error. +cmdline::missing_option_argument_error::~missing_option_argument_error(void) + throw() +{ +} + + +/// Returns the option name for which no argument was provided. +/// +/// \return The option name. +const std::string& +cmdline::missing_option_argument_error::option(void) const +{ + return _option; +} + + +/// Constructs a new option_argument_value_error. +/// +/// \param option_ The option to which an invalid argument was passed. The +/// option name must be fully specified (with - or -- in front). +/// \param argument_ The invalid argument. +/// \param reason_ The reason describing why the argument is invalid. +cmdline::option_argument_value_error::option_argument_value_error( + const std::string& option_, const std::string& argument_, + const std::string& reason_) : + usage_error(F("Invalid argument '%s' for option %s: %s") % argument_ % + option_ % reason_), + _option(option_), + _argument(argument_), + _reason(reason_) +{ + VALIDATE_OPTION_NAME(option_); +} + + +/// Destructor for the error. +cmdline::option_argument_value_error::~option_argument_value_error(void) + throw() +{ +} + + +/// Returns the option to which the invalid argument was passed. +/// +/// \return The option name. +const std::string& +cmdline::option_argument_value_error::option(void) const +{ + return _option; +} + + +/// Returns the invalid argument value. +/// +/// \return The invalid argument. +const std::string& +cmdline::option_argument_value_error::argument(void) const +{ + return _argument; +} + + +/// Constructs a new unknown_option_error. +/// +/// \param option_ The unknown option. The option name must be fully specified +/// (with - or -- in front). +cmdline::unknown_option_error::unknown_option_error( + const std::string& option_) : + usage_error(F("Unknown option %s") % option_), + _option(option_) +{ + VALIDATE_OPTION_NAME(option_); +} + + +/// Destructor for the error. +cmdline::unknown_option_error::~unknown_option_error(void) throw() +{ +} + + +/// Returns the unknown option name. +/// +/// \return The unknown option. +const std::string& +cmdline::unknown_option_error::option(void) const +{ + return _option; +} diff --git a/utils/cmdline/exceptions.hpp b/utils/cmdline/exceptions.hpp new file mode 100644 index 000000000000..59f99e835ce1 --- /dev/null +++ b/utils/cmdline/exceptions.hpp @@ -0,0 +1,109 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/exceptions.hpp +/// Exception types raised by the cmdline module. + +#if !defined(UTILS_CMDLINE_EXCEPTIONS_HPP) +#define UTILS_CMDLINE_EXCEPTIONS_HPP + +#include +#include + +namespace utils { +namespace cmdline { + + +/// Base exception for cmdline errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Generic error to describe problems caused by the user. +class usage_error : public error { +public: + explicit usage_error(const std::string&); + ~usage_error(void) throw(); +}; + + +/// Error denoting that no argument was provided to an option that required one. +class missing_option_argument_error : public usage_error { + /// Name of the option for which no required argument was specified. + std::string _option; + +public: + explicit missing_option_argument_error(const std::string&); + ~missing_option_argument_error(void) throw(); + + const std::string& option(void) const; +}; + + +/// Error denoting that the argument provided to an option is invalid. +class option_argument_value_error : public usage_error { + /// Name of the option for which the argument was invalid. + std::string _option; + + /// Raw value of the invalid user-provided argument. + std::string _argument; + + /// Reason describing why the argument is invalid. + std::string _reason; + +public: + explicit option_argument_value_error(const std::string&, const std::string&, + const std::string&); + ~option_argument_value_error(void) throw(); + + const std::string& option(void) const; + const std::string& argument(void) const; +}; + + +/// Error denoting that the user specified an unknown option. +class unknown_option_error : public usage_error { + /// Name of the option that was not known. + std::string _option; + +public: + explicit unknown_option_error(const std::string&); + ~unknown_option_error(void) throw(); + + const std::string& option(void) const; +}; + + +} // namespace cmdline +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_EXCEPTIONS_HPP) diff --git a/utils/cmdline/exceptions_test.cpp b/utils/cmdline/exceptions_test.cpp new file mode 100644 index 000000000000..b541e08f6995 --- /dev/null +++ b/utils/cmdline/exceptions_test.cpp @@ -0,0 +1,83 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/exceptions.hpp" + +#include + +#include + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const cmdline::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_option_argument_error); +ATF_TEST_CASE_BODY(missing_option_argument_error) +{ + const cmdline::missing_option_argument_error e("-o"); + ATF_REQUIRE(std::strcmp("Missing required argument for option -o", + e.what()) == 0); + ATF_REQUIRE_EQ("-o", e.option()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(option_argument_value_error); +ATF_TEST_CASE_BODY(option_argument_value_error) +{ + const cmdline::option_argument_value_error e("--the_option", "the value", + "the reason"); + ATF_REQUIRE(std::strcmp("Invalid argument 'the value' for option " + "--the_option: the reason", e.what()) == 0); + ATF_REQUIRE_EQ("--the_option", e.option()); + ATF_REQUIRE_EQ("the value", e.argument()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_option_error); +ATF_TEST_CASE_BODY(unknown_option_error) +{ + const cmdline::unknown_option_error e("--foo"); + ATF_REQUIRE(std::strcmp("Unknown option --foo", e.what()) == 0); + ATF_REQUIRE_EQ("--foo", e.option()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, missing_option_argument_error); + ATF_ADD_TEST_CASE(tcs, option_argument_value_error); + ATF_ADD_TEST_CASE(tcs, unknown_option_error); +} diff --git a/utils/cmdline/globals.cpp b/utils/cmdline/globals.cpp new file mode 100644 index 000000000000..76e0231fa36b --- /dev/null +++ b/utils/cmdline/globals.cpp @@ -0,0 +1,78 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/globals.hpp" + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + +namespace { + + +/// The name of the binary used to execute the program. +static std::string Progname; + + +} // anonymous namespace + + +/// Initializes the global state of the CLI. +/// +/// This function can only be called once during the execution of a program, +/// unless override_for_testing is set to true. +/// +/// \param argv0 The value of argv[0]; i.e. the program name. +/// \param override_for_testing Should always be set to false unless for tests +/// of this functionality, which may set this to true to redefine internal +/// state. +void +cmdline::init(const char* argv0, const bool override_for_testing) +{ + if (!override_for_testing) + PRE_MSG(Progname.empty(), "cmdline::init called more than once"); + Progname = utils::fs::path(argv0).leaf_name(); + LD(F("Program name: %s") % Progname); + POST(!Progname.empty()); +} + + +/// Gets the program name. +/// +/// \pre init() must have been called in advance. +/// +/// \return The program name. +const std::string& +cmdline::progname(void) +{ + PRE_MSG(!Progname.empty(), "cmdline::init not called yet"); + return Progname; +} diff --git a/utils/cmdline/globals.hpp b/utils/cmdline/globals.hpp new file mode 100644 index 000000000000..ab7904d69520 --- /dev/null +++ b/utils/cmdline/globals.hpp @@ -0,0 +1,48 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/globals.hpp +/// Representation of global, immutable state for a CLI. + +#if !defined(UTILS_CMDLINE_GLOBALS_HPP) +#define UTILS_CMDLINE_GLOBALS_HPP + +#include + +namespace utils { +namespace cmdline { + + +void init(const char*, const bool = false); +const std::string& progname(void); + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_GLOBALS_HPP) diff --git a/utils/cmdline/globals_test.cpp b/utils/cmdline/globals_test.cpp new file mode 100644 index 000000000000..5c2ac7cc2d6c --- /dev/null +++ b/utils/cmdline/globals_test.cpp @@ -0,0 +1,77 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/globals.hpp" + +#include + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__absolute); +ATF_TEST_CASE_BODY(progname__absolute) +{ + cmdline::init("/path/to/foobar"); + ATF_REQUIRE_EQ("foobar", cmdline::progname()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__relative); +ATF_TEST_CASE_BODY(progname__relative) +{ + cmdline::init("to/barbaz"); + ATF_REQUIRE_EQ("barbaz", cmdline::progname()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__plain); +ATF_TEST_CASE_BODY(progname__plain) +{ + cmdline::init("program"); + ATF_REQUIRE_EQ("program", cmdline::progname()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__override_for_testing); +ATF_TEST_CASE_BODY(progname__override_for_testing) +{ + cmdline::init("program"); + ATF_REQUIRE_EQ("program", cmdline::progname()); + + cmdline::init("foo", true); + ATF_REQUIRE_EQ("foo", cmdline::progname()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, progname__absolute); + ATF_ADD_TEST_CASE(tcs, progname__relative); + ATF_ADD_TEST_CASE(tcs, progname__plain); + ATF_ADD_TEST_CASE(tcs, progname__override_for_testing); +} diff --git a/utils/cmdline/options.cpp b/utils/cmdline/options.cpp new file mode 100644 index 000000000000..61736e31c11e --- /dev/null +++ b/utils/cmdline/options.cpp @@ -0,0 +1,605 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/options.hpp" + +#include +#include + +#include "utils/cmdline/exceptions.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace cmdline = utils::cmdline; +namespace text = utils::text; + + +/// Constructs a generic option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ If not NULL, specifies that the option must receive an +/// argument and specifies the name of such argument for documentation +/// purposes. +/// \param default_value_ If not NULL, specifies that the option has a default +/// value for the mandatory argument. +cmdline::base_option::base_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + _short_name(short_name_), + _long_name(long_name_), + _description(description_), + _arg_name(arg_name_ == NULL ? "" : arg_name_), + _has_default_value(default_value_ != NULL), + _default_value(default_value_ == NULL ? "" : default_value_) +{ + INV(short_name_ != '\0'); +} + + +/// Constructs a generic option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ If not NULL, specifies that the option must receive an +/// argument and specifies the name of such argument for documentation +/// purposes. +/// \param default_value_ If not NULL, specifies that the option has a default +/// value for the mandatory argument. +cmdline::base_option::base_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + _short_name('\0'), + _long_name(long_name_), + _description(description_), + _arg_name(arg_name_ == NULL ? "" : arg_name_), + _has_default_value(default_value_ != NULL), + _default_value(default_value_ == NULL ? "" : default_value_) +{ +} + + +/// Destructor for the option. +cmdline::base_option::~base_option(void) +{ +} + + +/// Checks whether the option has a short name or not. +/// +/// \return True if the option has a short name, false otherwise. +bool +cmdline::base_option::has_short_name(void) const +{ + return _short_name != '\0'; +} + + +/// Returns the short name of the option. +/// +/// \pre has_short_name() must be true. +/// +/// \return The short name. +char +cmdline::base_option::short_name(void) const +{ + PRE(has_short_name()); + return _short_name; +} + + +/// Returns the long name of the option. +/// +/// \return The long name. +const std::string& +cmdline::base_option::long_name(void) const +{ + return _long_name; +} + + +/// Returns the description of the option. +/// +/// \return The description. +const std::string& +cmdline::base_option::description(void) const +{ + return _description; +} + + +/// Checks whether the option needs an argument or not. +/// +/// \return True if the option needs an argument, false otherwise. +bool +cmdline::base_option::needs_arg(void) const +{ + return !_arg_name.empty(); +} + + +/// Returns the argument name of the option for documentation purposes. +/// +/// \pre needs_arg() must be true. +/// +/// \return The argument name. +const std::string& +cmdline::base_option::arg_name(void) const +{ + INV(needs_arg()); + return _arg_name; +} + + +/// Checks whether the option has a default value for its argument. +/// +/// \pre needs_arg() must be true. +/// +/// \return True if the option has a default value, false otherwise. +bool +cmdline::base_option::has_default_value(void) const +{ + PRE(needs_arg()); + return _has_default_value; +} + + +/// Returns the default value for the argument to the option. +/// +/// \pre has_default_value() must be true. +/// +/// \return The default value. +const std::string& +cmdline::base_option::default_value(void) const +{ + INV(has_default_value()); + return _default_value;; +} + + +/// Formats the short name of the option for documentation purposes. +/// +/// \return A string describing the option's short name. +std::string +cmdline::base_option::format_short_name(void) const +{ + PRE(has_short_name()); + + if (needs_arg()) { + return F("-%s %s") % short_name() % arg_name(); + } else { + return F("-%s") % short_name(); + } +} + + +/// Formats the long name of the option for documentation purposes. +/// +/// \return A string describing the option's long name. +std::string +cmdline::base_option::format_long_name(void) const +{ + if (needs_arg()) { + return F("--%s=%s") % long_name() % arg_name(); + } else { + return F("--%s") % long_name(); + } +} + + + +/// Ensures that an argument passed to the option is valid. +/// +/// This must be reimplemented by subclasses that describe options with +/// arguments. +/// +/// \throw cmdline::option_argument_value_error Subclasses must raise this +/// exception to indicate the cases in which str is invalid. +void +cmdline::base_option::validate(const std::string& /* str */) const +{ + UNREACHABLE_MSG("Option does not support an argument"); +} + + +/// Constructs a boolean option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +cmdline::bool_option::bool_option(const char short_name_, + const char* long_name_, + const char* description_) : + base_option(short_name_, long_name_, description_) +{ +} + + +/// Constructs a boolean option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +cmdline::bool_option::bool_option(const char* long_name_, + const char* description_) : + base_option(long_name_, description_) +{ +} + + +/// Constructs an integer option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::int_option::int_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) +{ +} + + +/// Constructs an integer option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::int_option::int_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(long_name_, description_, arg_name_, default_value_) +{ +} + + +/// Ensures that an integer argument passed to the int_option is valid. +/// +/// \param raw_value The argument representing an integer as provided by the +/// user. +/// +/// \throw cmdline::option_argument_value_error If the integer provided in +/// raw_value is invalid. +void +cmdline::int_option::validate(const std::string& raw_value) const +{ + try { + (void)text::to_type< int >(raw_value); + } catch (const std::runtime_error& e) { + throw cmdline::option_argument_value_error( + F("--%s") % long_name(), raw_value, "Not a valid integer"); + } +} + + +/// Converts an integer argument to a native integer. +/// +/// \param raw_value The argument representing an integer as provided by the +/// user. +/// +/// \return The integer. +/// +/// \pre validate(raw_value) must be true. +int +cmdline::int_option::convert(const std::string& raw_value) +{ + try { + return text::to_type< int >(raw_value); + } catch (const std::runtime_error& e) { + PRE_MSG(false, F("Raw value '%s' for int option not properly " + "validated: %s") % raw_value % e.what()); + } +} + + +/// Constructs a list option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::list_option::list_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) +{ +} + + +/// Constructs a list option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::list_option::list_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(long_name_, description_, arg_name_, default_value_) +{ +} + + +/// Ensures that a lisstring argument passed to the list_option is valid. +void +cmdline::list_option::validate( + const std::string& /* raw_value */) const +{ + // Any list is potentially valid; the caller must check for semantics. +} + + +/// Converts a string argument to a vector. +/// +/// \param raw_value The argument representing a list as provided by the user. +/// +/// \return The list. +/// +/// \pre validate(raw_value) must be true. +cmdline::list_option::option_type +cmdline::list_option::convert(const std::string& raw_value) +{ + try { + return text::split(raw_value, ','); + } catch (const std::runtime_error& e) { + PRE_MSG(false, F("Raw value '%s' for list option not properly " + "validated: %s") % raw_value % e.what()); + } +} + + +/// Constructs a path option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::path_option::path_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) +{ +} + + +/// Constructs a path option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::path_option::path_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(long_name_, description_, arg_name_, default_value_) +{ +} + + +/// Ensures that a path argument passed to the path_option is valid. +/// +/// \param raw_value The argument representing a path as provided by the user. +/// +/// \throw cmdline::option_argument_value_error If the path provided in +/// raw_value is invalid. +void +cmdline::path_option::validate(const std::string& raw_value) const +{ + try { + (void)utils::fs::path(raw_value); + } catch (const utils::fs::error& e) { + throw cmdline::option_argument_value_error(F("--%s") % long_name(), + raw_value, e.what()); + } +} + + +/// Converts a path argument to a utils::fs::path. +/// +/// \param raw_value The argument representing a path as provided by the user. +/// +/// \return The path. +/// +/// \pre validate(raw_value) must be true. +utils::fs::path +cmdline::path_option::convert(const std::string& raw_value) +{ + try { + return utils::fs::path(raw_value); + } catch (const std::runtime_error& e) { + PRE_MSG(false, F("Raw value '%s' for path option not properly " + "validated: %s") % raw_value % e.what()); + } +} + + +/// Constructs a property option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. Must include the '=' delimiter. +cmdline::property_option::property_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_) : + base_option(short_name_, long_name_, description_, arg_name_) +{ + PRE(arg_name().find('=') != std::string::npos); +} + + +/// Constructs a property option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. Must include the '=' delimiter. +cmdline::property_option::property_option(const char* long_name_, + const char* description_, + const char* arg_name_) : + base_option(long_name_, description_, arg_name_) +{ + PRE(arg_name().find('=') != std::string::npos); +} + + +/// Validates the argument to a property option. +/// +/// \param raw_value The argument provided by the user. +void +cmdline::property_option::validate(const std::string& raw_value) const +{ + const std::string::size_type pos = raw_value.find('='); + if (pos == std::string::npos) + throw cmdline::option_argument_value_error( + F("--%s") % long_name(), raw_value, + F("Argument does not have the form '%s'") % arg_name()); + + const std::string key = raw_value.substr(0, pos); + if (key.empty()) + throw cmdline::option_argument_value_error( + F("--%s") % long_name(), raw_value, "Empty property name"); + + const std::string value = raw_value.substr(pos + 1); + if (value.empty()) + throw cmdline::option_argument_value_error( + F("--%s") % long_name(), raw_value, "Empty value"); +} + + +/// Returns the property option in a key/value pair form. +/// +/// \param raw_value The argument provided by the user. +/// +/// \return raw_value The key/value pair representation of the property. +/// +/// \pre validate(raw_value) must be true. +cmdline::property_option::option_type +cmdline::property_option::convert(const std::string& raw_value) +{ + const std::string::size_type pos = raw_value.find('='); + return std::make_pair(raw_value.substr(0, pos), raw_value.substr(pos + 1)); +} + + +/// Constructs a string option with both a short and a long name. +/// +/// \param short_name_ The short name for the option. +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::string_option::string_option(const char short_name_, + const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) +{ +} + + +/// Constructs a string option with a long name only. +/// +/// \param long_name_ The long name for the option. +/// \param description_ A user-friendly description for the option. +/// \param arg_name_ The name of the mandatory argument, for documentation +/// purposes. +/// \param default_value_ If not NULL, the default value for the mandatory +/// argument. +cmdline::string_option::string_option(const char* long_name_, + const char* description_, + const char* arg_name_, + const char* default_value_) : + base_option(long_name_, description_, arg_name_, default_value_) +{ +} + + +/// Does nothing; all string values are valid arguments to a string_option. +void +cmdline::string_option::validate( + const std::string& /* raw_value */) const +{ + // Nothing to do. +} + + +/// Returns the string unmodified. +/// +/// \param raw_value The argument provided by the user. +/// +/// \return raw_value +/// +/// \pre validate(raw_value) must be true. +std::string +cmdline::string_option::convert(const std::string& raw_value) +{ + return raw_value; +} diff --git a/utils/cmdline/options.hpp b/utils/cmdline/options.hpp new file mode 100644 index 000000000000..f3a83889e491 --- /dev/null +++ b/utils/cmdline/options.hpp @@ -0,0 +1,237 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/options.hpp +/// Definitions of command-line options. + +#if !defined(UTILS_CMDLINE_OPTIONS_HPP) +#define UTILS_CMDLINE_OPTIONS_HPP + +#include "utils/cmdline/options_fwd.hpp" + +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace cmdline { + + +/// Type-less base option class. +/// +/// This abstract class provides the most generic representation of options. It +/// allows defining options with both short and long names, with and without +/// arguments and with and without optional values. These are all the possible +/// combinations supported by the getopt_long(3) function, on which this is +/// built. +/// +/// The internal values (e.g. the default value) of a generic option are all +/// represented as strings. However, from the caller's perspective, this is +/// suboptimal. Hence why this class must be specialized: the subclasses +/// provide type-specific accessors and provide automatic validation of the +/// types (e.g. a string '3foo' is not passed to an integer option). +/// +/// Given that subclasses are used through templatized code, they must provide: +/// +///
    +///
  • A public option_type typedef that defines the type of the +/// option.
  • +/// +///
  • A convert() method that takes a string and converts it to +/// option_type. The string can be assumed to be convertible to the +/// destination type. Should not raise exceptions.
  • +/// +///
  • A validate() method that matches the implementation of convert(). +/// This method can throw option_argument_value_error if the string cannot +/// be converted appropriately. If validate() does not throw, then +/// convert() must execute successfully.
  • +///
+/// +/// TODO(jmmv): Many methods in this class are split into two parts: has_foo() +/// and foo(), the former to query if the foo is available and the latter to get +/// the foo. It'd be very nice if we'd use something similar Boost.Optional to +/// simplify this interface altogether. +class base_option { + /// Short name of the option; 0 to indicate that none is available. + char _short_name; + + /// Long name of the option. + std::string _long_name; + + /// Textual description of the purpose of the option. + std::string _description; + + /// Descriptive name of the required argument; empty if not allowed. + std::string _arg_name; + + /// Whether the option has a default value or not. + /// + /// \todo We should probably be using the optional class here. + bool _has_default_value; + + /// If _has_default_value is true, the default value. + std::string _default_value; + +public: + base_option(const char, const char*, const char*, const char* = NULL, + const char* = NULL); + base_option(const char*, const char*, const char* = NULL, + const char* = NULL); + virtual ~base_option(void); + + bool has_short_name(void) const; + char short_name(void) const; + const std::string& long_name(void) const; + const std::string& description(void) const; + + bool needs_arg(void) const; + const std::string& arg_name(void) const; + + bool has_default_value(void) const; + const std::string& default_value(void) const; + + std::string format_short_name(void) const; + std::string format_long_name(void) const; + + virtual void validate(const std::string&) const; +}; + + +/// Definition of a boolean option. +/// +/// A boolean option can be specified once in the command line, at which point +/// is set to true. Such an option cannot carry optional arguments. +class bool_option : public base_option { +public: + bool_option(const char, const char*, const char*); + bool_option(const char*, const char*); + virtual ~bool_option(void) {} + + /// The data type of this option. + typedef bool option_type; +}; + + +/// Definition of an integer option. +class int_option : public base_option { +public: + int_option(const char, const char*, const char*, const char*, + const char* = NULL); + int_option(const char*, const char*, const char*, const char* = NULL); + virtual ~int_option(void) {} + + /// The data type of this option. + typedef int option_type; + + virtual void validate(const std::string& str) const; + static int convert(const std::string& str); +}; + + +/// Definition of a comma-separated list of strings. +class list_option : public base_option { +public: + list_option(const char, const char*, const char*, const char*, + const char* = NULL); + list_option(const char*, const char*, const char*, const char* = NULL); + virtual ~list_option(void) {} + + /// The data type of this option. + typedef std::vector< std::string > option_type; + + virtual void validate(const std::string&) const; + static option_type convert(const std::string&); +}; + + +/// Definition of an option representing a path. +/// +/// The path pointed to by the option may not exist, but it must be +/// syntactically valid. +class path_option : public base_option { +public: + path_option(const char, const char*, const char*, const char*, + const char* = NULL); + path_option(const char*, const char*, const char*, const char* = NULL); + virtual ~path_option(void) {} + + /// The data type of this option. + typedef utils::fs::path option_type; + + virtual void validate(const std::string&) const; + static utils::fs::path convert(const std::string&); +}; + + +/// Definition of a property option. +/// +/// A property option is an option whose required arguments are of the form +/// 'name=value'. Both components of the property are treated as free-form +/// non-empty strings; any other validation must happen on the caller side. +/// +/// \todo Would be nice if the delimiter was parametrizable. With the current +/// parser interface (convert() being a static method), the only way to do +/// this would be to templatize this class. +class property_option : public base_option { +public: + property_option(const char, const char*, const char*, const char*); + property_option(const char*, const char*, const char*); + virtual ~property_option(void) {} + + /// The data type of this option. + typedef std::pair< std::string, std::string > option_type; + + virtual void validate(const std::string& str) const; + static option_type convert(const std::string& str); +}; + + +/// Definition of a free-form string option. +/// +/// This class provides no restrictions on the argument passed to the option. +class string_option : public base_option { +public: + string_option(const char, const char*, const char*, const char*, + const char* = NULL); + string_option(const char*, const char*, const char*, const char* = NULL); + virtual ~string_option(void) {} + + /// The data type of this option. + typedef std::string option_type; + + virtual void validate(const std::string& str) const; + static std::string convert(const std::string& str); +}; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_OPTIONS_HPP) diff --git a/utils/cmdline/options_fwd.hpp b/utils/cmdline/options_fwd.hpp new file mode 100644 index 000000000000..8b45797e3920 --- /dev/null +++ b/utils/cmdline/options_fwd.hpp @@ -0,0 +1,51 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/options_fwd.hpp +/// Forward declarations for utils/cmdline/options.hpp + +#if !defined(UTILS_CMDLINE_OPTIONS_FWD_HPP) +#define UTILS_CMDLINE_OPTIONS_FWD_HPP + +namespace utils { +namespace cmdline { + + +class base_option; +class bool_option; +class int_option; +class list_option; +class path_option; +class property_option; +class string_option; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_OPTIONS_FWD_HPP) diff --git a/utils/cmdline/options_test.cpp b/utils/cmdline/options_test.cpp new file mode 100644 index 000000000000..82fd706a191a --- /dev/null +++ b/utils/cmdline/options_test.cpp @@ -0,0 +1,526 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/options.hpp" + +#include + +#include "utils/cmdline/exceptions.hpp" +#include "utils/defs.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; + +namespace { + + +/// Simple string-based option type for testing purposes. +class mock_option : public cmdline::base_option { +public: + /// Constructs a mock option with a short name and a long name. + /// + /// + /// \param short_name_ The short name for the option. + /// \param long_name_ The long name for the option. + /// \param description_ A user-friendly description for the option. + /// \param arg_name_ If not NULL, specifies that the option must receive an + /// argument and specifies the name of such argument for documentation + /// purposes. + /// \param default_value_ If not NULL, specifies that the option has a + /// default value for the mandatory argument. + mock_option(const char short_name_, const char* long_name_, + const char* description_, const char* arg_name_ = NULL, + const char* default_value_ = NULL) : + base_option(short_name_, long_name_, description_, arg_name_, + default_value_) {} + + /// Constructs a mock option with a long name only. + /// + /// \param long_name_ The long name for the option. + /// \param description_ A user-friendly description for the option. + /// \param arg_name_ If not NULL, specifies that the option must receive an + /// argument and specifies the name of such argument for documentation + /// purposes. + /// \param default_value_ If not NULL, specifies that the option has a + /// default value for the mandatory argument. + mock_option(const char* long_name_, + const char* description_, const char* arg_name_ = NULL, + const char* default_value_ = NULL) : + base_option(long_name_, description_, arg_name_, default_value_) {} + + /// The data type of this option. + typedef std::string option_type; + + /// Ensures that the argument passed to the option is valid. + /// + /// In this particular mock option, this does not perform any validation. + void + validate(const std::string& /* str */) const + { + // Do nothing. + } + + /// Returns the input parameter without any conversion. + /// + /// \param str The user-provided argument to the option. + /// + /// \return The same value as provided by the user without conversion. + static std::string + convert(const std::string& str) + { + return str; + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__short_name__no_arg); +ATF_TEST_CASE_BODY(base_option__short_name__no_arg) +{ + const mock_option o('f', "force", "Force execution"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('f', o.short_name()); + ATF_REQUIRE_EQ("force", o.long_name()); + ATF_REQUIRE_EQ("Force execution", o.description()); + ATF_REQUIRE(!o.needs_arg()); + ATF_REQUIRE_EQ("-f", o.format_short_name()); + ATF_REQUIRE_EQ("--force", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__short_name__with_arg__no_default); +ATF_TEST_CASE_BODY(base_option__short_name__with_arg__no_default) +{ + const mock_option o('c', "conf_file", "Configuration file", "path"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('c', o.short_name()); + ATF_REQUIRE_EQ("conf_file", o.long_name()); + ATF_REQUIRE_EQ("Configuration file", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("path", o.arg_name()); + ATF_REQUIRE(!o.has_default_value()); + ATF_REQUIRE_EQ("-c path", o.format_short_name()); + ATF_REQUIRE_EQ("--conf_file=path", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__short_name__with_arg__with_default); +ATF_TEST_CASE_BODY(base_option__short_name__with_arg__with_default) +{ + const mock_option o('c', "conf_file", "Configuration file", "path", + "defpath"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('c', o.short_name()); + ATF_REQUIRE_EQ("conf_file", o.long_name()); + ATF_REQUIRE_EQ("Configuration file", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("path", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("defpath", o.default_value()); + ATF_REQUIRE_EQ("-c path", o.format_short_name()); + ATF_REQUIRE_EQ("--conf_file=path", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__long_name__no_arg); +ATF_TEST_CASE_BODY(base_option__long_name__no_arg) +{ + const mock_option o("dryrun", "Dry run mode"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("dryrun", o.long_name()); + ATF_REQUIRE_EQ("Dry run mode", o.description()); + ATF_REQUIRE(!o.needs_arg()); + ATF_REQUIRE_EQ("--dryrun", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__long_name__with_arg__no_default); +ATF_TEST_CASE_BODY(base_option__long_name__with_arg__no_default) +{ + const mock_option o("helper", "Path to helper", "path"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("helper", o.long_name()); + ATF_REQUIRE_EQ("Path to helper", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("path", o.arg_name()); + ATF_REQUIRE(!o.has_default_value()); + ATF_REQUIRE_EQ("--helper=path", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_option__long_name__with_arg__with_default); +ATF_TEST_CASE_BODY(base_option__long_name__with_arg__with_default) +{ + const mock_option o("executable", "Executable name", "file", "foo"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("executable", o.long_name()); + ATF_REQUIRE_EQ("Executable name", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("file", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("foo", o.default_value()); + ATF_REQUIRE_EQ("--executable=file", o.format_long_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_option__short_name); +ATF_TEST_CASE_BODY(bool_option__short_name) +{ + const cmdline::bool_option o('f', "force", "Force execution"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('f', o.short_name()); + ATF_REQUIRE_EQ("force", o.long_name()); + ATF_REQUIRE_EQ("Force execution", o.description()); + ATF_REQUIRE(!o.needs_arg()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_option__long_name); +ATF_TEST_CASE_BODY(bool_option__long_name) +{ + const cmdline::bool_option o("force", "Force execution"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("force", o.long_name()); + ATF_REQUIRE_EQ("Force execution", o.description()); + ATF_REQUIRE(!o.needs_arg()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_option__short_name); +ATF_TEST_CASE_BODY(int_option__short_name) +{ + const cmdline::int_option o('p', "int", "The int", "arg", "value"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("int", o.long_name()); + ATF_REQUIRE_EQ("The int", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_option__long_name); +ATF_TEST_CASE_BODY(int_option__long_name) +{ + const cmdline::int_option o("int", "The int", "arg", "value"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("int", o.long_name()); + ATF_REQUIRE_EQ("The int", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_option__type); +ATF_TEST_CASE_BODY(int_option__type) +{ + const cmdline::int_option o("int", "The int", "arg"); + + o.validate("123"); + ATF_REQUIRE_EQ(123, cmdline::int_option::convert("123")); + + o.validate("-567"); + ATF_REQUIRE_EQ(-567, cmdline::int_option::convert("-567")); + + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("5a")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("a5")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("5 a")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("5.0")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_option__short_name); +ATF_TEST_CASE_BODY(list_option__short_name) +{ + const cmdline::list_option o('p', "list", "The list", "arg", "value"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("list", o.long_name()); + ATF_REQUIRE_EQ("The list", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_option__long_name); +ATF_TEST_CASE_BODY(list_option__long_name) +{ + const cmdline::list_option o("list", "The list", "arg", "value"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("list", o.long_name()); + ATF_REQUIRE_EQ("The list", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_option__type); +ATF_TEST_CASE_BODY(list_option__type) +{ + const cmdline::list_option o("list", "The list", "arg"); + + o.validate(""); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert(""); + ATF_REQUIRE(words.empty()); + } + + o.validate("foo"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert("foo"); + ATF_REQUIRE_EQ(1, words.size()); + ATF_REQUIRE_EQ("foo", words[0]); + } + + o.validate("foo,bar,baz"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert("foo,bar,baz"); + ATF_REQUIRE_EQ(3, words.size()); + ATF_REQUIRE_EQ("foo", words[0]); + ATF_REQUIRE_EQ("bar", words[1]); + ATF_REQUIRE_EQ("baz", words[2]); + } + + o.validate("foo,bar,"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert("foo,bar,"); + ATF_REQUIRE_EQ(3, words.size()); + ATF_REQUIRE_EQ("foo", words[0]); + ATF_REQUIRE_EQ("bar", words[1]); + ATF_REQUIRE_EQ("", words[2]); + } + + o.validate(",foo,bar"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert(",foo,bar"); + ATF_REQUIRE_EQ(3, words.size()); + ATF_REQUIRE_EQ("", words[0]); + ATF_REQUIRE_EQ("foo", words[1]); + ATF_REQUIRE_EQ("bar", words[2]); + } + + o.validate("foo,,bar"); + { + const cmdline::list_option::option_type words = + cmdline::list_option::convert("foo,,bar"); + ATF_REQUIRE_EQ(3, words.size()); + ATF_REQUIRE_EQ("foo", words[0]); + ATF_REQUIRE_EQ("", words[1]); + ATF_REQUIRE_EQ("bar", words[2]); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(path_option__short_name); +ATF_TEST_CASE_BODY(path_option__short_name) +{ + const cmdline::path_option o('p', "path", "The path", "arg", "value"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("path", o.long_name()); + ATF_REQUIRE_EQ("The path", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(path_option__long_name); +ATF_TEST_CASE_BODY(path_option__long_name) +{ + const cmdline::path_option o("path", "The path", "arg", "value"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("path", o.long_name()); + ATF_REQUIRE_EQ("The path", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(path_option__type); +ATF_TEST_CASE_BODY(path_option__type) +{ + const cmdline::path_option o("path", "The path", "arg"); + + o.validate("/some/path"); + + try { + o.validate(""); + fail("option_argument_value_error not raised"); + } catch (const cmdline::option_argument_value_error& e) { + // Expected; ignore. + } + + const cmdline::path_option::option_type path = + cmdline::path_option::convert("/foo/bar"); + ATF_REQUIRE_EQ("bar", path.leaf_name()); // Ensure valid type. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(property_option__short_name); +ATF_TEST_CASE_BODY(property_option__short_name) +{ + const cmdline::property_option o('p', "property", "The property", "a=b"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("property", o.long_name()); + ATF_REQUIRE_EQ("The property", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("a=b", o.arg_name()); + ATF_REQUIRE(!o.has_default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(property_option__long_name); +ATF_TEST_CASE_BODY(property_option__long_name) +{ + const cmdline::property_option o("property", "The property", "a=b"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("property", o.long_name()); + ATF_REQUIRE_EQ("The property", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("a=b", o.arg_name()); + ATF_REQUIRE(!o.has_default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(property_option__type); +ATF_TEST_CASE_BODY(property_option__type) +{ + typedef std::pair< std::string, std::string > string_pair; + const cmdline::property_option o("property", "The property", "a=b"); + + o.validate("foo=bar"); + ATF_REQUIRE(string_pair("foo", "bar") == + cmdline::property_option::convert("foo=bar")); + + o.validate(" foo = bar baz"); + ATF_REQUIRE(string_pair(" foo ", " bar baz") == + cmdline::property_option::convert(" foo = bar baz")); + + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("=")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("a=")); + ATF_REQUIRE_THROW(cmdline::option_argument_value_error, o.validate("=b")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_option__short_name); +ATF_TEST_CASE_BODY(string_option__short_name) +{ + const cmdline::string_option o('p', "string", "The string", "arg", "value"); + ATF_REQUIRE(o.has_short_name()); + ATF_REQUIRE_EQ('p', o.short_name()); + ATF_REQUIRE_EQ("string", o.long_name()); + ATF_REQUIRE_EQ("The string", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_option__long_name); +ATF_TEST_CASE_BODY(string_option__long_name) +{ + const cmdline::string_option o("string", "The string", "arg", "value"); + ATF_REQUIRE(!o.has_short_name()); + ATF_REQUIRE_EQ("string", o.long_name()); + ATF_REQUIRE_EQ("The string", o.description()); + ATF_REQUIRE(o.needs_arg()); + ATF_REQUIRE_EQ("arg", o.arg_name()); + ATF_REQUIRE(o.has_default_value()); + ATF_REQUIRE_EQ("value", o.default_value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_option__type); +ATF_TEST_CASE_BODY(string_option__type) +{ + const cmdline::string_option o("string", "The string", "foo"); + + o.validate(""); + o.validate("some string"); + + const cmdline::string_option::option_type string = + cmdline::string_option::convert("foo"); + ATF_REQUIRE_EQ(3, string.length()); // Ensure valid type. +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, base_option__short_name__no_arg); + ATF_ADD_TEST_CASE(tcs, base_option__short_name__with_arg__no_default); + ATF_ADD_TEST_CASE(tcs, base_option__short_name__with_arg__with_default); + ATF_ADD_TEST_CASE(tcs, base_option__long_name__no_arg); + ATF_ADD_TEST_CASE(tcs, base_option__long_name__with_arg__no_default); + ATF_ADD_TEST_CASE(tcs, base_option__long_name__with_arg__with_default); + + ATF_ADD_TEST_CASE(tcs, bool_option__short_name); + ATF_ADD_TEST_CASE(tcs, bool_option__long_name); + + ATF_ADD_TEST_CASE(tcs, int_option__short_name); + ATF_ADD_TEST_CASE(tcs, int_option__long_name); + ATF_ADD_TEST_CASE(tcs, int_option__type); + + ATF_ADD_TEST_CASE(tcs, list_option__short_name); + ATF_ADD_TEST_CASE(tcs, list_option__long_name); + ATF_ADD_TEST_CASE(tcs, list_option__type); + + ATF_ADD_TEST_CASE(tcs, path_option__short_name); + ATF_ADD_TEST_CASE(tcs, path_option__long_name); + ATF_ADD_TEST_CASE(tcs, path_option__type); + + ATF_ADD_TEST_CASE(tcs, property_option__short_name); + ATF_ADD_TEST_CASE(tcs, property_option__long_name); + ATF_ADD_TEST_CASE(tcs, property_option__type); + + ATF_ADD_TEST_CASE(tcs, string_option__short_name); + ATF_ADD_TEST_CASE(tcs, string_option__long_name); + ATF_ADD_TEST_CASE(tcs, string_option__type); +} diff --git a/utils/cmdline/parser.cpp b/utils/cmdline/parser.cpp new file mode 100644 index 000000000000..5c83f6d69cc4 --- /dev/null +++ b/utils/cmdline/parser.cpp @@ -0,0 +1,385 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/parser.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include +} + +#include +#include +#include + +#include "utils/auto_array.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/format/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + +namespace { + + +/// Auxiliary data to call getopt_long(3). +struct getopt_data : utils::noncopyable { + /// Plain-text representation of the short options. + /// + /// This string follows the syntax expected by getopt_long(3) in the + /// argument to describe the short options. + std::string short_options; + + /// Representation of the long options as expected by getopt_long(3). + utils::auto_array< ::option > long_options; + + /// Auto-generated identifiers to be able to parse long options. + std::map< int, const cmdline::base_option* > ids; +}; + + +/// Converts a cmdline::options_vector to a getopt_data. +/// +/// \param options The high-level definition of the options. +/// \param [out] data An object containing the necessary data to call +/// getopt_long(3) and interpret its results. +static void +options_to_getopt_data(const cmdline::options_vector& options, + getopt_data& data) +{ + data.short_options.clear(); + data.long_options.reset(new ::option[options.size() + 1]); + + int cur_id = 512; + + for (cmdline::options_vector::size_type i = 0; i < options.size(); i++) { + const cmdline::base_option* option = options[i]; + ::option& long_option = data.long_options[i]; + + long_option.name = option->long_name().c_str(); + if (option->needs_arg()) + long_option.has_arg = required_argument; + else + long_option.has_arg = no_argument; + + int id = -1; + if (option->has_short_name()) { + data.short_options += option->short_name(); + if (option->needs_arg()) + data.short_options += ':'; + id = option->short_name(); + } else { + id = cur_id++; + } + long_option.flag = NULL; + long_option.val = id; + data.ids[id] = option; + } + + ::option& last_long_option = data.long_options[options.size()]; + last_long_option.name = NULL; + last_long_option.has_arg = 0; + last_long_option.flag = NULL; + last_long_option.val = 0; +} + + +/// Converts an argc/argv pair to an args_vector. +/// +/// \param argc The value of argc as passed to main(). +/// \param argv The value of argv as passed to main(). +/// +/// \return An args_vector with the same contents of argc/argv. +static cmdline::args_vector +argv_to_vector(int argc, const char* const argv[]) +{ + PRE(argv[argc] == NULL); + cmdline::args_vector args; + for (int i = 0; i < argc; i++) + args.push_back(argv[i]); + return args; +} + + +/// Creates a mutable version of argv. +/// +/// \param argc The value of argc as passed to main(). +/// \param argv The value of argv as passed to main(). +/// +/// \return A new argv, with mutable buffers. The returned array must be +/// released using the free_mutable_argv() function. +static char** +make_mutable_argv(const int argc, const char* const* argv) +{ + char** mutable_argv = new char*[argc + 1]; + for (int i = 0; i < argc; i++) + mutable_argv[i] = ::strdup(argv[i]); + mutable_argv[argc] = NULL; + return mutable_argv; +} + + +/// Releases the object returned by make_mutable_argv(). +/// +/// \param argv A dynamically-allocated argv as returned by make_mutable_argv(). +static void +free_mutable_argv(char** argv) +{ + char** ptr = argv; + while (*ptr != NULL) { + ::free(*ptr); + ptr++; + } + delete [] argv; +} + + +/// Finds the name of the offending option after a getopt_long error. +/// +/// \param data Our internal getopt data used for the call to getopt_long. +/// \param getopt_optopt The value of getopt(3)'s optopt after the error. +/// \param argv The argv passed to getopt_long. +/// \param getopt_optind The value of getopt(3)'s optind after the error. +/// +/// \return A fully-specified option name (i.e. an option name prefixed by +/// either '-' or '--'). +static std::string +find_option_name(const getopt_data& data, const int getopt_optopt, + char** argv, const int getopt_optind) +{ + PRE(getopt_optopt >= 0); + + if (getopt_optopt == 0) { + return argv[getopt_optind - 1]; + } else if (getopt_optopt < std::numeric_limits< char >::max()) { + INV(getopt_optopt > 0); + const char ch = static_cast< char >(getopt_optopt); + return F("-%s") % ch; + } else { + for (const ::option* opt = &data.long_options[0]; opt->name != NULL; + opt++) { + if (opt->val == getopt_optopt) + return F("--%s") % opt->name; + } + UNREACHABLE; + } +} + + +} // anonymous namespace + + +/// Constructs a new parsed_cmdline. +/// +/// Use the cmdline::parse() free functions to construct. +/// +/// \param option_values_ A mapping of long option names to values. This +/// contains a representation of the options provided by the user. Note +/// that each value is actually a collection values: a user may specify a +/// flag multiple times, and depending on the case we want to honor one or +/// the other. For those options that support no argument, the argument +/// value is the empty string. +/// \param arguments_ The list of non-option arguments in the command line. +cmdline::parsed_cmdline::parsed_cmdline( + const std::map< std::string, std::vector< std::string > >& option_values_, + const cmdline::args_vector& arguments_) : + _option_values(option_values_), + _arguments(arguments_) +{ +} + + +/// Checks if the given option has been given in the command line. +/// +/// \param name The long option name to check for presence. +/// +/// \return True if the option has been given; false otherwise. +bool +cmdline::parsed_cmdline::has_option(const std::string& name) const +{ + return _option_values.find(name) != _option_values.end(); +} + + +/// Gets the raw value of an option. +/// +/// The raw value of an option is a collection of strings that represent all the +/// values passed to the option on the command line. It is up to the consumer +/// if he wants to honor only the last value or all of them. +/// +/// The caller has to use get_option() instead; this function is internal. +/// +/// \pre has_option(name) must be true. +/// +/// \param name The option to query. +/// +/// \return The value of the option as a plain string. +const std::vector< std::string >& +cmdline::parsed_cmdline::get_option_raw(const std::string& name) const +{ + std::map< std::string, std::vector< std::string > >::const_iterator iter = + _option_values.find(name); + INV_MSG(iter != _option_values.end(), F("Undefined option --%s") % name); + return (*iter).second; +} + + +/// Returns the non-option arguments found in the command line. +/// +/// \return The arguments, if any. +const cmdline::args_vector& +cmdline::parsed_cmdline::arguments(void) const +{ + return _arguments; +} + + +/// Parses a command line. +/// +/// \param args The command line to parse, broken down by words. +/// \param options The description of the supported options. +/// +/// \return The parsed command line. +/// +/// \pre args[0] must be the program or command name. +/// +/// \throw cmdline::error See the description of parse(argc, argv, options) for +/// more details on the raised errors. +cmdline::parsed_cmdline +cmdline::parse(const cmdline::args_vector& args, + const cmdline::options_vector& options) +{ + PRE_MSG(args.size() >= 1, "No progname or command name found"); + + utils::auto_array< const char* > argv(new const char*[args.size() + 1]); + for (args_vector::size_type i = 0; i < args.size(); i++) + argv[i] = args[i].c_str(); + argv[args.size()] = NULL; + return parse(static_cast< int >(args.size()), argv.get(), options); +} + + +/// Parses a command line. +/// +/// \param argc The number of arguments in argv, without counting the +/// terminating NULL. +/// \param argv The arguments to parse. The array is NULL-terminated. +/// \param options The description of the supported options. +/// +/// \return The parsed command line. +/// +/// \pre args[0] must be the program or command name. +/// +/// \throw cmdline::missing_option_argument_error If the user specified an +/// option that requires an argument, but no argument was provided. +/// \throw cmdline::unknown_option_error If the user specified an unknown +/// option (i.e. an option not defined in options). +/// \throw cmdline::option_argument_value_error If the user passed an invalid +/// argument to a supported option. +cmdline::parsed_cmdline +cmdline::parse(const int argc, const char* const* argv, + const cmdline::options_vector& options) +{ + PRE_MSG(argc >= 1, "No progname or command name found"); + + getopt_data data; + options_to_getopt_data(options, data); + + std::map< std::string, std::vector< std::string > > option_values; + + for (cmdline::options_vector::const_iterator iter = options.begin(); + iter != options.end(); iter++) { + const cmdline::base_option* option = *iter; + if (option->needs_arg() && option->has_default_value()) + option_values[option->long_name()].push_back( + option->default_value()); + } + + args_vector args; + + int mutable_argc = argc; + char** mutable_argv = make_mutable_argv(argc, argv); + const int old_opterr = ::opterr; + try { + int ch; + + ::opterr = 0; + + while ((ch = ::getopt_long(mutable_argc, mutable_argv, + ("+:" + data.short_options).c_str(), + data.long_options.get(), NULL)) != -1) { + if (ch == ':' ) { + const std::string name = find_option_name( + data, ::optopt, mutable_argv, ::optind); + throw cmdline::missing_option_argument_error(name); + } else if (ch == '?') { + const std::string name = find_option_name( + data, ::optopt, mutable_argv, ::optind); + throw cmdline::unknown_option_error(name); + } + + const std::map< int, const cmdline::base_option* >::const_iterator + id = data.ids.find(ch); + INV(id != data.ids.end()); + const cmdline::base_option* option = (*id).second; + + if (option->needs_arg()) { + if (::optarg != NULL) { + option->validate(::optarg); + option_values[option->long_name()].push_back(::optarg); + } else + INV(option->has_default_value()); + } else { + option_values[option->long_name()].push_back(""); + } + } + args = argv_to_vector(mutable_argc - optind, mutable_argv + optind); + + ::opterr = old_opterr; + ::optind = GETOPT_OPTIND_RESET_VALUE; +#if defined(HAVE_GETOPT_WITH_OPTRESET) + ::optreset = 1; +#endif + } catch (...) { + free_mutable_argv(mutable_argv); + ::opterr = old_opterr; + ::optind = GETOPT_OPTIND_RESET_VALUE; +#if defined(HAVE_GETOPT_WITH_OPTRESET) + ::optreset = 1; +#endif + throw; + } + free_mutable_argv(mutable_argv); + + return parsed_cmdline(option_values, args); +} diff --git a/utils/cmdline/parser.hpp b/utils/cmdline/parser.hpp new file mode 100644 index 000000000000..657fd1f01dd3 --- /dev/null +++ b/utils/cmdline/parser.hpp @@ -0,0 +1,85 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/parser.hpp +/// Routines and data types to parse command line options and arguments. + +#if !defined(UTILS_CMDLINE_PARSER_HPP) +#define UTILS_CMDLINE_PARSER_HPP + +#include "utils/cmdline/parser_fwd.hpp" + +#include +#include +#include + +namespace utils { +namespace cmdline { + + +/// Representation of a parsed command line. +/// +/// This class is returned by the command line parsing algorithm and provides +/// methods to query the values of the options and the value of the arguments. +/// All the values fed into this class can considered to be sane (i.e. the +/// arguments to the options and the arguments to the command are valid), as all +/// validation happens during parsing (before this class is instantiated). +class parsed_cmdline { + /// Mapping of option names to all the values provided. + std::map< std::string, std::vector< std::string > > _option_values; + + /// Collection of arguments with all options removed. + args_vector _arguments; + + const std::vector< std::string >& get_option_raw(const std::string&) const; + +public: + parsed_cmdline(const std::map< std::string, std::vector< std::string > >&, + const args_vector&); + + bool has_option(const std::string&) const; + + template< typename Option > + typename Option::option_type get_option(const std::string&) const; + + template< typename Option > + std::vector< typename Option::option_type > get_multi_option( + const std::string&) const; + + const args_vector& arguments(void) const; +}; + + +parsed_cmdline parse(const args_vector&, const options_vector&); +parsed_cmdline parse(const int, const char* const*, const options_vector&); + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_PARSER_HPP) diff --git a/utils/cmdline/parser.ipp b/utils/cmdline/parser.ipp new file mode 100644 index 000000000000..820826a15bfe --- /dev/null +++ b/utils/cmdline/parser.ipp @@ -0,0 +1,83 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_CMDLINE_PARSER_IPP) +#define UTILS_CMDLINE_PARSER_IPP + +#include "utils/cmdline/parser.hpp" + + +/// Gets the value of an option. +/// +/// If the option has been specified multiple times on the command line, this +/// only returns the last value. This is the traditional behavior. +/// +/// The option must support arguments. Otherwise, a call to this function will +/// not compile because the option type will lack the definition of some fields +/// and/or methods. +/// +/// \param name The option to query. +/// +/// \return The value of the option converted to the appropriate type. +/// +/// \pre has_option(name) must be true. +template< typename Option > typename Option::option_type +utils::cmdline::parsed_cmdline::get_option(const std::string& name) const +{ + const std::vector< std::string >& raw_values = get_option_raw(name); + return Option::convert(raw_values[raw_values.size() - 1]); +} + + +/// Gets the values of an option that supports repetition. +/// +/// The option must support arguments. Otherwise, a call to this function will +/// not compile because the option type will lack the definition of some fields +/// and/or methods. +/// +/// \param name The option to query. +/// +/// \return The values of the option converted to the appropriate type. +/// +/// \pre has_option(name) must be true. +template< typename Option > std::vector< typename Option::option_type > +utils::cmdline::parsed_cmdline::get_multi_option(const std::string& name) const +{ + std::vector< typename Option::option_type > values; + + const std::vector< std::string >& raw_values = get_option_raw(name); + for (std::vector< std::string >::const_iterator iter = raw_values.begin(); + iter != raw_values.end(); iter++) { + values.push_back(Option::convert(*iter)); + } + + return values; +} + + +#endif // !defined(UTILS_CMDLINE_PARSER_IPP) diff --git a/utils/cmdline/parser_fwd.hpp b/utils/cmdline/parser_fwd.hpp new file mode 100644 index 000000000000..a136e99a47ac --- /dev/null +++ b/utils/cmdline/parser_fwd.hpp @@ -0,0 +1,58 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/parser_fwd.hpp +/// Forward declarations for utils/cmdline/parser.hpp + +#if !defined(UTILS_CMDLINE_PARSER_FWD_HPP) +#define UTILS_CMDLINE_PARSER_FWD_HPP + +#include +#include + +#include "utils/cmdline/options_fwd.hpp" + +namespace utils { +namespace cmdline { + + +/// Replacement for argc and argv to represent a command line. +typedef std::vector< std::string > args_vector; + + +/// Collection of options to be used during parsing. +typedef std::vector< const base_option* > options_vector; + + +class parsed_cmdline; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_PARSER_FWD_HPP) diff --git a/utils/cmdline/parser_test.cpp b/utils/cmdline/parser_test.cpp new file mode 100644 index 000000000000..96370d279d2e --- /dev/null +++ b/utils/cmdline/parser_test.cpp @@ -0,0 +1,688 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/parser.ipp" + +#if defined(HAVE_CONFIG_H) +#include "config.h" +#endif + +extern "C" { +#include +#include +#include +} + +#include +#include +#include +#include +#include +#include + +#include + +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; + +using cmdline::base_option; +using cmdline::bool_option; +using cmdline::int_option; +using cmdline::parse; +using cmdline::parsed_cmdline; +using cmdline::string_option; + + +namespace { + + +/// Mock option type to check the validate and convert methods sequence. +/// +/// Instances of this option accept a string argument that must be either "zero" +/// or "one". These are validated and converted to integers. +class mock_option : public base_option { +public: + /// Constructs the new option. + /// + /// \param long_name_ The long name for the option. All other option + /// properties are irrelevant for the tests using this, so they are set + /// to arbitrary values. + mock_option(const char* long_name_) : + base_option(long_name_, "Irrelevant description", "arg") + { + } + + /// The type of the argument of this option. + typedef int option_type; + + /// Checks that the user-provided option is valid. + /// + /// \param str The user argument; must be "zero" or "one". + /// + /// \throw cmdline::option_argument_value_error If str is not valid. + void + validate(const std::string& str) const + { + if (str != "zero" && str != "one") + throw cmdline::option_argument_value_error(F("--%s") % long_name(), + str, "Unknown value"); + } + + /// Converts the user-provided argument to our native integer type. + /// + /// \param str The user argument; must be "zero" or "one". + /// + /// \return 0 if the input is "zero", or 1 if the input is "one". + /// + /// \throw std::runtime_error If str is not valid. In real life, this + /// should be a precondition because validate() has already ensured that + /// the values passed to convert() are correct. However, we raise an + /// exception here because we are actually validating that this code + /// sequence holds true. + static int + convert(const std::string& str) + { + if (str == "zero") + return 0; + else if (str == "one") + return 1; + else { + // This would generally be an assertion but, given that this is + // test code, we want to catch any errors regardless of how the + // binary is built. + throw std::runtime_error("Value not validated properly."); + } + } +}; + + +/// Redirects stdout and stderr to a file. +/// +/// This fails the test case in case of any error. +/// +/// \param file The name of the file to redirect stdout and stderr to. +/// +/// \return A copy of the old stdout and stderr file descriptors. +static std::pair< int, int > +mock_stdfds(const char* file) +{ + std::cout.flush(); + std::cerr.flush(); + + const int oldout = ::dup(STDOUT_FILENO); + ATF_REQUIRE(oldout != -1); + const int olderr = ::dup(STDERR_FILENO); + ATF_REQUIRE(olderr != -1); + + const int fd = ::open(file, O_WRONLY | O_CREAT | O_TRUNC, 0644); + ATF_REQUIRE(fd != -1); + ATF_REQUIRE(::dup2(fd, STDOUT_FILENO) != -1); + ATF_REQUIRE(::dup2(fd, STDERR_FILENO) != -1); + ::close(fd); + + return std::make_pair(oldout, olderr); +} + + +/// Restores stdout and stderr after a call to mock_stdfds. +/// +/// \param oldfds The copy of the previous stdout and stderr as returned by the +/// call to mock_fds(). +static void +restore_stdfds(const std::pair< int, int >& oldfds) +{ + ATF_REQUIRE(::dup2(oldfds.first, STDOUT_FILENO) != -1); + ::close(oldfds.first); + ATF_REQUIRE(::dup2(oldfds.second, STDERR_FILENO) != -1); + ::close(oldfds.second); +} + + +/// Checks whether a '+:' prefix to the short options of getopt_long works. +/// +/// It turns out that the getopt_long(3) implementation of Ubuntu 10.04.1 (and +/// very likely other distributions) does not properly report a missing argument +/// to a second long option as such. Instead of returning ':' when the second +/// long option provided on the command line does not carry a required argument, +/// it will mistakenly return '?' which translates to "unknown option". +/// +/// As a result of this bug, we cannot properly detect that 'flag2' requires an +/// argument in a command line like: 'progname --flag1=foo --flag2'. +/// +/// I am not sure if we could fully workaround the issue in the implementation +/// of our library. For the time being I am just using this bug detection in +/// the test cases to prevent failures that are not really our fault. +/// +/// \return bool True if getopt_long is broken and does not interpret '+:' +/// correctly; False otherwise. +static bool +is_getopt_long_pluscolon_broken(void) +{ + struct ::option long_options[] = { + { "flag1", 1, NULL, '1' }, + { "flag2", 1, NULL, '2' }, + { NULL, 0, NULL, 0 } + }; + + const int argc = 3; + char* argv[4]; + argv[0] = ::strdup("progname"); + argv[1] = ::strdup("--flag1=a"); + argv[2] = ::strdup("--flag2"); + argv[3] = NULL; + + const int old_opterr = ::opterr; + ::opterr = 0; + + bool got_colon = false; + + int opt; + while ((opt = ::getopt_long(argc, argv, "+:", long_options, NULL)) != -1) { + switch (opt) { + case '1': break; + case '2': break; + case ':': got_colon = true; break; + case '?': break; + default: UNREACHABLE; break; + } + } + + ::opterr = old_opterr; + ::optind = 1; +#if defined(HAVE_GETOPT_WITH_OPTRESET) + ::optreset = 1; +#endif + + for (char** arg = &argv[0]; *arg != NULL; arg++) + std::free(*arg); + + return !got_colon; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__no_options); +ATF_TEST_CASE_BODY(progname__no_options) +{ + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + std::vector< const base_option* > options; + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(cmdline.arguments().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(progname__some_options); +ATF_TEST_CASE_BODY(progname__some_options) +{ + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + const string_option a('a', "a_option", "Foo", NULL); + const string_option b('b', "b_option", "Bar", "arg", "foo"); + const string_option c("c_option", "Baz", NULL); + const string_option d("d_option", "Wohoo", "arg", "bar"); + std::vector< const base_option* > options; + options.push_back(&a); + options.push_back(&b); + options.push_back(&c); + options.push_back(&d); + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE_EQ("foo", cmdline.get_option< string_option >("b_option")); + ATF_REQUIRE_EQ("bar", cmdline.get_option< string_option >("d_option")); + ATF_REQUIRE(cmdline.arguments().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_args__no_options); +ATF_TEST_CASE_BODY(some_args__no_options) +{ + const int argc = 5; + const char* const argv[] = {"progname", "foo", "-c", "--opt", "bar", NULL}; + std::vector< const base_option* > options; + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(!cmdline.has_option("c")); + ATF_REQUIRE(!cmdline.has_option("opt")); + ATF_REQUIRE_EQ(4, cmdline.arguments().size()); + ATF_REQUIRE_EQ("foo", cmdline.arguments()[0]); + ATF_REQUIRE_EQ("-c", cmdline.arguments()[1]); + ATF_REQUIRE_EQ("--opt", cmdline.arguments()[2]); + ATF_REQUIRE_EQ("bar", cmdline.arguments()[3]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_args__some_options); +ATF_TEST_CASE_BODY(some_args__some_options) +{ + const int argc = 5; + const char* const argv[] = {"progname", "foo", "-c", "--opt", "bar", NULL}; + const string_option c('c', "opt", "Description", NULL); + std::vector< const base_option* > options; + options.push_back(&c); + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(!cmdline.has_option("c")); + ATF_REQUIRE(!cmdline.has_option("opt")); + ATF_REQUIRE_EQ(4, cmdline.arguments().size()); + ATF_REQUIRE_EQ("foo", cmdline.arguments()[0]); + ATF_REQUIRE_EQ("-c", cmdline.arguments()[1]); + ATF_REQUIRE_EQ("--opt", cmdline.arguments()[2]); + ATF_REQUIRE_EQ("bar", cmdline.arguments()[3]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_options__all_known); +ATF_TEST_CASE_BODY(some_options__all_known) +{ + const int argc = 14; + const char* const argv[] = { + "progname", + "-a", + "-bvalue_b", + "-c", "value_c", + //"-d", // Options with default optional values are unsupported. + "-evalue_e", // Has default; overriden. + "--f_long", + "--g_long=value_g", + "--h_long", "value_h", + //"--i_long", // Options with default optional values are unsupported. + "--j_long", "value_j", // Has default; overriden as separate argument. + "arg1", "arg2", NULL, + }; + const bool_option a('a', "a_long", ""); + const string_option b('b', "b_long", "Description", "arg"); + const string_option c('c', "c_long", "ABCD", "foo"); + const string_option d('d', "d_long", "Description", "bar", "default_d"); + const string_option e('e', "e_long", "Description", "baz", "default_e"); + const bool_option f("f_long", "Description"); + const string_option g("g_long", "Description", "arg"); + const string_option h("h_long", "Description", "foo"); + const string_option i("i_long", "EFGH", "bar", "default_i"); + const string_option j("j_long", "Description", "baz", "default_j"); + std::vector< const base_option* > options; + options.push_back(&a); + options.push_back(&b); + options.push_back(&c); + options.push_back(&d); + options.push_back(&e); + options.push_back(&f); + options.push_back(&g); + options.push_back(&h); + options.push_back(&i); + options.push_back(&j); + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(cmdline.has_option("a_long")); + ATF_REQUIRE_EQ("value_b", cmdline.get_option< string_option >("b_long")); + ATF_REQUIRE_EQ("value_c", cmdline.get_option< string_option >("c_long")); + ATF_REQUIRE_EQ("default_d", cmdline.get_option< string_option >("d_long")); + ATF_REQUIRE_EQ("value_e", cmdline.get_option< string_option >("e_long")); + ATF_REQUIRE(cmdline.has_option("f_long")); + ATF_REQUIRE_EQ("value_g", cmdline.get_option< string_option >("g_long")); + ATF_REQUIRE_EQ("value_h", cmdline.get_option< string_option >("h_long")); + ATF_REQUIRE_EQ("default_i", cmdline.get_option< string_option >("i_long")); + ATF_REQUIRE_EQ("value_j", cmdline.get_option< string_option >("j_long")); + ATF_REQUIRE_EQ(2, cmdline.arguments().size()); + ATF_REQUIRE_EQ("arg1", cmdline.arguments()[0]); + ATF_REQUIRE_EQ("arg2", cmdline.arguments()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_options__multi); +ATF_TEST_CASE_BODY(some_options__multi) +{ + const int argc = 9; + const char* const argv[] = { + "progname", + "-a1", + "-bvalue1", + "-a2", + "--a_long=3", + "-bvalue2", + "--b_long=value3", + "arg1", "arg2", NULL, + }; + const int_option a('a', "a_long", "Description", "arg"); + const string_option b('b', "b_long", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&a); + options.push_back(&b); + const parsed_cmdline cmdline = parse(argc, argv, options); + + { + ATF_REQUIRE_EQ(3, cmdline.get_option< int_option >("a_long")); + const std::vector< int > multi = + cmdline.get_multi_option< int_option >("a_long"); + ATF_REQUIRE_EQ(3, multi.size()); + ATF_REQUIRE_EQ(1, multi[0]); + ATF_REQUIRE_EQ(2, multi[1]); + ATF_REQUIRE_EQ(3, multi[2]); + } + + { + ATF_REQUIRE_EQ("value3", cmdline.get_option< string_option >("b_long")); + const std::vector< std::string > multi = + cmdline.get_multi_option< string_option >("b_long"); + ATF_REQUIRE_EQ(3, multi.size()); + ATF_REQUIRE_EQ("value1", multi[0]); + ATF_REQUIRE_EQ("value2", multi[1]); + ATF_REQUIRE_EQ("value3", multi[2]); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommands); +ATF_TEST_CASE_BODY(subcommands) +{ + const int argc = 5; + const char* const argv[] = {"progname", "--flag1", "subcommand", + "--flag2", "arg", NULL}; + const bool_option flag1("flag1", ""); + std::vector< const base_option* > options; + options.push_back(&flag1); + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE( cmdline.has_option("flag1")); + ATF_REQUIRE(!cmdline.has_option("flag2")); + ATF_REQUIRE_EQ(3, cmdline.arguments().size()); + ATF_REQUIRE_EQ("subcommand", cmdline.arguments()[0]); + ATF_REQUIRE_EQ("--flag2", cmdline.arguments()[1]); + ATF_REQUIRE_EQ("arg", cmdline.arguments()[2]); + + const bool_option flag2("flag2", ""); + std::vector< const base_option* > options2; + options2.push_back(&flag2); + const parsed_cmdline cmdline2 = parse(cmdline.arguments(), options2); + + ATF_REQUIRE(!cmdline2.has_option("flag1")); + ATF_REQUIRE( cmdline2.has_option("flag2")); + ATF_REQUIRE_EQ(1, cmdline2.arguments().size()); + ATF_REQUIRE_EQ("arg", cmdline2.arguments()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_option_argument_error__short); +ATF_TEST_CASE_BODY(missing_option_argument_error__short) +{ + const int argc = 3; + const char* const argv[] = {"progname", "-a3", "-b", NULL}; + const string_option flag1('a', "flag1", "Description", "arg"); + const string_option flag2('b', "flag2", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + + try { + parse(argc, argv, options); + fail("missing_option_argument_error not raised"); + } catch (const cmdline::missing_option_argument_error& e) { + ATF_REQUIRE_EQ("-b", e.option()); + } catch (const cmdline::unknown_option_error& e) { + if (is_getopt_long_pluscolon_broken()) + expect_fail("Your getopt_long is broken"); + fail("Got unknown_option_error instead of " + "missing_option_argument_error"); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_option_argument_error__shortblock); +ATF_TEST_CASE_BODY(missing_option_argument_error__shortblock) +{ + const int argc = 3; + const char* const argv[] = {"progname", "-ab3", "-ac", NULL}; + const bool_option flag1('a', "flag1", "Description"); + const string_option flag2('b', "flag2", "Description", "arg"); + const string_option flag3('c', "flag2", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + options.push_back(&flag3); + + try { + parse(argc, argv, options); + fail("missing_option_argument_error not raised"); + } catch (const cmdline::missing_option_argument_error& e) { + ATF_REQUIRE_EQ("-c", e.option()); + } catch (const cmdline::unknown_option_error& e) { + if (is_getopt_long_pluscolon_broken()) + expect_fail("Your getopt_long is broken"); + fail("Got unknown_option_error instead of " + "missing_option_argument_error"); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(missing_option_argument_error__long); +ATF_TEST_CASE_BODY(missing_option_argument_error__long) +{ + const int argc = 3; + const char* const argv[] = {"progname", "--flag1=a", "--flag2", NULL}; + const string_option flag1("flag1", "Description", "arg"); + const string_option flag2("flag2", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + + try { + parse(argc, argv, options); + fail("missing_option_argument_error not raised"); + } catch (const cmdline::missing_option_argument_error& e) { + ATF_REQUIRE_EQ("--flag2", e.option()); + } catch (const cmdline::unknown_option_error& e) { + if (is_getopt_long_pluscolon_broken()) + expect_fail("Your getopt_long is broken"); + fail("Got unknown_option_error instead of " + "missing_option_argument_error"); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_option_error__short); +ATF_TEST_CASE_BODY(unknown_option_error__short) +{ + const int argc = 3; + const char* const argv[] = {"progname", "-a", "-b", NULL}; + const bool_option flag1('a', "flag1", "Description"); + std::vector< const base_option* > options; + options.push_back(&flag1); + + try { + parse(argc, argv, options); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("-b", e.option()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_option_error__shortblock); +ATF_TEST_CASE_BODY(unknown_option_error__shortblock) +{ + const int argc = 3; + const char* const argv[] = {"progname", "-a", "-bdc", NULL}; + const bool_option flag1('a', "flag1", "Description"); + const bool_option flag2('b', "flag2", "Description"); + const bool_option flag3('c', "flag3", "Description"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + options.push_back(&flag3); + + try { + parse(argc, argv, options); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("-d", e.option()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_option_error__long); +ATF_TEST_CASE_BODY(unknown_option_error__long) +{ + const int argc = 3; + const char* const argv[] = {"progname", "--flag1=a", "--flag2", NULL}; + const string_option flag1("flag1", "Description", "arg"); + std::vector< const base_option* > options; + options.push_back(&flag1); + + try { + parse(argc, argv, options); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("--flag2", e.option()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_plus_option_error); +ATF_TEST_CASE_BODY(unknown_plus_option_error) +{ + const int argc = 2; + const char* const argv[] = {"progname", "-+", NULL}; + const cmdline::options_vector options; + + try { + parse(argc, argv, options); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("-+", e.option()); + } catch (const cmdline::missing_option_argument_error& e) { + fail("Looks like getopt_long thinks a + option is defined and it " + "even requires an argument"); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(option_types); +ATF_TEST_CASE_BODY(option_types) +{ + const int argc = 3; + const char* const argv[] = {"progname", "--flag1=a", "--flag2=one", NULL}; + const string_option flag1("flag1", "The flag1", "arg"); + const mock_option flag2("flag2"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + + const parsed_cmdline cmdline = parse(argc, argv, options); + + ATF_REQUIRE(cmdline.has_option("flag1")); + ATF_REQUIRE(cmdline.has_option("flag2")); + ATF_REQUIRE_EQ("a", cmdline.get_option< string_option >("flag1")); + ATF_REQUIRE_EQ(1, cmdline.get_option< mock_option >("flag2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(option_validation_error); +ATF_TEST_CASE_BODY(option_validation_error) +{ + const int argc = 3; + const char* const argv[] = {"progname", "--flag1=zero", "--flag2=foo", + NULL}; + const mock_option flag1("flag1"); + const mock_option flag2("flag2"); + std::vector< const base_option* > options; + options.push_back(&flag1); + options.push_back(&flag2); + + try { + parse(argc, argv, options); + fail("option_argument_value_error not raised"); + } catch (const cmdline::option_argument_value_error& e) { + ATF_REQUIRE_EQ("--flag2", e.option()); + ATF_REQUIRE_EQ("foo", e.argument()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(silent_errors); +ATF_TEST_CASE_BODY(silent_errors) +{ + const int argc = 2; + const char* const argv[] = {"progname", "-h", NULL}; + cmdline::options_vector options; + + try { + std::pair< int, int > oldfds = mock_stdfds("output.txt"); + try { + parse(argc, argv, options); + } catch (...) { + restore_stdfds(oldfds); + throw; + } + restore_stdfds(oldfds); + fail("unknown_option_error not raised"); + } catch (const cmdline::unknown_option_error& e) { + ATF_REQUIRE_EQ("-h", e.option()); + } + + std::ifstream input("output.txt"); + ATF_REQUIRE(input); + + bool has_output = false; + std::string line; + while (std::getline(input, line).good()) { + std::cout << line << '\n'; + has_output = true; + } + + if (has_output) + fail("getopt_long printed messages on stdout/stderr by itself"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, progname__no_options); + ATF_ADD_TEST_CASE(tcs, progname__some_options); + ATF_ADD_TEST_CASE(tcs, some_args__no_options); + ATF_ADD_TEST_CASE(tcs, some_args__some_options); + ATF_ADD_TEST_CASE(tcs, some_options__all_known); + ATF_ADD_TEST_CASE(tcs, some_options__multi); + ATF_ADD_TEST_CASE(tcs, subcommands); + ATF_ADD_TEST_CASE(tcs, missing_option_argument_error__short); + ATF_ADD_TEST_CASE(tcs, missing_option_argument_error__shortblock); + ATF_ADD_TEST_CASE(tcs, missing_option_argument_error__long); + ATF_ADD_TEST_CASE(tcs, unknown_option_error__short); + ATF_ADD_TEST_CASE(tcs, unknown_option_error__shortblock); + ATF_ADD_TEST_CASE(tcs, unknown_option_error__long); + ATF_ADD_TEST_CASE(tcs, unknown_plus_option_error); + ATF_ADD_TEST_CASE(tcs, option_types); + ATF_ADD_TEST_CASE(tcs, option_validation_error); + ATF_ADD_TEST_CASE(tcs, silent_errors); +} diff --git a/utils/cmdline/ui.cpp b/utils/cmdline/ui.cpp new file mode 100644 index 000000000000..a682360a4259 --- /dev/null +++ b/utils/cmdline/ui.cpp @@ -0,0 +1,276 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/ui.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include +#include + +#if defined(HAVE_TERMIOS_H) +# include +#endif +#include +} + +#include + +#include "utils/cmdline/globals.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/text/operations.ipp" +#include "utils/text/table.hpp" + +namespace cmdline = utils::cmdline; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +/// Destructor for the class. +cmdline::ui::~ui(void) +{ +} + + +/// Writes a single line to stderr. +/// +/// The written line is printed as is, without being wrapped to fit within the +/// screen width. If the caller wants to print more than one line, it shall +/// invoke this function once per line. +/// +/// \param message The line to print. Should not include a trailing newline +/// character. +/// \param newline Whether to append a newline to the message or not. +void +cmdline::ui::err(const std::string& message, const bool newline) +{ + LI(F("stderr: %s") % message); + if (newline) + std::cerr << message << "\n"; + else { + std::cerr << message; + std::cerr.flush(); + } +} + + +/// Writes a single line to stdout. +/// +/// The written line is printed as is, without being wrapped to fit within the +/// screen width. If the caller wants to print more than one line, it shall +/// invoke this function once per line. +/// +/// \param message The line to print. Should not include a trailing newline +/// character. +/// \param newline Whether to append a newline to the message or not. +void +cmdline::ui::out(const std::string& message, const bool newline) +{ + LI(F("stdout: %s") % message); + if (newline) + std::cout << message << "\n"; + else { + std::cout << message; + std::cout.flush(); + } +} + + +/// Queries the width of the screen. +/// +/// This information comes first from the COLUMNS environment variable. If not +/// present or invalid, and if the stdout of the current process is connected to +/// a terminal the width is deduced from the terminal itself. Ultimately, if +/// all fails, none is returned. This function shall not raise any errors. +/// +/// Be aware that the results of this query are cached during execution. +/// Subsequent calls to this function will always return the same value even if +/// the terminal size has actually changed. +/// +/// \todo Install a signal handler for SIGWINCH so that we can readjust our +/// knowledge of the terminal width when the user resizes the window. +/// +/// \return The width of the screen if it was possible to determine it, or none +/// otherwise. +optional< std::size_t > +cmdline::ui::screen_width(void) const +{ + static bool done = false; + static optional< std::size_t > width = none; + + if (!done) { + const optional< std::string > columns = utils::getenv("COLUMNS"); + if (columns) { + if (columns.get().length() > 0) { + try { + width = utils::make_optional( + utils::text::to_type< std::size_t >(columns.get())); + } catch (const utils::text::value_error& e) { + LD(F("Ignoring invalid value in COLUMNS variable: %s") % + e.what()); + } + } + } + if (!width) { + struct ::winsize ws; + if (::ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1) + width = optional< std::size_t >(ws.ws_col); + } + + if (width && width.get() >= 80) + width.get() -= 5; + + done = true; + } + + return width; +} + + +/// Writes a line to stdout. +/// +/// The line is wrapped to fit on screen. +/// +/// \param message The line to print, without the trailing newline character. +void +cmdline::ui::out_wrap(const std::string& message) +{ + const optional< std::size_t > max_width = screen_width(); + if (max_width) { + const std::vector< std::string > lines = text::refill( + message, max_width.get()); + for (std::vector< std::string >::const_iterator iter = lines.begin(); + iter != lines.end(); iter++) + out(*iter); + } else + out(message); +} + + +/// Writes a line to stdout with a leading tag. +/// +/// If the line does not fit on the current screen width, the line is broken +/// into pieces and the tag is repeated on every line. +/// +/// \param tag The leading line tag. +/// \param message The message to be printed, without the trailing newline +/// character. +/// \param repeat If true, print the tag on every line; otherwise, indent the +/// text of all lines to match the width of the tag on the first line. +void +cmdline::ui::out_tag_wrap(const std::string& tag, const std::string& message, + const bool repeat) +{ + const optional< std::size_t > max_width = screen_width(); + if (max_width && max_width.get() > tag.length()) { + const std::vector< std::string > lines = text::refill( + message, max_width.get() - tag.length()); + for (std::vector< std::string >::const_iterator iter = lines.begin(); + iter != lines.end(); iter++) { + if (repeat || iter == lines.begin()) + out(F("%s%s") % tag % *iter); + else + out(F("%s%s") % std::string(tag.length(), ' ') % *iter); + } + } else { + out(F("%s%s") % tag % message); + } +} + + +/// Writes a table to stdout. +/// +/// \param table The table to write. +/// \param formatter The table formatter to use to convert the table to a +/// console representation. +/// \param prefix Text to prepend to all the lines of the output table. +void +cmdline::ui::out_table(const text::table& table, + text::table_formatter formatter, + const std::string& prefix) +{ + if (table.empty()) + return; + + const optional< std::size_t > max_width = screen_width(); + if (max_width) + formatter.set_table_width(max_width.get() - prefix.length()); + + const std::vector< std::string > lines = formatter.format(table); + for (std::vector< std::string >::const_iterator iter = lines.begin(); + iter != lines.end(); ++iter) + out(prefix + *iter); +} + + +/// Formats and prints an error message. +/// +/// \param ui_ The user interface object used to print the message. +/// \param message The message to print. Should not end with a newline +/// character. +void +cmdline::print_error(ui* ui_, const std::string& message) +{ + LE(message); + ui_->err(F("%s: E: %s") % cmdline::progname() % message); +} + + +/// Formats and prints an informational message. +/// +/// \param ui_ The user interface object used to print the message. +/// \param message The message to print. Should not end with a newline +/// character. +void +cmdline::print_info(ui* ui_, const std::string& message) +{ + LI(message); + ui_->err(F("%s: I: %s") % cmdline::progname() % message); +} + + +/// Formats and prints a warning message. +/// +/// \param ui_ The user interface object used to print the message. +/// \param message The message to print. Should not end with a newline +/// character. +void +cmdline::print_warning(ui* ui_, const std::string& message) +{ + LW(message); + ui_->err(F("%s: W: %s") % cmdline::progname() % message); +} diff --git a/utils/cmdline/ui.hpp b/utils/cmdline/ui.hpp new file mode 100644 index 000000000000..433bbe903b03 --- /dev/null +++ b/utils/cmdline/ui.hpp @@ -0,0 +1,79 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/ui.hpp +/// Abstractions and utilities to write formatted messages to the console. + +#if !defined(UTILS_CMDLINE_UI_HPP) +#define UTILS_CMDLINE_UI_HPP + +#include "utils/cmdline/ui_fwd.hpp" + +#include +#include + +#include "utils/optional_fwd.hpp" +#include "utils/text/table_fwd.hpp" + +namespace utils { +namespace cmdline { + + +/// Interface to interact with the CLI. +/// +/// The main purpose of this class is to substitute direct usages of stdout and +/// stderr. An instance of this class is passed to every command of a CLI, +/// which allows unit testing and validation of the interaction with the user. +/// +/// This class writes directly to stdout and stderr. For testing purposes, see +/// the utils::cmdline::ui_mock class. +class ui { +public: + virtual ~ui(void); + + virtual void err(const std::string&, const bool = true); + virtual void out(const std::string&, const bool = true); + virtual optional< std::size_t > screen_width(void) const; + + void out_wrap(const std::string&); + void out_tag_wrap(const std::string&, const std::string&, + const bool = true); + void out_table(const utils::text::table&, utils::text::table_formatter, + const std::string&); +}; + + +void print_error(ui*, const std::string&); +void print_info(ui*, const std::string&); +void print_warning(ui*, const std::string&); + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_UI_HPP) diff --git a/utils/cmdline/ui_fwd.hpp b/utils/cmdline/ui_fwd.hpp new file mode 100644 index 000000000000..4417beb1a8e8 --- /dev/null +++ b/utils/cmdline/ui_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/ui_fwd.hpp +/// Forward declarations for utils/cmdline/ui.hpp + +#if !defined(UTILS_CMDLINE_UI_FWD_HPP) +#define UTILS_CMDLINE_UI_FWD_HPP + +namespace utils { +namespace cmdline { + + +class ui; + + +} // namespace cmdline +} // namespace utils + +#endif // !defined(UTILS_CMDLINE_UI_FWD_HPP) diff --git a/utils/cmdline/ui_mock.cpp b/utils/cmdline/ui_mock.cpp new file mode 100644 index 000000000000..b77943cf147b --- /dev/null +++ b/utils/cmdline/ui_mock.cpp @@ -0,0 +1,114 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/ui_mock.hpp" + +#include + +#include "utils/optional.ipp" + +using utils::cmdline::ui_mock; +using utils::none; +using utils::optional; + + +/// Constructs a new mock UI. +/// +/// \param screen_width_ The width of the screen to use for testing purposes. +/// Defaults to 0 to prevent uncontrolled wrapping on our tests. +ui_mock::ui_mock(const std::size_t screen_width_) : + _screen_width(screen_width_) +{ +} + + +/// Writes a line to stderr and records it for further inspection. +/// +/// \param message The line to print and record, without the trailing newline +/// character. +/// \param newline Whether to append a newline to the message or not. +void +ui_mock::err(const std::string& message, const bool newline) +{ + if (newline) + std::cerr << message << "\n"; + else { + std::cerr << message << "\n"; + std::cerr.flush(); + } + _err_log.push_back(message); +} + + +/// Writes a line to stdout and records it for further inspection. +/// +/// \param message The line to print and record, without the trailing newline +/// character. +/// \param newline Whether to append a newline to the message or not. +void +ui_mock::out(const std::string& message, const bool newline) +{ + if (newline) + std::cout << message << "\n"; + else { + std::cout << message << "\n"; + std::cout.flush(); + } + _out_log.push_back(message); +} + + +/// Queries the width of the screen. +/// +/// \return Always none, as we do not want to depend on line wrapping in our +/// tests. +optional< std::size_t > +ui_mock::screen_width(void) const +{ + return _screen_width > 0 ? optional< std::size_t >(_screen_width) : none; +} + + +/// Gets all the lines written to stderr. +/// +/// \return The printed lines. +const std::vector< std::string >& +ui_mock::err_log(void) const +{ + return _err_log; +} + + +/// Gets all the lines written to stdout. +/// +/// \return The printed lines. +const std::vector< std::string >& +ui_mock::out_log(void) const +{ + return _out_log; +} diff --git a/utils/cmdline/ui_mock.hpp b/utils/cmdline/ui_mock.hpp new file mode 100644 index 000000000000..2c37683af7f3 --- /dev/null +++ b/utils/cmdline/ui_mock.hpp @@ -0,0 +1,78 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/cmdline/ui_mock.hpp +/// Provides the utils::cmdline::ui_mock class. +/// +/// This file is only supposed to be included from test program, never from +/// production code. + +#if !defined(UTILS_CMDLINE_UI_MOCK_HPP) +#define UTILS_CMDLINE_UI_MOCK_HPP + +#include +#include +#include + +#include "utils/cmdline/ui.hpp" + +namespace utils { +namespace cmdline { + + +/// Testable interface to interact with the CLI. +/// +/// This class records all writes to stdout and stderr to allow further +/// inspection for testing purposes. +class ui_mock : public ui { + /// Fake width of the screen; if 0, represents none. + std::size_t _screen_width; + + /// Messages sent to stderr. + std::vector< std::string > _err_log; + + /// Messages sent to stdout. + std::vector< std::string > _out_log; + +public: + ui_mock(const std::size_t = 0); + + void err(const std::string&, const bool = true); + void out(const std::string&, const bool = true); + optional< std::size_t > screen_width(void) const; + + const std::vector< std::string >& err_log(void) const; + const std::vector< std::string >& out_log(void) const; +}; + + +} // namespace cmdline +} // namespace utils + + +#endif // !defined(UTILS_CMDLINE_UI_MOCK_HPP) diff --git a/utils/cmdline/ui_test.cpp b/utils/cmdline/ui_test.cpp new file mode 100644 index 000000000000..92c64baf95a3 --- /dev/null +++ b/utils/cmdline/ui_test.cpp @@ -0,0 +1,424 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/cmdline/ui.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include +#include + +#include +#if defined(HAVE_TERMIOS_H) +# include +#endif +#include +} + +#include +#include + +#include + +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/text/table.hpp" + +namespace cmdline = utils::cmdline; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Reopens stdout as a tty and returns its width. +/// +/// \return The width of the tty in columns. If the width is wider than 80, the +/// result is 5 columns narrower to match the screen_width() algorithm. +static std::size_t +reopen_stdout(void) +{ + const int fd = ::open("/dev/tty", O_WRONLY); + if (fd == -1) + ATF_SKIP(F("Cannot open tty for test: %s") % ::strerror(errno)); + struct ::winsize ws; + if (::ioctl(fd, TIOCGWINSZ, &ws) == -1) + ATF_SKIP(F("Cannot determine size of tty: %s") % ::strerror(errno)); + + if (fd != STDOUT_FILENO) { + if (::dup2(fd, STDOUT_FILENO) == -1) + ATF_SKIP(F("Failed to redirect stdout: %s") % ::strerror(errno)); + ::close(fd); + } + + return ws.ws_col >= 80 ? ws.ws_col - 5 : ws.ws_col; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_set__no_tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_set__no_tty) +{ + utils::setenv("COLUMNS", "4321"); + ::close(STDOUT_FILENO); + + cmdline::ui ui; + ATF_REQUIRE_EQ(4321 - 5, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_set__tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_set__tty) +{ + utils::setenv("COLUMNS", "4321"); + (void)reopen_stdout(); + + cmdline::ui ui; + ATF_REQUIRE_EQ(4321 - 5, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_empty__no_tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_empty__no_tty) +{ + utils::setenv("COLUMNS", ""); + ::close(STDOUT_FILENO); + + cmdline::ui ui; + ATF_REQUIRE(!ui.screen_width()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_empty__tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_empty__tty) +{ + utils::setenv("COLUMNS", ""); + const std::size_t columns = reopen_stdout(); + + cmdline::ui ui; + ATF_REQUIRE_EQ(columns, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_invalid__no_tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_invalid__no_tty) +{ + utils::setenv("COLUMNS", "foo bar"); + ::close(STDOUT_FILENO); + + cmdline::ui ui; + ATF_REQUIRE(!ui.screen_width()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__columns_invalid__tty); +ATF_TEST_CASE_BODY(ui__screen_width__columns_invalid__tty) +{ + utils::setenv("COLUMNS", "foo bar"); + const std::size_t columns = reopen_stdout(); + + cmdline::ui ui; + ATF_REQUIRE_EQ(columns, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__tty_is_file); +ATF_TEST_CASE_BODY(ui__screen_width__tty_is_file) +{ + utils::unsetenv("COLUMNS"); + const int fd = ::open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0755); + ATF_REQUIRE(fd != -1); + if (fd != STDOUT_FILENO) { + ATF_REQUIRE(::dup2(fd, STDOUT_FILENO) != -1); + ::close(fd); + } + + cmdline::ui ui; + ATF_REQUIRE(!ui.screen_width()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__screen_width__cached); +ATF_TEST_CASE_BODY(ui__screen_width__cached) +{ + cmdline::ui ui; + + utils::setenv("COLUMNS", "100"); + ATF_REQUIRE_EQ(100 - 5, ui.screen_width().get()); + + utils::setenv("COLUMNS", "80"); + ATF_REQUIRE_EQ(100 - 5, ui.screen_width().get()); + + utils::unsetenv("COLUMNS"); + ATF_REQUIRE_EQ(100 - 5, ui.screen_width().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__err); +ATF_TEST_CASE_BODY(ui__err) +{ + cmdline::ui_mock ui(10); // Keep shorter than message. + ui.err("This is a short message"); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("This is a short message", ui.err_log()[0]); + ATF_REQUIRE(ui.out_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__err__tolerates_newline); +ATF_TEST_CASE_BODY(ui__err__tolerates_newline) +{ + cmdline::ui_mock ui(10); // Keep shorter than message. + ui.err("This is a short message\n"); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("This is a short message\n", ui.err_log()[0]); + ATF_REQUIRE(ui.out_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out); +ATF_TEST_CASE_BODY(ui__out) +{ + cmdline::ui_mock ui(10); // Keep shorter than message. + ui.out("This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("This is a short message", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out__tolerates_newline); +ATF_TEST_CASE_BODY(ui__out__tolerates_newline) +{ + cmdline::ui_mock ui(10); // Keep shorter than message. + ui.out("This is a short message\n"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("This is a short message\n", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_wrap__no_refill); +ATF_TEST_CASE_BODY(ui__out_wrap__no_refill) +{ + cmdline::ui_mock ui(100); + ui.out_wrap("This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("This is a short message", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_wrap__refill); +ATF_TEST_CASE_BODY(ui__out_wrap__refill) +{ + cmdline::ui_mock ui(16); + ui.out_wrap("This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("This is a short", ui.out_log()[0]); + ATF_REQUIRE_EQ("message", ui.out_log()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_tag_wrap__no_refill); +ATF_TEST_CASE_BODY(ui__out_tag_wrap__no_refill) +{ + cmdline::ui_mock ui(100); + ui.out_tag_wrap("Some long tag: ", "This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("Some long tag: This is a short message", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_tag_wrap__refill__repeat); +ATF_TEST_CASE_BODY(ui__out_tag_wrap__refill__repeat) +{ + cmdline::ui_mock ui(32); + ui.out_tag_wrap("Some long tag: ", "This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("Some long tag: This is a short", ui.out_log()[0]); + ATF_REQUIRE_EQ("Some long tag: message", ui.out_log()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_tag_wrap__refill__no_repeat); +ATF_TEST_CASE_BODY(ui__out_tag_wrap__refill__no_repeat) +{ + cmdline::ui_mock ui(32); + ui.out_tag_wrap("Some long tag: ", "This is a short message", false); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("Some long tag: This is a short", ui.out_log()[0]); + ATF_REQUIRE_EQ(" message", ui.out_log()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_tag_wrap__tag_too_long); +ATF_TEST_CASE_BODY(ui__out_tag_wrap__tag_too_long) +{ + cmdline::ui_mock ui(5); + ui.out_tag_wrap("Some long tag: ", "This is a short message"); + ATF_REQUIRE(ui.err_log().empty()); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("Some long tag: This is a short message", ui.out_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_table__empty); +ATF_TEST_CASE_BODY(ui__out_table__empty) +{ + const text::table table(3); + + text::table_formatter formatter; + formatter.set_separator(" | "); + formatter.set_column_width(0, 23); + formatter.set_column_width(1, text::table_formatter::width_refill); + + cmdline::ui_mock ui(52); + ui.out_table(table, formatter, " "); + ATF_REQUIRE(ui.out_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ui__out_table__not_empty); +ATF_TEST_CASE_BODY(ui__out_table__not_empty) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + text::table_formatter formatter; + formatter.set_separator(" | "); + formatter.set_column_width(0, 23); + formatter.set_column_width(1, text::table_formatter::width_refill); + + cmdline::ui_mock ui(52); + ui.out_table(table, formatter, " "); + ATF_REQUIRE_EQ(4, ui.out_log().size()); + ATF_REQUIRE_EQ(" First | Second | Third", + ui.out_log()[0]); + ATF_REQUIRE_EQ(" Fourth with some text | Fifth with | Sixth foo", + ui.out_log()[1]); + ATF_REQUIRE_EQ(" | some more | ", + ui.out_log()[2]); + ATF_REQUIRE_EQ(" | text | ", + ui.out_log()[3]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(print_error); +ATF_TEST_CASE_BODY(print_error) +{ + cmdline::init("error-program"); + cmdline::ui_mock ui; + cmdline::print_error(&ui, "The error."); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("error-program: E: The error.", ui.err_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(print_info); +ATF_TEST_CASE_BODY(print_info) +{ + cmdline::init("info-program"); + cmdline::ui_mock ui; + cmdline::print_info(&ui, "The info."); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("info-program: I: The info.", ui.err_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(print_warning); +ATF_TEST_CASE_BODY(print_warning) +{ + cmdline::init("warning-program"); + cmdline::ui_mock ui; + cmdline::print_warning(&ui, "The warning."); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("warning-program: W: The warning.", ui.err_log()[0]); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_set__no_tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_set__tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_empty__no_tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_empty__tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_invalid__no_tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__columns_invalid__tty); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__tty_is_file); + ATF_ADD_TEST_CASE(tcs, ui__screen_width__cached); + + ATF_ADD_TEST_CASE(tcs, ui__err); + ATF_ADD_TEST_CASE(tcs, ui__err__tolerates_newline); + ATF_ADD_TEST_CASE(tcs, ui__out); + ATF_ADD_TEST_CASE(tcs, ui__out__tolerates_newline); + + ATF_ADD_TEST_CASE(tcs, ui__out_wrap__no_refill); + ATF_ADD_TEST_CASE(tcs, ui__out_wrap__refill); + ATF_ADD_TEST_CASE(tcs, ui__out_tag_wrap__no_refill); + ATF_ADD_TEST_CASE(tcs, ui__out_tag_wrap__refill__repeat); + ATF_ADD_TEST_CASE(tcs, ui__out_tag_wrap__refill__no_repeat); + ATF_ADD_TEST_CASE(tcs, ui__out_tag_wrap__tag_too_long); + ATF_ADD_TEST_CASE(tcs, ui__out_table__empty); + ATF_ADD_TEST_CASE(tcs, ui__out_table__not_empty); + + ATF_ADD_TEST_CASE(tcs, print_error); + ATF_ADD_TEST_CASE(tcs, print_info); + ATF_ADD_TEST_CASE(tcs, print_warning); +} diff --git a/utils/config/Kyuafile b/utils/config/Kyuafile new file mode 100644 index 000000000000..c607a1757275 --- /dev/null +++ b/utils/config/Kyuafile @@ -0,0 +1,10 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="exceptions_test"} +atf_test_program{name="keys_test"} +atf_test_program{name="lua_module_test"} +atf_test_program{name="nodes_test"} +atf_test_program{name="parser_test"} +atf_test_program{name="tree_test"} diff --git a/utils/config/Makefile.am.inc b/utils/config/Makefile.am.inc new file mode 100644 index 000000000000..7c276ec4e798 --- /dev/null +++ b/utils/config/Makefile.am.inc @@ -0,0 +1,87 @@ +# Copyright 2012 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +UTILS_CFLAGS += $(LUTOK_CFLAGS) +UTILS_LIBS += $(LUTOK_LIBS) + +libutils_a_CPPFLAGS += $(LUTOK_CFLAGS) +libutils_a_SOURCES += utils/config/exceptions.cpp +libutils_a_SOURCES += utils/config/exceptions.hpp +libutils_a_SOURCES += utils/config/keys.cpp +libutils_a_SOURCES += utils/config/keys.hpp +libutils_a_SOURCES += utils/config/keys_fwd.hpp +libutils_a_SOURCES += utils/config/lua_module.cpp +libutils_a_SOURCES += utils/config/lua_module.hpp +libutils_a_SOURCES += utils/config/nodes.cpp +libutils_a_SOURCES += utils/config/nodes.hpp +libutils_a_SOURCES += utils/config/nodes.ipp +libutils_a_SOURCES += utils/config/nodes_fwd.hpp +libutils_a_SOURCES += utils/config/parser.cpp +libutils_a_SOURCES += utils/config/parser.hpp +libutils_a_SOURCES += utils/config/parser_fwd.hpp +libutils_a_SOURCES += utils/config/tree.cpp +libutils_a_SOURCES += utils/config/tree.hpp +libutils_a_SOURCES += utils/config/tree.ipp +libutils_a_SOURCES += utils/config/tree_fwd.hpp + +if WITH_ATF +tests_utils_configdir = $(pkgtestsdir)/utils/config + +tests_utils_config_DATA = utils/config/Kyuafile +EXTRA_DIST += $(tests_utils_config_DATA) + +tests_utils_config_PROGRAMS = utils/config/exceptions_test +utils_config_exceptions_test_SOURCES = utils/config/exceptions_test.cpp +utils_config_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/keys_test +utils_config_keys_test_SOURCES = utils/config/keys_test.cpp +utils_config_keys_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_keys_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/lua_module_test +utils_config_lua_module_test_SOURCES = utils/config/lua_module_test.cpp +utils_config_lua_module_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_lua_module_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/nodes_test +utils_config_nodes_test_SOURCES = utils/config/nodes_test.cpp +utils_config_nodes_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_nodes_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/parser_test +utils_config_parser_test_SOURCES = utils/config/parser_test.cpp +utils_config_parser_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_parser_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_config_PROGRAMS += utils/config/tree_test +utils_config_tree_test_SOURCES = utils/config/tree_test.cpp +utils_config_tree_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_config_tree_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/config/exceptions.cpp b/utils/config/exceptions.cpp new file mode 100644 index 000000000000..e9afdf7ea6f7 --- /dev/null +++ b/utils/config/exceptions.cpp @@ -0,0 +1,149 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/exceptions.hpp" + +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" + +namespace config = utils::config; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +config::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +config::error::~error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param key The key that caused the combination conflict. +/// \param format The plain-text error message. +config::bad_combination_error::bad_combination_error( + const detail::tree_key& key, const std::string& format) : + error(F(format.empty() ? "Combination conflict in key '%s'" : format) % + detail::flatten_key(key)) +{ +} + + +/// Destructor for the error. +config::bad_combination_error::~bad_combination_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +config::invalid_key_error::invalid_key_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +config::invalid_key_error::~invalid_key_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param key The unknown key. +/// \param message The plain-text error message. +config::invalid_key_value::invalid_key_value(const detail::tree_key& key, + const std::string& message) : + error(F("Invalid value for property '%s': %s") + % detail::flatten_key(key) % message) +{ +} + + +/// Destructor for the error. +config::invalid_key_value::~invalid_key_value(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +config::syntax_error::syntax_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +config::syntax_error::~syntax_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param key The unknown key. +/// \param format The message for the error. Must include a single "%s" +/// placedholder, which will be replaced by the key itself. +config::unknown_key_error::unknown_key_error(const detail::tree_key& key, + const std::string& format) : + error(F(format.empty() ? "Unknown configuration property '%s'" : format) % + detail::flatten_key(key)) +{ +} + + +/// Destructor for the error. +config::unknown_key_error::~unknown_key_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +config::value_error::value_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +config::value_error::~value_error(void) throw() +{ +} diff --git a/utils/config/exceptions.hpp b/utils/config/exceptions.hpp new file mode 100644 index 000000000000..2096e67f43c8 --- /dev/null +++ b/utils/config/exceptions.hpp @@ -0,0 +1,106 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/exceptions.hpp +/// Exception types raised by the config module. + +#if !defined(UTILS_CONFIG_EXCEPTIONS_HPP) +#define UTILS_CONFIG_EXCEPTIONS_HPP + +#include + +#include "utils/config/keys_fwd.hpp" +#include "utils/config/tree_fwd.hpp" + +namespace utils { +namespace config { + + +/// Base exceptions for config errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Exception denoting that two trees cannot be combined. +class bad_combination_error : public error { +public: + explicit bad_combination_error(const detail::tree_key&, + const std::string&); + ~bad_combination_error(void) throw(); +}; + + +/// Exception denoting that a key was not found within a tree. +class invalid_key_error : public error { +public: + explicit invalid_key_error(const std::string&); + ~invalid_key_error(void) throw(); +}; + + +/// Exception denoting that a key was given an invalid value. +class invalid_key_value : public error { +public: + explicit invalid_key_value(const detail::tree_key&, const std::string&); + ~invalid_key_value(void) throw(); +}; + + +/// Exception denoting that a configuration file is invalid. +class syntax_error : public error { +public: + explicit syntax_error(const std::string&); + ~syntax_error(void) throw(); +}; + + +/// Exception denoting that a key was not found within a tree. +class unknown_key_error : public error { +public: + explicit unknown_key_error(const detail::tree_key&, + const std::string& = ""); + ~unknown_key_error(void) throw(); +}; + + +/// Exception denoting that a value was invalid. +class value_error : public error { +public: + explicit value_error(const std::string&); + ~value_error(void) throw(); +}; + + +} // namespace config +} // namespace utils + + +#endif // !defined(UTILS_CONFIG_EXCEPTIONS_HPP) diff --git a/utils/config/exceptions_test.cpp b/utils/config/exceptions_test.cpp new file mode 100644 index 000000000000..a82fb9ea8f0c --- /dev/null +++ b/utils/config/exceptions_test.cpp @@ -0,0 +1,133 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/exceptions.hpp" + +#include + +#include + +#include "utils/config/tree.ipp" + +namespace config = utils::config; +namespace detail = utils::config::detail; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const config::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bad_combination_error); +ATF_TEST_CASE_BODY(bad_combination_error) +{ + detail::tree_key key; + key.push_back("first"); + key.push_back("second"); + + const config::bad_combination_error e(key, "Failed to combine '%s'"); + ATF_REQUIRE(std::strcmp("Failed to combine 'first.second'", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_key_error); +ATF_TEST_CASE_BODY(invalid_key_error) +{ + const config::invalid_key_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_key_value); +ATF_TEST_CASE_BODY(invalid_key_value) +{ + detail::tree_key key; + key.push_back("1"); + key.push_back("two"); + + const config::invalid_key_value e(key, "foo bar"); + ATF_REQUIRE(std::strcmp("Invalid value for property '1.two': foo bar", + e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_error); +ATF_TEST_CASE_BODY(syntax_error) +{ + const config::syntax_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_key_error__default_message); +ATF_TEST_CASE_BODY(unknown_key_error__default_message) +{ + detail::tree_key key; + key.push_back("1"); + key.push_back("two"); + + const config::unknown_key_error e(key); + ATF_REQUIRE(std::strcmp("Unknown configuration property '1.two'", + e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_key_error__custom_message); +ATF_TEST_CASE_BODY(unknown_key_error__custom_message) +{ + detail::tree_key key; + key.push_back("1"); + key.push_back("two"); + + const config::unknown_key_error e(key, "The test '%s' string"); + ATF_REQUIRE(std::strcmp("The test '1.two' string", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(value_error); +ATF_TEST_CASE_BODY(value_error) +{ + const config::value_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, bad_combination_error); + ATF_ADD_TEST_CASE(tcs, invalid_key_error); + ATF_ADD_TEST_CASE(tcs, invalid_key_value); + ATF_ADD_TEST_CASE(tcs, syntax_error); + ATF_ADD_TEST_CASE(tcs, unknown_key_error__default_message); + ATF_ADD_TEST_CASE(tcs, unknown_key_error__custom_message); + ATF_ADD_TEST_CASE(tcs, value_error); +} diff --git a/utils/config/keys.cpp b/utils/config/keys.cpp new file mode 100644 index 000000000000..574eee14dcd2 --- /dev/null +++ b/utils/config/keys.cpp @@ -0,0 +1,70 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/tree.ipp" + +#include "utils/config/exceptions.hpp" +#include "utils/format/macros.hpp" +#include "utils/text/operations.hpp" + +namespace config = utils::config; +namespace text = utils::text; + + +/// Converts a key to its textual representation. +/// +/// \param key The key to convert. +/// +/// \return a flattened representation of \p key, "."-joined. +std::string +utils::config::detail::flatten_key(const tree_key& key) +{ + PRE(!key.empty()); + return text::join(key, "."); +} + + +/// Parses and validates a textual key. +/// +/// \param str The key to process in dotted notation. +/// +/// \return The tokenized key if valid. +/// +/// \throw invalid_key_error If the input key is empty or invalid for any other +/// reason. Invalid does NOT mean unknown though. +utils::config::detail::tree_key +utils::config::detail::parse_key(const std::string& str) +{ + const tree_key key = text::split(str, '.'); + if (key.empty()) + throw invalid_key_error("Empty key"); + for (tree_key::const_iterator iter = key.begin(); iter != key.end(); iter++) + if ((*iter).empty()) + throw invalid_key_error(F("Empty component in key '%s'") % str); + return key; +} diff --git a/utils/config/keys.hpp b/utils/config/keys.hpp new file mode 100644 index 000000000000..ad258d69fc08 --- /dev/null +++ b/utils/config/keys.hpp @@ -0,0 +1,52 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/keys.hpp +/// Representation and manipulation of tree keys. + +#if !defined(UTILS_CONFIG_KEYS_HPP) +#define UTILS_CONFIG_KEYS_HPP + +#include "utils/config/keys_fwd.hpp" + +#include + +namespace utils { +namespace config { +namespace detail { + + +std::string flatten_key(const tree_key&); +tree_key parse_key(const std::string&); + + +} // namespace detail +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_KEYS_HPP) diff --git a/utils/config/keys_fwd.hpp b/utils/config/keys_fwd.hpp new file mode 100644 index 000000000000..101272698b65 --- /dev/null +++ b/utils/config/keys_fwd.hpp @@ -0,0 +1,51 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/keys_fwd.hpp +/// Forward declarations for utils/config/keys.hpp + +#if !defined(UTILS_CONFIG_KEYS_FWD_HPP) +#define UTILS_CONFIG_KEYS_FWD_HPP + +#include +#include + +namespace utils { +namespace config { +namespace detail { + + +/// Representation of a valid, tokenized key. +typedef std::vector< std::string > tree_key; + + +} // namespace detail +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_KEYS_FWD_HPP) diff --git a/utils/config/keys_test.cpp b/utils/config/keys_test.cpp new file mode 100644 index 000000000000..dc30f0fc8806 --- /dev/null +++ b/utils/config/keys_test.cpp @@ -0,0 +1,114 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/keys.hpp" + +#include + +#include "utils/config/exceptions.hpp" + +namespace config = utils::config; + + +ATF_TEST_CASE_WITHOUT_HEAD(flatten_key__one); +ATF_TEST_CASE_BODY(flatten_key__one) +{ + config::detail::tree_key key; + key.push_back("foo"); + ATF_REQUIRE_EQ("foo", config::detail::flatten_key(key)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(flatten_key__many); +ATF_TEST_CASE_BODY(flatten_key__many) +{ + config::detail::tree_key key; + key.push_back("foo"); + key.push_back("1"); + key.push_back("bar"); + ATF_REQUIRE_EQ("foo.1.bar", config::detail::flatten_key(key)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_key__one); +ATF_TEST_CASE_BODY(parse_key__one) +{ + config::detail::tree_key exp_key; + exp_key.push_back("one"); + ATF_REQUIRE(exp_key == config::detail::parse_key("one")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_key__many); +ATF_TEST_CASE_BODY(parse_key__many) +{ + config::detail::tree_key exp_key; + exp_key.push_back("one"); + exp_key.push_back("2"); + exp_key.push_back("foo"); + ATF_REQUIRE(exp_key == config::detail::parse_key("one.2.foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_key__empty_key); +ATF_TEST_CASE_BODY(parse_key__empty_key) +{ + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty key", + config::detail::parse_key("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_key__empty_component); +ATF_TEST_CASE_BODY(parse_key__empty_component) +{ + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty component in key '.'", + config::detail::parse_key(".")); + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty component in key 'a.'", + config::detail::parse_key("a.")); + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty component in key '.b'", + config::detail::parse_key(".b")); + ATF_REQUIRE_THROW_RE(config::invalid_key_error, + "Empty component in key 'a..b'", + config::detail::parse_key("a..b")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, flatten_key__one); + ATF_ADD_TEST_CASE(tcs, flatten_key__many); + + ATF_ADD_TEST_CASE(tcs, parse_key__one); + ATF_ADD_TEST_CASE(tcs, parse_key__many); + ATF_ADD_TEST_CASE(tcs, parse_key__empty_key); + ATF_ADD_TEST_CASE(tcs, parse_key__empty_component); +} diff --git a/utils/config/lua_module.cpp b/utils/config/lua_module.cpp new file mode 100644 index 000000000000..891f07302e0a --- /dev/null +++ b/utils/config/lua_module.cpp @@ -0,0 +1,282 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/lua_module.hpp" + +#include +#include + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/config/tree.ipp" + +namespace config = utils::config; +namespace detail = utils::config::detail; + + +namespace { + + +/// Gets the tree singleton stored in the Lua state. +/// +/// \param state The Lua state. The registry must contain a key named +/// "tree" with a pointer to the singleton. +/// +/// \return A reference to the tree associated with the Lua state. +/// +/// \throw syntax_error If the tree cannot be located. +config::tree& +get_global_tree(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + state.push_value(lutok::registry_index); + state.push_string("tree"); + state.get_table(-2); + if (state.is_nil(-1)) + throw config::syntax_error("Cannot find tree singleton; global state " + "corrupted?"); + config::tree& tree = **state.to_userdata< config::tree* >(-1); + state.pop(1); + return tree; +} + + +/// Gets a fully-qualified tree key from the state. +/// +/// \param state The Lua state. +/// \param table_index An index to the Lua stack pointing to the table being +/// accessed. If this table contains a tree_key metadata property, this is +/// considered to be the prefix of the tree key. +/// \param field_index An index to the Lua stack pointing to the entry +/// containing the name of the field being indexed. +/// +/// \return A dotted key. +/// +/// \throw invalid_key_error If the name of the key is invalid. +static std::string +get_tree_key(lutok::state& state, const int table_index, const int field_index) +{ + PRE(state.is_string(field_index)); + const std::string field = state.to_string(field_index); + if (!field.empty() && field[0] == '_') + throw config::invalid_key_error( + F("Configuration key cannot have an underscore as a prefix; " + "found %s") % field); + + std::string tree_key; + if (state.get_metafield(table_index, "tree_key")) { + tree_key = state.to_string(-1) + "." + state.to_string(field_index - 1); + state.pop(1); + } else + tree_key = state.to_string(field_index); + return tree_key; +} + + +static int redirect_newindex(lutok::state&); +static int redirect_index(lutok::state&); + + +/// Creates a table for a new configuration inner node. +/// +/// \post state(-1) Contains the new table. +/// +/// \param state The Lua state in which to push the table. +/// \param tree_key The key to which the new table corresponds. +static void +new_table_for_key(lutok::state& state, const std::string& tree_key) +{ + state.new_table(); + { + state.new_table(); + { + state.push_string("__index"); + state.push_cxx_function(redirect_index); + state.set_table(-3); + + state.push_string("__newindex"); + state.push_cxx_function(redirect_newindex); + state.set_table(-3); + + state.push_string("tree_key"); + state.push_string(tree_key); + state.set_table(-3); + } + state.set_metatable(-2); + } +} + + +/// Sets the value of an configuration node. +/// +/// \pre state(-3) The table to index. If this is not _G, then the table +/// metadata must contain a tree_key property describing the path to +/// current level. +/// \pre state(-2) The field to index into the table. Must be a string. +/// \pre state(-1) The value to set the indexed table field to. +/// +/// \param state The Lua state in which to operate. +/// +/// \return The number of result values on the Lua stack; always 0. +/// +/// \throw invalid_key_error If the provided key is invalid. +/// \throw unknown_key_error If the key cannot be located. +/// \throw value_error If the value has an unsupported type or cannot be +/// set on the key, or if the input table or index are invalid. +static int +redirect_newindex(lutok::state& state) +{ + if (!state.is_table(-3)) + throw config::value_error("Indexed object is not a table"); + if (!state.is_string(-2)) + throw config::value_error("Invalid field in configuration object " + "reference; must be a string"); + + const std::string dotted_key = get_tree_key(state, -3, -2); + try { + config::tree& tree = get_global_tree(state); + tree.set_lua(dotted_key, state, -1); + } catch (const config::value_error& e) { + throw config::invalid_key_value(detail::parse_key(dotted_key), + e.what()); + } + + // Now really set the key in the Lua table, but prevent direct accesses from + // the user by prefixing it. We do this to ensure that re-setting the same + // key of the tree results in a call to __newindex instead of __index. + state.push_string("_" + state.to_string(-2)); + state.push_value(-2); + state.raw_set(-5); + + return 0; +} + + +/// Indexes a configuration node. +/// +/// \pre state(-3) The table to index. If this is not _G, then the table +/// metadata must contain a tree_key property describing the path to +/// current level. If the field does not exist, a new table is created. +/// \pre state(-1) The field to index into the table. Must be a string. +/// +/// \param state The Lua state in which to operate. +/// +/// \return The number of result values on the Lua stack; always 1. +/// +/// \throw value_error If the input table or index are invalid. +static int +redirect_index(lutok::state& state) +{ + if (!state.is_table(-2)) + throw config::value_error("Indexed object is not a table"); + if (!state.is_string(-1)) + throw config::value_error("Invalid field in configuration object " + "reference; must be a string"); + + // Query if the key has already been set by a call to redirect_newindex. + state.push_string("_" + state.to_string(-1)); + state.raw_get(-3); + if (!state.is_nil(-1)) + return 1; + state.pop(1); + + state.push_value(-1); // Duplicate the field name. + state.raw_get(-3); // Get table[field] to see if it's defined. + if (state.is_nil(-1)) { + state.pop(1); + + // The stack is now the same as when we entered the function, but we + // know that the field is undefined and thus have to create a new + // configuration table. + INV(state.is_table(-2)); + INV(state.is_string(-1)); + + const config::tree& tree = get_global_tree(state); + const std::string tree_key = get_tree_key(state, -2, -1); + if (tree.is_set(tree_key)) { + // Publish the pre-recorded value in the tree to the Lua state, + // instead of considering this table key a new inner node. + tree.push_lua(tree_key, state); + } else { + state.push_string("_" + state.to_string(-1)); + state.insert(-2); + state.pop(1); + + new_table_for_key(state, tree_key); + + // Duplicate the newly created table and place it deep in the stack + // so that the raw_set below leaves us with the return value of this + // function at the top of the stack. + state.push_value(-1); + state.insert(-4); + + state.raw_set(-3); + state.pop(1); + } + } + return 1; +} + + +} // anonymous namespace + + +/// Install wrappers for globals to set values in the configuration tree. +/// +/// This function installs wrappers to capture all accesses to global variables. +/// Such wrappers redirect the reads and writes to the out_tree, which is the +/// entity that defines what configuration variables exist. +/// +/// \param state The Lua state into which to install the wrappers. +/// \param out_tree The tree with the layout definition and where the +/// configuration settings will be collected. +void +config::redirect(lutok::state& state, tree& out_tree) +{ + lutok::stack_cleaner cleaner(state); + + state.get_global_table(); + { + state.push_string("__index"); + state.push_cxx_function(redirect_index); + state.set_table(-3); + + state.push_string("__newindex"); + state.push_cxx_function(redirect_newindex); + state.set_table(-3); + } + state.set_metatable(-1); + + state.push_value(lutok::registry_index); + state.push_string("tree"); + config::tree** tree = state.new_userdata< config::tree* >(); + *tree = &out_tree; + state.set_table(-3); + state.pop(1); +} diff --git a/utils/config/lua_module.hpp b/utils/config/lua_module.hpp new file mode 100644 index 000000000000..7f0d5d0b4c5f --- /dev/null +++ b/utils/config/lua_module.hpp @@ -0,0 +1,50 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/lua_module.hpp +/// Bindings to expose a configuration tree to Lua. + +#if !defined(UTILS_CONFIG_LUA_MODULE_HPP) +#define UTILS_CONFIG_LUA_MODULE_HPP + +#include + +#include "lutok/state.hpp" +#include "utils/config/tree_fwd.hpp" + +namespace utils { +namespace config { + + +void redirect(lutok::state&, tree&); + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_LUA_MODULE_HPP) diff --git a/utils/config/lua_module_test.cpp b/utils/config/lua_module_test.cpp new file mode 100644 index 000000000000..484d129c4021 --- /dev/null +++ b/utils/config/lua_module_test.cpp @@ -0,0 +1,474 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/lua_module.hpp" + +#include + +#include +#include +#include + +#include "utils/config/tree.ipp" +#include "utils/defs.hpp" + +namespace config = utils::config; + + +namespace { + + +/// Non-native type to use as a leaf node. +struct custom_type { + /// The value recorded in the object. + int value; + + /// Constructs a new object. + /// + /// \param value_ The value to store in the object. + explicit custom_type(const int value_) : + value(value_) + { + } +}; + + +/// Custom implementation of a node type for testing purposes. +class custom_node : public config::typed_leaf_node< custom_type > { +public: + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< custom_node > new_node(new custom_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Pushes the node's value onto the Lua stack. + /// + /// \param state The Lua state onto which to push the value. + void + push_lua(lutok::state& state) const + { + state.push_integer(value().value * 5); + } + + /// Sets the value of the node from an entry in the Lua stack. + /// + /// \param state The Lua state from which to get the value. + /// \param value_index The stack index in which the value resides. + void + set_lua(lutok::state& state, const int value_index) + { + ATF_REQUIRE(state.is_number(value_index)); + set(custom_type(state.to_integer(value_index) * 2)); + } + + /// Sets the value of the node from a raw string representation. + /// + /// \post The test case is marked as failed, as this function is not + /// supposed to be invoked by the lua_module code. + void + set_string(const std::string& /* raw_value */) + { + ATF_FAIL("Should not be used"); + } + + /// Converts the contents of the node to a string. + /// + /// \post The test case is marked as failed, as this function is not + /// supposed to be invoked by the lua_module code. + /// + /// \return Nothing. + std::string + to_string(void) const + { + ATF_FAIL("Should not be used"); + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(top__valid_types); +ATF_TEST_CASE_BODY(top__valid_types) +{ + config::tree tree; + tree.define< config::bool_node >("top_boolean"); + tree.define< config::int_node >("top_integer"); + tree.define< config::string_node >("top_string"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, + "top_boolean = true\n" + "top_integer = 12345\n" + "top_string = 'a foo'\n", + 0, 0, 0); + } + + ATF_REQUIRE_EQ(true, tree.lookup< config::bool_node >("top_boolean")); + ATF_REQUIRE_EQ(12345, tree.lookup< config::int_node >("top_integer")); + ATF_REQUIRE_EQ("a foo", tree.lookup< config::string_node >("top_string")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(top__invalid_types); +ATF_TEST_CASE_BODY(top__invalid_types) +{ + config::tree tree; + tree.define< config::bool_node >("top_boolean"); + tree.define< config::int_node >("top_integer"); + + { + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE( + lutok::error, + "Invalid value for property 'top_boolean': Not a boolean", + lutok::do_string(state, + "top_boolean = true\n" + "top_integer = 8\n" + "top_boolean = 'foo'\n", + 0, 0, 0)); + } + + ATF_REQUIRE_EQ(true, tree.lookup< config::bool_node >("top_boolean")); + ATF_REQUIRE_EQ(8, tree.lookup< config::int_node >("top_integer")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(top__reuse); +ATF_TEST_CASE_BODY(top__reuse) +{ + config::tree tree; + tree.define< config::int_node >("first"); + tree.define< config::int_node >("second"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "first = 100; second = first * 2", 0, 0, 0); + } + + ATF_REQUIRE_EQ(100, tree.lookup< config::int_node >("first")); + ATF_REQUIRE_EQ(200, tree.lookup< config::int_node >("second")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(top__reset); +ATF_TEST_CASE_BODY(top__reset) +{ + config::tree tree; + tree.define< config::int_node >("first"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "first = 100; first = 200", 0, 0, 0); + } + + ATF_REQUIRE_EQ(200, tree.lookup< config::int_node >("first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(top__already_set_on_entry); +ATF_TEST_CASE_BODY(top__already_set_on_entry) +{ + config::tree tree; + tree.define< config::int_node >("first"); + tree.set< config::int_node >("first", 100); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "first = first * 15", 0, 0, 0); + } + + ATF_REQUIRE_EQ(1500, tree.lookup< config::int_node >("first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__valid_types); +ATF_TEST_CASE_BODY(subtree__valid_types) +{ + config::tree tree; + tree.define< config::bool_node >("root.boolean"); + tree.define< config::int_node >("root.a.integer"); + tree.define< config::string_node >("root.string"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, + "root.boolean = true\n" + "root.a.integer = 12345\n" + "root.string = 'a foo'\n", + 0, 0, 0); + } + + ATF_REQUIRE_EQ(true, tree.lookup< config::bool_node >("root.boolean")); + ATF_REQUIRE_EQ(12345, tree.lookup< config::int_node >("root.a.integer")); + ATF_REQUIRE_EQ("a foo", tree.lookup< config::string_node >("root.string")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__reuse); +ATF_TEST_CASE_BODY(subtree__reuse) +{ + config::tree tree; + tree.define< config::int_node >("a.first"); + tree.define< config::int_node >("a.second"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "a.first = 100; a.second = a.first * 2", + 0, 0, 0); + } + + ATF_REQUIRE_EQ(100, tree.lookup< config::int_node >("a.first")); + ATF_REQUIRE_EQ(200, tree.lookup< config::int_node >("a.second")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__reset); +ATF_TEST_CASE_BODY(subtree__reset) +{ + config::tree tree; + tree.define< config::int_node >("a.first"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "a.first = 100; a.first = 200", 0, 0, 0); + } + + ATF_REQUIRE_EQ(200, tree.lookup< config::int_node >("a.first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__already_set_on_entry); +ATF_TEST_CASE_BODY(subtree__already_set_on_entry) +{ + config::tree tree; + tree.define< config::int_node >("a.first"); + tree.set< config::int_node >("a.first", 100); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "a.first = a.first * 15", 0, 0, 0); + } + + ATF_REQUIRE_EQ(1500, tree.lookup< config::int_node >("a.first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subtree__override_inner); +ATF_TEST_CASE_BODY(subtree__override_inner) +{ + config::tree tree; + tree.define_dynamic("root"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "root.test = 'a'", 0, 0, 0); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid value for property 'root'", + lutok::do_string(state, "root = 'b'", 0, 0, 0)); + // Ensure that the previous assignment to 'root' did not cause any + // inconsistencies in the environment that would prevent a new + // assignment from working. + lutok::do_string(state, "root.test2 = 'c'", 0, 0, 0); + } + + ATF_REQUIRE_EQ("a", tree.lookup< config::string_node >("root.test")); + ATF_REQUIRE_EQ("c", tree.lookup< config::string_node >("root.test2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dynamic_subtree__strings); +ATF_TEST_CASE_BODY(dynamic_subtree__strings) +{ + config::tree tree; + tree.define_dynamic("root"); + + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, + "root.key1 = 1234\n" + "root.a.b.key2 = 'foo bar'\n", + 0, 0, 0); + + ATF_REQUIRE_EQ("1234", tree.lookup< config::string_node >("root.key1")); + ATF_REQUIRE_EQ("foo bar", + tree.lookup< config::string_node >("root.a.b.key2")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dynamic_subtree__invalid_types); +ATF_TEST_CASE_BODY(dynamic_subtree__invalid_types) +{ + config::tree tree; + tree.define_dynamic("root"); + + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE(lutok::error, + "Invalid value for property 'root.boolean': " + "Not a string", + lutok::do_string(state, "root.boolean = true", + 0, 0, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, + "Invalid value for property 'root.table': " + "Not a string", + lutok::do_string(state, "root.table = {}", + 0, 0, 0)); + ATF_REQUIRE(!tree.is_set("root.boolean")); + ATF_REQUIRE(!tree.is_set("root.table")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(locals); +ATF_TEST_CASE_BODY(locals) +{ + config::tree tree; + tree.define< config::int_node >("the_key"); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, + "local function generate()\n" + " return 15\n" + "end\n" + "local test_var = 20\n" + "the_key = generate() + test_var\n", + 0, 0, 0); + } + + ATF_REQUIRE_EQ(35, tree.lookup< config::int_node >("the_key")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(custom_node); +ATF_TEST_CASE_BODY(custom_node) +{ + config::tree tree; + tree.define< custom_node >("key1"); + tree.define< custom_node >("key2"); + tree.set< custom_node >("key2", custom_type(10)); + + { + lutok::state state; + config::redirect(state, tree); + lutok::do_string(state, "key1 = 512\n", 0, 0, 0); + lutok::do_string(state, "key2 = key2 * 2\n", 0, 0, 0); + } + + ATF_REQUIRE_EQ(1024, tree.lookup< custom_node >("key1").value); + ATF_REQUIRE_EQ(200, tree.lookup< custom_node >("key2").value); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_key); +ATF_TEST_CASE_BODY(invalid_key) +{ + config::tree tree; + + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE(lutok::error, "Empty component in key 'root.'", + lutok::do_string(state, "root['']['a'] = 12345\n", + 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unknown_key); +ATF_TEST_CASE_BODY(unknown_key) +{ + config::tree tree; + tree.define< config::bool_node >("static.bool"); + + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE(lutok::error, + "Unknown configuration property 'static.int'", + lutok::do_string(state, + "static.int = 12345\n", + 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(value_error); +ATF_TEST_CASE_BODY(value_error) +{ + config::tree tree; + tree.define< config::bool_node >("a.b"); + + lutok::state state; + config::redirect(state, tree); + ATF_REQUIRE_THROW_RE(lutok::error, + "Invalid value for property 'a.b': Not a boolean", + lutok::do_string(state, "a.b = 12345\n", 0, 0, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, + "Invalid value for property 'a': ", + lutok::do_string(state, "a = 1\n", 0, 0, 0)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, top__valid_types); + ATF_ADD_TEST_CASE(tcs, top__invalid_types); + ATF_ADD_TEST_CASE(tcs, top__reuse); + ATF_ADD_TEST_CASE(tcs, top__reset); + ATF_ADD_TEST_CASE(tcs, top__already_set_on_entry); + + ATF_ADD_TEST_CASE(tcs, subtree__valid_types); + ATF_ADD_TEST_CASE(tcs, subtree__reuse); + ATF_ADD_TEST_CASE(tcs, subtree__reset); + ATF_ADD_TEST_CASE(tcs, subtree__already_set_on_entry); + ATF_ADD_TEST_CASE(tcs, subtree__override_inner); + + ATF_ADD_TEST_CASE(tcs, dynamic_subtree__strings); + ATF_ADD_TEST_CASE(tcs, dynamic_subtree__invalid_types); + + ATF_ADD_TEST_CASE(tcs, locals); + ATF_ADD_TEST_CASE(tcs, custom_node); + + ATF_ADD_TEST_CASE(tcs, invalid_key); + ATF_ADD_TEST_CASE(tcs, unknown_key); + ATF_ADD_TEST_CASE(tcs, value_error); +} diff --git a/utils/config/nodes.cpp b/utils/config/nodes.cpp new file mode 100644 index 000000000000..1c6e848daf07 --- /dev/null +++ b/utils/config/nodes.cpp @@ -0,0 +1,589 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/nodes.ipp" + +#include + +#include + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/format/macros.hpp" + +namespace config = utils::config; + + +/// Destructor. +config::detail::base_node::~base_node(void) +{ +} + + +/// Constructor. +/// +/// \param dynamic_ Whether the node is dynamic or not. +config::detail::inner_node::inner_node(const bool dynamic_) : + _dynamic(dynamic_) +{ +} + + +/// Destructor. +config::detail::inner_node::~inner_node(void) +{ + for (children_map::const_iterator iter = _children.begin(); + iter != _children.end(); ++iter) + delete (*iter).second; +} + + +/// Fills the given node with a copy of this node's data. +/// +/// \param node The node to fill. Should be the fresh return value of a +/// deep_copy() operation. +void +config::detail::inner_node::copy_into(inner_node* node) const +{ + node->_dynamic = _dynamic; + for (children_map::const_iterator iter = _children.begin(); + iter != _children.end(); ++iter) { + base_node* new_node = (*iter).second->deep_copy(); + try { + node->_children[(*iter).first] = new_node; + } catch (...) { + delete new_node; + throw; + } + } +} + + +/// Combines two children sets, preferring the keys in the first set only. +/// +/// This operation is not symmetrical on c1 and c2. The caller is responsible +/// for invoking this twice so that the two key sets are combined if they happen +/// to differ. +/// +/// \param key Key to this node. +/// \param c1 First children set. +/// \param c2 First children set. +/// \param [in,out] node The node to combine into. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +void +config::detail::inner_node::combine_children_into( + const tree_key& key, + const children_map& c1, const children_map& c2, + inner_node* node) const +{ + for (children_map::const_iterator iter1 = c1.begin(); + iter1 != c1.end(); ++iter1) { + const std::string& name = (*iter1).first; + + if (node->_children.find(name) != node->_children.end()) { + continue; + } + + std::auto_ptr< base_node > new_node; + + children_map::const_iterator iter2 = c2.find(name); + if (iter2 == c2.end()) { + new_node.reset((*iter1).second->deep_copy()); + } else { + tree_key child_key = key; + child_key.push_back(name); + new_node.reset((*iter1).second->combine(child_key, + (*iter2).second)); + } + + node->_children[name] = new_node.release(); + } +} + + +/// Combines this inner node with another inner node onto a new node. +/// +/// The "dynamic" property is inherited by the new node if either of the two +/// nodes are dynamic. +/// +/// \param key Key to this node. +/// \param other_base The node to combine with. +/// \param [in,out] node The node to combine into. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +void +config::detail::inner_node::combine_into(const tree_key& key, + const base_node* other_base, + inner_node* node) const +{ + try { + const inner_node& other = dynamic_cast< const inner_node& >( + *other_base); + + node->_dynamic = _dynamic || other._dynamic; + + combine_children_into(key, _children, other._children, node); + combine_children_into(key, other._children, _children, node); + } catch (const std::bad_cast& unused_e) { + throw config::bad_combination_error( + key, "'%s' is an inner node in the base tree but a leaf node in " + "the overrides treee"); + } +} + + +/// Finds a node without creating it if not found. +/// +/// This recursive algorithm traverses the tree searching for a particular key. +/// The returned node is constant, so this can only be used for querying +/// purposes. For this reason, this algorithm does not create intermediate +/// nodes if they don't exist (as would be necessary to set a new node). +/// +/// \param key The key to be queried. +/// \param key_pos The current level within the key to be examined. +/// +/// \return A reference to the located node, if successful. +/// +/// \throw unknown_key_error If the provided key is unknown. +const config::detail::base_node* +config::detail::inner_node::lookup_ro(const tree_key& key, + const tree_key::size_type key_pos) const +{ + PRE(key_pos < key.size()); + + const children_map::const_iterator child_iter = _children.find( + key[key_pos]); + if (child_iter == _children.end()) + throw unknown_key_error(key); + + if (key_pos == key.size() - 1) { + return (*child_iter).second; + } else { + PRE(key_pos < key.size() - 1); + try { + const inner_node& child = dynamic_cast< const inner_node& >( + *(*child_iter).second); + return child.lookup_ro(key, key_pos + 1); + } catch (const std::bad_cast& e) { + throw unknown_key_error( + key, "Cannot address incomplete configuration property '%s'"); + } + } +} + + +/// Finds a node and creates it if not found. +/// +/// This recursive algorithm traverses the tree searching for a particular key, +/// creating any intermediate nodes if they do not already exist (for the case +/// of dynamic inner nodes). The returned node is non-constant, so this can be +/// used by the algorithms that set key values. +/// +/// \param key The key to be queried. +/// \param key_pos The current level within the key to be examined. +/// \param new_node A function that returns a new leaf node of the desired +/// type. This is only called if the leaf cannot be found, but it has +/// already been defined. +/// +/// \return A reference to the located node, if successful. +/// +/// \throw invalid_key_value If the resulting node of the search would be an +/// inner node. +/// \throw unknown_key_error If the provided key is unknown. +config::leaf_node* +config::detail::inner_node::lookup_rw(const tree_key& key, + const tree_key::size_type key_pos, + new_node_hook new_node) +{ + PRE(key_pos < key.size()); + + children_map::const_iterator child_iter = _children.find(key[key_pos]); + if (child_iter == _children.end()) { + if (_dynamic) { + base_node* const child = (key_pos == key.size() - 1) ? + static_cast< base_node* >(new_node()) : + static_cast< base_node* >(new dynamic_inner_node()); + _children.insert(children_map::value_type(key[key_pos], child)); + child_iter = _children.find(key[key_pos]); + } else { + throw unknown_key_error(key); + } + } + + if (key_pos == key.size() - 1) { + try { + leaf_node& child = dynamic_cast< leaf_node& >( + *(*child_iter).second); + return &child; + } catch (const std::bad_cast& unused_error) { + throw invalid_key_value(key, "Type mismatch"); + } + } else { + PRE(key_pos < key.size() - 1); + try { + inner_node& child = dynamic_cast< inner_node& >( + *(*child_iter).second); + return child.lookup_rw(key, key_pos + 1, new_node); + } catch (const std::bad_cast& e) { + throw unknown_key_error( + key, "Cannot address incomplete configuration property '%s'"); + } + } +} + + +/// Converts the subtree to a collection of key/value string pairs. +/// +/// \param [out] properties The accumulator for the generated properties. The +/// contents of the map are only extended. +/// \param key The path to the current node. +void +config::detail::inner_node::all_properties(properties_map& properties, + const tree_key& key) const +{ + for (children_map::const_iterator iter = _children.begin(); + iter != _children.end(); ++iter) { + tree_key child_key = key; + child_key.push_back((*iter).first); + try { + leaf_node& child = dynamic_cast< leaf_node& >(*(*iter).second); + if (child.is_set()) + properties[flatten_key(child_key)] = child.to_string(); + } catch (const std::bad_cast& unused_error) { + inner_node& child = dynamic_cast< inner_node& >(*(*iter).second); + child.all_properties(properties, child_key); + } + } +} + + +/// Constructor. +config::detail::static_inner_node::static_inner_node(void) : + inner_node(false) +{ +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::detail::static_inner_node::deep_copy(void) const +{ + std::auto_ptr< inner_node > new_node(new static_inner_node()); + copy_into(new_node.get()); + return new_node.release(); +} + + +/// Combines this node with another one. +/// +/// \param key Key to this node. +/// \param other The node to combine with. +/// +/// \return A new node representing the combination. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +config::detail::base_node* +config::detail::static_inner_node::combine(const tree_key& key, + const base_node* other) const +{ + std::auto_ptr< inner_node > new_node(new static_inner_node()); + combine_into(key, other, new_node.get()); + return new_node.release(); +} + + +/// Registers a key as valid and having a specific type. +/// +/// This method does not raise errors on invalid/unknown keys or other +/// tree-related issues. The reasons is that define() is a method that does not +/// depend on user input: it is intended to pre-populate the tree with a +/// specific structure, and that happens once at coding time. +/// +/// \param key The key to be registered. +/// \param key_pos The current level within the key to be examined. +/// \param new_node A function that returns a new leaf node of the desired +/// type. +void +config::detail::static_inner_node::define(const tree_key& key, + const tree_key::size_type key_pos, + new_node_hook new_node) +{ + PRE(key_pos < key.size()); + + if (key_pos == key.size() - 1) { + PRE_MSG(_children.find(key[key_pos]) == _children.end(), + "Key already defined"); + _children.insert(children_map::value_type(key[key_pos], new_node())); + } else { + PRE(key_pos < key.size() - 1); + const children_map::const_iterator child_iter = _children.find( + key[key_pos]); + + if (child_iter == _children.end()) { + static_inner_node* const child_ptr = new static_inner_node(); + _children.insert(children_map::value_type(key[key_pos], child_ptr)); + child_ptr->define(key, key_pos + 1, new_node); + } else { + try { + static_inner_node& child = dynamic_cast< static_inner_node& >( + *(*child_iter).second); + child.define(key, key_pos + 1, new_node); + } catch (const std::bad_cast& e) { + UNREACHABLE; + } + } + } +} + + +/// Constructor. +config::detail::dynamic_inner_node::dynamic_inner_node(void) : + inner_node(true) +{ +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::detail::dynamic_inner_node::deep_copy(void) const +{ + std::auto_ptr< inner_node > new_node(new dynamic_inner_node()); + copy_into(new_node.get()); + return new_node.release(); +} + + +/// Combines this node with another one. +/// +/// \param key Key to this node. +/// \param other The node to combine with. +/// +/// \return A new node representing the combination. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +config::detail::base_node* +config::detail::dynamic_inner_node::combine(const tree_key& key, + const base_node* other) const +{ + std::auto_ptr< inner_node > new_node(new dynamic_inner_node()); + combine_into(key, other, new_node.get()); + return new_node.release(); +} + + +/// Destructor. +config::leaf_node::~leaf_node(void) +{ +} + + +/// Combines this node with another one. +/// +/// \param key Key to this node. +/// \param other_base The node to combine with. +/// +/// \return A new node representing the combination. +/// +/// \throw bad_combination_error If the two nodes cannot be combined. +config::detail::base_node* +config::leaf_node::combine(const detail::tree_key& key, + const base_node* other_base) const +{ + try { + const leaf_node& other = dynamic_cast< const leaf_node& >(*other_base); + + if (other.is_set()) { + return other.deep_copy(); + } else { + return deep_copy(); + } + } catch (const std::bad_cast& unused_e) { + throw config::bad_combination_error( + key, "'%s' is a leaf node in the base tree but an inner node in " + "the overrides treee"); + } +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::bool_node::deep_copy(void) const +{ + std::auto_ptr< bool_node > new_node(new bool_node()); + new_node->_value = _value; + return new_node.release(); +} + + +/// Pushes the node's value onto the Lua stack. +/// +/// \param state The Lua state onto which to push the value. +void +config::bool_node::push_lua(lutok::state& state) const +{ + state.push_boolean(value()); +} + + +/// Sets the value of the node from an entry in the Lua stack. +/// +/// \param state The Lua state from which to get the value. +/// \param value_index The stack index in which the value resides. +/// +/// \throw value_error If the value in state(value_index) cannot be +/// processed by this node. +void +config::bool_node::set_lua(lutok::state& state, const int value_index) +{ + if (state.is_boolean(value_index)) + set(state.to_boolean(value_index)); + else + throw value_error("Not a boolean"); +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::int_node::deep_copy(void) const +{ + std::auto_ptr< int_node > new_node(new int_node()); + new_node->_value = _value; + return new_node.release(); +} + + +/// Pushes the node's value onto the Lua stack. +/// +/// \param state The Lua state onto which to push the value. +void +config::int_node::push_lua(lutok::state& state) const +{ + state.push_integer(value()); +} + + +/// Sets the value of the node from an entry in the Lua stack. +/// +/// \param state The Lua state from which to get the value. +/// \param value_index The stack index in which the value resides. +/// +/// \throw value_error If the value in state(value_index) cannot be +/// processed by this node. +void +config::int_node::set_lua(lutok::state& state, const int value_index) +{ + if (state.is_number(value_index)) + set(state.to_integer(value_index)); + else + throw value_error("Not an integer"); +} + + +/// Checks a given value for validity. +/// +/// \param new_value The value to validate. +/// +/// \throw value_error If the value is not valid. +void +config::positive_int_node::validate(const value_type& new_value) const +{ + if (new_value <= 0) + throw value_error("Must be a positive integer"); +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::string_node::deep_copy(void) const +{ + std::auto_ptr< string_node > new_node(new string_node()); + new_node->_value = _value; + return new_node.release(); +} + + +/// Pushes the node's value onto the Lua stack. +/// +/// \param state The Lua state onto which to push the value. +void +config::string_node::push_lua(lutok::state& state) const +{ + state.push_string(value()); +} + + +/// Sets the value of the node from an entry in the Lua stack. +/// +/// \param state The Lua state from which to get the value. +/// \param value_index The stack index in which the value resides. +/// +/// \throw value_error If the value in state(value_index) cannot be +/// processed by this node. +void +config::string_node::set_lua(lutok::state& state, const int value_index) +{ + if (state.is_string(value_index)) + set(state.to_string(value_index)); + else + throw value_error("Not a string"); +} + + +/// Copies the node. +/// +/// \return A dynamically-allocated node. +config::detail::base_node* +config::strings_set_node::deep_copy(void) const +{ + std::auto_ptr< strings_set_node > new_node(new strings_set_node()); + new_node->_value = _value; + return new_node.release(); +} + + +/// Converts a single word to the native type. +/// +/// \param raw_value The value to parse. +/// +/// \return The parsed value. +std::string +config::strings_set_node::parse_one(const std::string& raw_value) const +{ + return raw_value; +} diff --git a/utils/config/nodes.hpp b/utils/config/nodes.hpp new file mode 100644 index 000000000000..6b766ff5d8f7 --- /dev/null +++ b/utils/config/nodes.hpp @@ -0,0 +1,272 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/nodes.hpp +/// Representation of tree nodes. + +#if !defined(UTILS_CONFIG_NODES_HPP) +#define UTILS_CONFIG_NODES_HPP + +#include "utils/config/nodes_fwd.hpp" + +#include +#include + +#include + +#include "utils/config/keys_fwd.hpp" +#include "utils/config/nodes_fwd.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.hpp" + +namespace utils { +namespace config { + + +namespace detail { + + +/// Base representation of a node. +/// +/// This abstract class provides the base type for every node in the tree. Due +/// to the dynamic nature of our trees (each leaf being able to hold arbitrary +/// data types), this base type is a necessity. +class base_node : noncopyable { +public: + virtual ~base_node(void) = 0; + + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* deep_copy(void) const = 0; + + /// Combines this node with another one. + /// + /// \param key Key to this node. + /// \param other The node to combine with. + /// + /// \return A new node representing the combination. + /// + /// \throw bad_combination_error If the two nodes cannot be combined. + virtual base_node* combine(const tree_key& key, const base_node* other) + const = 0; +}; + + +} // namespace detail + + +/// Abstract leaf node without any specified type. +/// +/// This base abstract type is necessary to have a common pointer type to which +/// to cast any leaf. We later provide templated derivates of this class, and +/// those cannot act in this manner. +/// +/// It is important to understand that a leaf can exist without actually holding +/// a value. Our trees are "strictly keyed": keys must have been pre-defined +/// before a value can be set on them. This is to ensure that the end user is +/// using valid key names and not making mistakes due to typos, for example. To +/// represent this condition, we define an "empty" key in the tree to denote +/// that the key is valid, yet it has not been set by the user. Only when an +/// explicit set is performed on the key, it gets a value. +class leaf_node : public detail::base_node { +public: + virtual ~leaf_node(void); + + virtual bool is_set(void) const = 0; + + base_node* combine(const detail::tree_key&, const base_node*) const; + + virtual void push_lua(lutok::state&) const = 0; + virtual void set_lua(lutok::state&, const int) = 0; + + virtual void set_string(const std::string&) = 0; + virtual std::string to_string(void) const = 0; +}; + + +/// Base leaf node for a single arbitrary type. +/// +/// This templated leaf node holds a single object of any type. The conversion +/// to/from string representations is undefined, as that depends on the +/// particular type being processed. You should reimplement this class for any +/// type that needs additional processing/validation during conversion. +template< typename ValueType > +class typed_leaf_node : public leaf_node { +public: + /// The type of the value held by this node. + typedef ValueType value_type; + + /// Constructs a new leaf node that contains no value. + typed_leaf_node(void); + + /// Checks whether the node has been set by the user. + bool is_set(void) const; + + /// Gets the value stored in the node. + const value_type& value(void) const; + + /// Gets the read-write value stored in the node. + value_type& value(void); + + /// Sets the value of the node. + void set(const value_type&); + +protected: + /// The value held by this node. + optional< value_type > _value; + +private: + virtual void validate(const value_type&) const; +}; + + +/// Leaf node holding a native type. +/// +/// This templated leaf node holds a native type. The conversion to/from string +/// representations of the value happens by means of iostreams. +template< typename ValueType > +class native_leaf_node : public typed_leaf_node< ValueType > { +public: + void set_string(const std::string&); + std::string to_string(void) const; +}; + + +/// A leaf node that holds a boolean value. +class bool_node : public native_leaf_node< bool > { +public: + virtual base_node* deep_copy(void) const; + + void push_lua(lutok::state&) const; + void set_lua(lutok::state&, const int); +}; + + +/// A leaf node that holds an integer value. +class int_node : public native_leaf_node< int > { +public: + virtual base_node* deep_copy(void) const; + + void push_lua(lutok::state&) const; + void set_lua(lutok::state&, const int); +}; + + +/// A leaf node that holds a positive non-zero integer value. +class positive_int_node : public int_node { + virtual void validate(const value_type&) const; +}; + + +/// A leaf node that holds a string value. +class string_node : public native_leaf_node< std::string > { +public: + virtual base_node* deep_copy(void) const; + + void push_lua(lutok::state&) const; + void set_lua(lutok::state&, const int); +}; + + +/// Base leaf node for a set of native types. +/// +/// This is a base abstract class because there is no generic way to parse a +/// single word in the textual representation of the set to the native value. +template< typename ValueType > +class base_set_node : public leaf_node { +public: + /// The type of the value held by this node. + typedef std::set< ValueType > value_type; + + base_set_node(void); + + /// Checks whether the node has been set by the user. + /// + /// \return True if a value has been set in the node. + bool is_set(void) const; + + /// Gets the value stored in the node. + /// + /// \pre The node must have a value. + /// + /// \return The value in the node. + const value_type& value(void) const; + + /// Gets the read-write value stored in the node. + /// + /// \pre The node must have a value. + /// + /// \return The value in the node. + value_type& value(void); + + /// Sets the value of the node. + void set(const value_type&); + + /// Sets the value of the node from a raw string representation. + void set_string(const std::string&); + + /// Converts the contents of the node to a string. + std::string to_string(void) const; + + /// Pushes the node's value onto the Lua stack. + void push_lua(lutok::state&) const; + + /// Sets the value of the node from an entry in the Lua stack. + void set_lua(lutok::state&, const int); + +protected: + /// The value held by this node. + optional< value_type > _value; + +private: + /// Converts a single word to the native type. + /// + /// \return The parsed value. + /// + /// \throw value_error If the value is invalid. + virtual ValueType parse_one(const std::string&) const = 0; + + virtual void validate(const value_type&) const; +}; + + +/// A leaf node that holds a set of strings. +class strings_set_node : public base_set_node< std::string > { +public: + virtual base_node* deep_copy(void) const; + +private: + std::string parse_one(const std::string&) const; +}; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_NODES_HPP) diff --git a/utils/config/nodes.ipp b/utils/config/nodes.ipp new file mode 100644 index 000000000000..9e0a1228cccd --- /dev/null +++ b/utils/config/nodes.ipp @@ -0,0 +1,408 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/nodes.hpp" + +#if !defined(UTILS_CONFIG_NODES_IPP) +#define UTILS_CONFIG_NODES_IPP + +#include +#include + +#include "utils/config/exceptions.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" +#include "utils/sanity.hpp" + +namespace utils { + + +namespace config { +namespace detail { + + +/// Type of the new_node() family of functions. +typedef base_node* (*new_node_hook)(void); + + +/// Creates a new leaf node of a given type. +/// +/// \tparam NodeType The type of the leaf node to create. +/// +/// \return A pointer to the newly-created node. +template< class NodeType > +base_node* +new_node(void) +{ + return new NodeType(); +} + + +/// Internal node of the tree. +/// +/// This abstract base class provides the mechanism to implement both static and +/// dynamic nodes. Ideally, the implementation would be split in subclasses and +/// this class would not include the knowledge of whether the node is dynamic or +/// not. However, because the static/dynamic difference depends on the leaf +/// types, we need to declare template functions and these cannot be virtual. +class inner_node : public base_node { + /// Whether the node is dynamic or not. + bool _dynamic; + +protected: + /// Type to represent the collection of children of this node. + /// + /// Note that these are one-level keys. They cannot contain dots, and thus + /// is why we use a string rather than a tree_key. + typedef std::map< std::string, base_node* > children_map; + + /// Mapping of keys to values that are descendants of this node. + children_map _children; + + void copy_into(inner_node*) const; + void combine_into(const tree_key&, const base_node*, inner_node*) const; + +private: + void combine_children_into(const tree_key&, + const children_map&, const children_map&, + inner_node*) const; + +public: + inner_node(const bool); + virtual ~inner_node(void) = 0; + + const base_node* lookup_ro(const tree_key&, + const tree_key::size_type) const; + leaf_node* lookup_rw(const tree_key&, const tree_key::size_type, + new_node_hook); + + void all_properties(properties_map&, const tree_key&) const; +}; + + +/// Static internal node of the tree. +/// +/// The direct children of this node must be pre-defined by calls to define(). +/// Attempts to traverse this node and resolve a key that is not a pre-defined +/// children will result in an "unknown key" error. +class static_inner_node : public config::detail::inner_node { +public: + static_inner_node(void); + + virtual base_node* deep_copy(void) const; + virtual base_node* combine(const tree_key&, const base_node*) const; + + void define(const tree_key&, const tree_key::size_type, new_node_hook); +}; + + +/// Dynamic internal node of the tree. +/// +/// The children of this node need not be pre-defined. Attempts to traverse +/// this node and resolve a key will result in such key being created. Any +/// intermediate non-existent nodes of the traversal will be created as dynamic +/// inner nodes as well. +class dynamic_inner_node : public config::detail::inner_node { +public: + virtual base_node* deep_copy(void) const; + virtual base_node* combine(const tree_key&, const base_node*) const; + + dynamic_inner_node(void); +}; + + +} // namespace detail +} // namespace config + + +/// Constructor for a node with an undefined value. +/// +/// This should only be called by the tree's define() method as a way to +/// register a node as known but undefined. The node will then serve as a +/// placeholder for future values. +template< typename ValueType > +config::typed_leaf_node< ValueType >::typed_leaf_node(void) : + _value(none) +{ +} + + +/// Checks whether the node has been set by the user. +/// +/// Nodes of the tree are predefined by the caller to specify the valid +/// types of the leaves. Such predefinition results in the creation of +/// nodes within the tree, but these nodes have not yet been set. +/// Traversing these nodes is invalid and should result in an "unknown key" +/// error. +/// +/// \return True if a value has been set in the node. +template< typename ValueType > +bool +config::typed_leaf_node< ValueType >::is_set(void) const +{ + return static_cast< bool >(_value); +} + + +/// Gets the value stored in the node. +/// +/// \pre The node must have a value. +/// +/// \return The value in the node. +template< typename ValueType > +const typename config::typed_leaf_node< ValueType >::value_type& +config::typed_leaf_node< ValueType >::value(void) const +{ + PRE(is_set()); + return _value.get(); +} + + +/// Gets the read-write value stored in the node. +/// +/// \pre The node must have a value. +/// +/// \return The value in the node. +template< typename ValueType > +typename config::typed_leaf_node< ValueType >::value_type& +config::typed_leaf_node< ValueType >::value(void) +{ + PRE(is_set()); + return _value.get(); +} + + +/// Sets the value of the node. +/// +/// \param value_ The new value to set the node to. +/// +/// \throw value_error If the value is invalid, according to validate(). +template< typename ValueType > +void +config::typed_leaf_node< ValueType >::set(const value_type& value_) +{ + validate(value_); + _value = optional< value_type >(value_); +} + + +/// Checks a given value for validity. +/// +/// This is called internally by the node right before updating the recorded +/// value. This method can be redefined by subclasses. +/// +/// \throw value_error If the value is not valid. +template< typename ValueType > +void +config::typed_leaf_node< ValueType >::validate( + const value_type& /* new_value */) const +{ +} + + +/// Sets the value of the node from a raw string representation. +/// +/// \param raw_value The value to set the node to. +/// +/// \throw value_error If the value is invalid. +template< typename ValueType > +void +config::native_leaf_node< ValueType >::set_string(const std::string& raw_value) +{ + try { + typed_leaf_node< ValueType >::set(text::to_type< ValueType >( + raw_value)); + } catch (const text::value_error& e) { + throw config::value_error(F("Failed to convert string value '%s' to " + "the node's type") % raw_value); + } +} + + +/// Converts the contents of the node to a string. +/// +/// \pre The node must have a value. +/// +/// \return A string representation of the value held by the node. +template< typename ValueType > +std::string +config::native_leaf_node< ValueType >::to_string(void) const +{ + PRE(typed_leaf_node< ValueType >::is_set()); + return F("%s") % typed_leaf_node< ValueType >::value(); +} + + +/// Constructor for a node with an undefined value. +/// +/// This should only be called by the tree's define() method as a way to +/// register a node as known but undefined. The node will then serve as a +/// placeholder for future values. +template< typename ValueType > +config::base_set_node< ValueType >::base_set_node(void) : + _value(none) +{ +} + + +/// Checks whether the node has been set. +/// +/// Remember that a node can exist before holding a value (i.e. when the node +/// has been defined as "known" but not yet set by the user). This function +/// checks whether the node laready holds a value. +/// +/// \return True if a value has been set in the node. +template< typename ValueType > +bool +config::base_set_node< ValueType >::is_set(void) const +{ + return static_cast< bool >(_value); +} + + +/// Gets the value stored in the node. +/// +/// \pre The node must have a value. +/// +/// \return The value in the node. +template< typename ValueType > +const typename config::base_set_node< ValueType >::value_type& +config::base_set_node< ValueType >::value(void) const +{ + PRE(is_set()); + return _value.get(); +} + + +/// Gets the read-write value stored in the node. +/// +/// \pre The node must have a value. +/// +/// \return The value in the node. +template< typename ValueType > +typename config::base_set_node< ValueType >::value_type& +config::base_set_node< ValueType >::value(void) +{ + PRE(is_set()); + return _value.get(); +} + + +/// Sets the value of the node. +/// +/// \param value_ The new value to set the node to. +/// +/// \throw value_error If the value is invalid, according to validate(). +template< typename ValueType > +void +config::base_set_node< ValueType >::set(const value_type& value_) +{ + validate(value_); + _value = optional< value_type >(value_); +} + + +/// Sets the value of the node from a raw string representation. +/// +/// \param raw_value The value to set the node to. +/// +/// \throw value_error If the value is invalid. +template< typename ValueType > +void +config::base_set_node< ValueType >::set_string(const std::string& raw_value) +{ + std::set< ValueType > new_value; + + const std::vector< std::string > words = text::split(raw_value, ' '); + for (std::vector< std::string >::const_iterator iter = words.begin(); + iter != words.end(); ++iter) { + if (!(*iter).empty()) + new_value.insert(parse_one(*iter)); + } + + set(new_value); +} + + +/// Converts the contents of the node to a string. +/// +/// \pre The node must have a value. +/// +/// \return A string representation of the value held by the node. +template< typename ValueType > +std::string +config::base_set_node< ValueType >::to_string(void) const +{ + PRE(is_set()); + return text::join(_value.get(), " "); +} + + +/// Pushes the node's value onto the Lua stack. +template< typename ValueType > +void +config::base_set_node< ValueType >::push_lua(lutok::state& /* state */) const +{ + UNREACHABLE; +} + + +/// Sets the value of the node from an entry in the Lua stack. +/// +/// \throw value_error If the value in state(value_index) cannot be +/// processed by this node. +template< typename ValueType > +void +config::base_set_node< ValueType >::set_lua( + lutok::state& /* state */, + const int /* value_index */) +{ + UNREACHABLE; +} + + +/// Checks a given value for validity. +/// +/// This is called internally by the node right before updating the recorded +/// value. This method can be redefined by subclasses. +/// +/// \throw value_error If the value is not valid. +template< typename ValueType > +void +config::base_set_node< ValueType >::validate( + const value_type& /* new_value */) const +{ +} + + +} // namespace utils + +#endif // !defined(UTILS_CONFIG_NODES_IPP) diff --git a/utils/config/nodes_fwd.hpp b/utils/config/nodes_fwd.hpp new file mode 100644 index 000000000000..b03328e79e95 --- /dev/null +++ b/utils/config/nodes_fwd.hpp @@ -0,0 +1,70 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/nodes_fwd.hpp +/// Forward declarations for utils/config/nodes.hpp + +#if !defined(UTILS_CONFIG_NODES_FWD_HPP) +#define UTILS_CONFIG_NODES_FWD_HPP + +#include +#include + +namespace utils { +namespace config { + + +/// Flat representation of all properties as strings. +typedef std::map< std::string, std::string > properties_map; + + +namespace detail { + + +class base_node; +class static_inner_node; + + +} // namespace detail + + +class leaf_node; +template< typename > class typed_leaf_node; +template< typename > class native_leaf_node; +class bool_node; +class int_node; +class positive_int_node; +class string_node; +template< typename > class base_set_node; +class strings_set_node; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_NODES_FWD_HPP) diff --git a/utils/config/nodes_test.cpp b/utils/config/nodes_test.cpp new file mode 100644 index 000000000000..e762d3aac38c --- /dev/null +++ b/utils/config/nodes_test.cpp @@ -0,0 +1,695 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/nodes.ipp" + +#include + +#include + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/defs.hpp" + +namespace config = utils::config; + + +namespace { + + +/// Typed leaf node that specializes the validate() method. +class validation_node : public config::int_node { + /// Checks a given value for validity against a fake value. + /// + /// \param new_value The value to validate. + /// + /// \throw value_error If the value is not valid. + void + validate(const value_type& new_value) const + { + if (new_value == 12345) + throw config::value_error("Custom validate method"); + } +}; + + +/// Set node that specializes the validate() method. +class set_validation_node : public config::strings_set_node { + /// Checks a given value for validity against a fake value. + /// + /// \param new_value The value to validate. + /// + /// \throw value_error If the value is not valid. + void + validate(const value_type& new_value) const + { + for (value_type::const_iterator iter = new_value.begin(); + iter != new_value.end(); ++iter) + if (*iter == "throw") + throw config::value_error("Custom validate method"); + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__deep_copy); +ATF_TEST_CASE_BODY(bool_node__deep_copy) +{ + config::bool_node node; + node.set(true); + config::detail::base_node* raw_copy = node.deep_copy(); + config::bool_node* copy = static_cast< config::bool_node* >(raw_copy); + ATF_REQUIRE(copy->value()); + copy->set(false); + ATF_REQUIRE(node.value()); + ATF_REQUIRE(!copy->value()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__is_set_and_set); +ATF_TEST_CASE_BODY(bool_node__is_set_and_set) +{ + config::bool_node node; + ATF_REQUIRE(!node.is_set()); + node.set(false); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__value_and_set); +ATF_TEST_CASE_BODY(bool_node__value_and_set) +{ + config::bool_node node; + node.set(false); + ATF_REQUIRE(!node.value()); + node.set(true); + ATF_REQUIRE( node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__push_lua); +ATF_TEST_CASE_BODY(bool_node__push_lua) +{ + lutok::state state; + + config::bool_node node; + node.set(true); + node.push_lua(state); + ATF_REQUIRE(state.is_boolean(-1)); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__set_lua__ok); +ATF_TEST_CASE_BODY(bool_node__set_lua__ok) +{ + lutok::state state; + + config::bool_node node; + state.push_boolean(false); + node.set_lua(state, -1); + state.pop(1); + ATF_REQUIRE(!node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__set_lua__invalid_value); +ATF_TEST_CASE_BODY(bool_node__set_lua__invalid_value) +{ + lutok::state state; + + config::bool_node node; + state.push_string("foo bar"); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__set_string__ok); +ATF_TEST_CASE_BODY(bool_node__set_string__ok) +{ + config::bool_node node; + node.set_string("false"); + ATF_REQUIRE(!node.value()); + node.set_string("true"); + ATF_REQUIRE( node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__set_string__invalid_value); +ATF_TEST_CASE_BODY(bool_node__set_string__invalid_value) +{ + config::bool_node node; + ATF_REQUIRE_THROW(config::value_error, node.set_string("12345")); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bool_node__to_string); +ATF_TEST_CASE_BODY(bool_node__to_string) +{ + config::bool_node node; + node.set(false); + ATF_REQUIRE_EQ("false", node.to_string()); + node.set(true); + ATF_REQUIRE_EQ("true", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__deep_copy); +ATF_TEST_CASE_BODY(int_node__deep_copy) +{ + config::int_node node; + node.set(5); + config::detail::base_node* raw_copy = node.deep_copy(); + config::int_node* copy = static_cast< config::int_node* >(raw_copy); + ATF_REQUIRE_EQ(5, copy->value()); + copy->set(10); + ATF_REQUIRE_EQ(5, node.value()); + ATF_REQUIRE_EQ(10, copy->value()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__is_set_and_set); +ATF_TEST_CASE_BODY(int_node__is_set_and_set) +{ + config::int_node node; + ATF_REQUIRE(!node.is_set()); + node.set(20); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__value_and_set); +ATF_TEST_CASE_BODY(int_node__value_and_set) +{ + config::int_node node; + node.set(20); + ATF_REQUIRE_EQ(20, node.value()); + node.set(0); + ATF_REQUIRE_EQ(0, node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__push_lua); +ATF_TEST_CASE_BODY(int_node__push_lua) +{ + lutok::state state; + + config::int_node node; + node.set(754); + node.push_lua(state); + ATF_REQUIRE(state.is_number(-1)); + ATF_REQUIRE_EQ(754, state.to_integer(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__set_lua__ok); +ATF_TEST_CASE_BODY(int_node__set_lua__ok) +{ + lutok::state state; + + config::int_node node; + state.push_integer(123); + state.push_string("456"); + node.set_lua(state, -2); + ATF_REQUIRE_EQ(123, node.value()); + node.set_lua(state, -1); + ATF_REQUIRE_EQ(456, node.value()); + state.pop(2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__set_lua__invalid_value); +ATF_TEST_CASE_BODY(int_node__set_lua__invalid_value) +{ + lutok::state state; + + config::int_node node; + state.push_boolean(true); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__set_string__ok); +ATF_TEST_CASE_BODY(int_node__set_string__ok) +{ + config::int_node node; + node.set_string("178"); + ATF_REQUIRE_EQ(178, node.value()); + node.set_string("-123"); + ATF_REQUIRE_EQ(-123, node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__set_string__invalid_value); +ATF_TEST_CASE_BODY(int_node__set_string__invalid_value) +{ + config::int_node node; + ATF_REQUIRE_THROW(config::value_error, node.set_string(" 23")); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(int_node__to_string); +ATF_TEST_CASE_BODY(int_node__to_string) +{ + config::int_node node; + node.set(89); + ATF_REQUIRE_EQ("89", node.to_string()); + node.set(-57); + ATF_REQUIRE_EQ("-57", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__deep_copy); +ATF_TEST_CASE_BODY(positive_int_node__deep_copy) +{ + config::positive_int_node node; + node.set(5); + config::detail::base_node* raw_copy = node.deep_copy(); + config::positive_int_node* copy = static_cast< config::positive_int_node* >( + raw_copy); + ATF_REQUIRE_EQ(5, copy->value()); + copy->set(10); + ATF_REQUIRE_EQ(5, node.value()); + ATF_REQUIRE_EQ(10, copy->value()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__is_set_and_set); +ATF_TEST_CASE_BODY(positive_int_node__is_set_and_set) +{ + config::positive_int_node node; + ATF_REQUIRE(!node.is_set()); + node.set(20); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__value_and_set); +ATF_TEST_CASE_BODY(positive_int_node__value_and_set) +{ + config::positive_int_node node; + node.set(20); + ATF_REQUIRE_EQ(20, node.value()); + node.set(1); + ATF_REQUIRE_EQ(1, node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__push_lua); +ATF_TEST_CASE_BODY(positive_int_node__push_lua) +{ + lutok::state state; + + config::positive_int_node node; + node.set(754); + node.push_lua(state); + ATF_REQUIRE(state.is_number(-1)); + ATF_REQUIRE_EQ(754, state.to_integer(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__set_lua__ok); +ATF_TEST_CASE_BODY(positive_int_node__set_lua__ok) +{ + lutok::state state; + + config::positive_int_node node; + state.push_integer(123); + state.push_string("456"); + node.set_lua(state, -2); + ATF_REQUIRE_EQ(123, node.value()); + node.set_lua(state, -1); + ATF_REQUIRE_EQ(456, node.value()); + state.pop(2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__set_lua__invalid_value); +ATF_TEST_CASE_BODY(positive_int_node__set_lua__invalid_value) +{ + lutok::state state; + + config::positive_int_node node; + state.push_boolean(true); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); + state.push_integer(0); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__set_string__ok); +ATF_TEST_CASE_BODY(positive_int_node__set_string__ok) +{ + config::positive_int_node node; + node.set_string("1"); + ATF_REQUIRE_EQ(1, node.value()); + node.set_string("178"); + ATF_REQUIRE_EQ(178, node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__set_string__invalid_value); +ATF_TEST_CASE_BODY(positive_int_node__set_string__invalid_value) +{ + config::positive_int_node node; + ATF_REQUIRE_THROW(config::value_error, node.set_string(" 23")); + ATF_REQUIRE(!node.is_set()); + ATF_REQUIRE_THROW(config::value_error, node.set_string("0")); + ATF_REQUIRE(!node.is_set()); + ATF_REQUIRE_THROW(config::value_error, node.set_string("-5")); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(positive_int_node__to_string); +ATF_TEST_CASE_BODY(positive_int_node__to_string) +{ + config::positive_int_node node; + node.set(89); + ATF_REQUIRE_EQ("89", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__deep_copy); +ATF_TEST_CASE_BODY(string_node__deep_copy) +{ + config::string_node node; + node.set("first"); + config::detail::base_node* raw_copy = node.deep_copy(); + config::string_node* copy = static_cast< config::string_node* >(raw_copy); + ATF_REQUIRE_EQ("first", copy->value()); + copy->set("second"); + ATF_REQUIRE_EQ("first", node.value()); + ATF_REQUIRE_EQ("second", copy->value()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__is_set_and_set); +ATF_TEST_CASE_BODY(string_node__is_set_and_set) +{ + config::string_node node; + ATF_REQUIRE(!node.is_set()); + node.set("foo"); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__value_and_set); +ATF_TEST_CASE_BODY(string_node__value_and_set) +{ + config::string_node node; + node.set("foo"); + ATF_REQUIRE_EQ("foo", node.value()); + node.set(""); + ATF_REQUIRE_EQ("", node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__push_lua); +ATF_TEST_CASE_BODY(string_node__push_lua) +{ + lutok::state state; + + config::string_node node; + node.set("some message"); + node.push_lua(state); + ATF_REQUIRE(state.is_string(-1)); + ATF_REQUIRE_EQ("some message", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__set_lua__ok); +ATF_TEST_CASE_BODY(string_node__set_lua__ok) +{ + lutok::state state; + + config::string_node node; + state.push_string("text 1"); + state.push_integer(231); + node.set_lua(state, -2); + ATF_REQUIRE_EQ("text 1", node.value()); + node.set_lua(state, -1); + ATF_REQUIRE_EQ("231", node.value()); + state.pop(2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__set_lua__invalid_value); +ATF_TEST_CASE_BODY(string_node__set_lua__invalid_value) +{ + lutok::state state; + + config::bool_node node; + state.new_table(); + ATF_REQUIRE_THROW(config::value_error, node.set_lua(state, -1)); + state.pop(1); + ATF_REQUIRE(!node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__set_string); +ATF_TEST_CASE_BODY(string_node__set_string) +{ + config::string_node node; + node.set_string("abcd efgh"); + ATF_REQUIRE_EQ("abcd efgh", node.value()); + node.set_string(" 1234 "); + ATF_REQUIRE_EQ(" 1234 ", node.value()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(string_node__to_string); +ATF_TEST_CASE_BODY(string_node__to_string) +{ + config::string_node node; + node.set(""); + ATF_REQUIRE_EQ("", node.to_string()); + node.set("aaa"); + ATF_REQUIRE_EQ("aaa", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__deep_copy); +ATF_TEST_CASE_BODY(strings_set_node__deep_copy) +{ + std::set< std::string > value; + config::strings_set_node node; + value.insert("foo"); + node.set(value); + config::detail::base_node* raw_copy = node.deep_copy(); + config::strings_set_node* copy = + static_cast< config::strings_set_node* >(raw_copy); + value.insert("bar"); + ATF_REQUIRE_EQ(1, copy->value().size()); + copy->set(value); + ATF_REQUIRE_EQ(1, node.value().size()); + ATF_REQUIRE_EQ(2, copy->value().size()); + delete copy; +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__is_set_and_set); +ATF_TEST_CASE_BODY(strings_set_node__is_set_and_set) +{ + std::set< std::string > value; + value.insert("foo"); + + config::strings_set_node node; + ATF_REQUIRE(!node.is_set()); + node.set(value); + ATF_REQUIRE( node.is_set()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__value_and_set); +ATF_TEST_CASE_BODY(strings_set_node__value_and_set) +{ + std::set< std::string > value; + value.insert("first"); + + config::strings_set_node node; + node.set(value); + ATF_REQUIRE(value == node.value()); + value.clear(); + node.set(value); + value.insert("second"); + ATF_REQUIRE(node.value().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__set_string); +ATF_TEST_CASE_BODY(strings_set_node__set_string) +{ + config::strings_set_node node; + { + std::set< std::string > expected; + expected.insert("abcd"); + expected.insert("efgh"); + + node.set_string("abcd efgh"); + ATF_REQUIRE(expected == node.value()); + } + { + std::set< std::string > expected; + expected.insert("1234"); + + node.set_string(" 1234 "); + ATF_REQUIRE(expected == node.value()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(strings_set_node__to_string); +ATF_TEST_CASE_BODY(strings_set_node__to_string) +{ + std::set< std::string > value; + config::strings_set_node node; + value.insert("second"); + value.insert("first"); + node.set(value); + ATF_REQUIRE_EQ("first second", node.to_string()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(typed_leaf_node__validate_set); +ATF_TEST_CASE_BODY(typed_leaf_node__validate_set) +{ + validation_node node; + node.set(1234); + ATF_REQUIRE_THROW_RE(config::value_error, "Custom validate method", + node.set(12345)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(typed_leaf_node__validate_set_string); +ATF_TEST_CASE_BODY(typed_leaf_node__validate_set_string) +{ + validation_node node; + node.set_string("1234"); + ATF_REQUIRE_THROW_RE(config::value_error, "Custom validate method", + node.set_string("12345")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_set_node__validate_set); +ATF_TEST_CASE_BODY(base_set_node__validate_set) +{ + set_validation_node node; + set_validation_node::value_type values; + values.insert("foo"); + values.insert("bar"); + node.set(values); + values.insert("throw"); + values.insert("baz"); + ATF_REQUIRE_THROW_RE(config::value_error, "Custom validate method", + node.set(values)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(base_set_node__validate_set_string); +ATF_TEST_CASE_BODY(base_set_node__validate_set_string) +{ + set_validation_node node; + node.set_string("foo bar"); + ATF_REQUIRE_THROW_RE(config::value_error, "Custom validate method", + node.set_string("foo bar throw baz")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, bool_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, bool_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, bool_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, bool_node__push_lua); + ATF_ADD_TEST_CASE(tcs, bool_node__set_lua__ok); + ATF_ADD_TEST_CASE(tcs, bool_node__set_lua__invalid_value); + ATF_ADD_TEST_CASE(tcs, bool_node__set_string__ok); + ATF_ADD_TEST_CASE(tcs, bool_node__set_string__invalid_value); + ATF_ADD_TEST_CASE(tcs, bool_node__to_string); + + ATF_ADD_TEST_CASE(tcs, int_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, int_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, int_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, int_node__push_lua); + ATF_ADD_TEST_CASE(tcs, int_node__set_lua__ok); + ATF_ADD_TEST_CASE(tcs, int_node__set_lua__invalid_value); + ATF_ADD_TEST_CASE(tcs, int_node__set_string__ok); + ATF_ADD_TEST_CASE(tcs, int_node__set_string__invalid_value); + ATF_ADD_TEST_CASE(tcs, int_node__to_string); + + ATF_ADD_TEST_CASE(tcs, positive_int_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, positive_int_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, positive_int_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, positive_int_node__push_lua); + ATF_ADD_TEST_CASE(tcs, positive_int_node__set_lua__ok); + ATF_ADD_TEST_CASE(tcs, positive_int_node__set_lua__invalid_value); + ATF_ADD_TEST_CASE(tcs, positive_int_node__set_string__ok); + ATF_ADD_TEST_CASE(tcs, positive_int_node__set_string__invalid_value); + ATF_ADD_TEST_CASE(tcs, positive_int_node__to_string); + + ATF_ADD_TEST_CASE(tcs, string_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, string_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, string_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, string_node__push_lua); + ATF_ADD_TEST_CASE(tcs, string_node__set_lua__ok); + ATF_ADD_TEST_CASE(tcs, string_node__set_lua__invalid_value); + ATF_ADD_TEST_CASE(tcs, string_node__set_string); + ATF_ADD_TEST_CASE(tcs, string_node__to_string); + + ATF_ADD_TEST_CASE(tcs, strings_set_node__deep_copy); + ATF_ADD_TEST_CASE(tcs, strings_set_node__is_set_and_set); + ATF_ADD_TEST_CASE(tcs, strings_set_node__value_and_set); + ATF_ADD_TEST_CASE(tcs, strings_set_node__set_string); + ATF_ADD_TEST_CASE(tcs, strings_set_node__to_string); + + ATF_ADD_TEST_CASE(tcs, typed_leaf_node__validate_set); + ATF_ADD_TEST_CASE(tcs, typed_leaf_node__validate_set_string); + ATF_ADD_TEST_CASE(tcs, base_set_node__validate_set); + ATF_ADD_TEST_CASE(tcs, base_set_node__validate_set_string); +} diff --git a/utils/config/parser.cpp b/utils/config/parser.cpp new file mode 100644 index 000000000000..7bfe5517fdd0 --- /dev/null +++ b/utils/config/parser.cpp @@ -0,0 +1,181 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/parser.hpp" + +#include +#include +#include +#include + +#include "utils/config/exceptions.hpp" +#include "utils/config/lua_module.hpp" +#include "utils/config/tree.ipp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" + +namespace config = utils::config; + + +// History of configuration file versions: +// +// 2 - Changed the syntax() call to take only a version number, instead of the +// word 'config' as the first argument and the version as the second one. +// Files now start with syntax(2) instead of syntax('config', 1). +// +// 1 - Initial version. + + +/// Internal implementation of the parser. +struct utils::config::parser::impl : utils::noncopyable { + /// Pointer to the parent parser. Needed for callbacks. + parser* _parent; + + /// The Lua state used by this parser to process the configuration file. + lutok::state _state; + + /// The tree to be filed in by the configuration parameters, as provided by + /// the caller. + config::tree& _tree; + + /// Whether syntax() has been called or not. + bool _syntax_called; + + /// Constructs a new implementation. + /// + /// \param parent_ Pointer to the class being constructed. + /// \param config_tree_ The configuration tree provided by the user. + impl(parser* const parent_, tree& config_tree_) : + _parent(parent_), _tree(config_tree_), _syntax_called(false) + { + } + + friend void lua_syntax(lutok::state&); + + /// Callback executed by the Lua syntax() function. + /// + /// \param syntax_version The syntax format version as provided by the + /// configuration file in the call to syntax(). + void + syntax_callback(const int syntax_version) + { + if (_syntax_called) + throw syntax_error("syntax() can only be called once"); + _syntax_called = true; + + // Allow the parser caller to populate the tree with its own schema + // depending on the format/version combination. + _parent->setup(_tree, syntax_version); + + // Export the config module to the Lua state so that all global variable + // accesses are redirected to the configuration tree. + config::redirect(_state, _tree); + } +}; + + +namespace { + + +static int +lua_syntax(lutok::state& state) +{ + if (!state.is_number(-1)) + throw config::value_error("Last argument to syntax must be a number"); + const int syntax_version = state.to_integer(-1); + + if (syntax_version == 1) { + if (state.get_top() != 2) + throw config::value_error("Version 1 files need two arguments to " + "syntax()"); + if (!state.is_string(-2) || state.to_string(-2) != "config") + throw config::value_error("First argument to syntax must be " + "'config' for version 1 files"); + } else { + if (state.get_top() != 1) + throw config::value_error("syntax() only takes one argument"); + } + + state.get_global("_config_parser"); + config::parser::impl* impl = + *state.to_userdata< config::parser::impl* >(-1); + state.pop(1); + + impl->syntax_callback(syntax_version); + + return 0; +} + + +} // anonymous namespace + + +/// Constructs a new parser. +/// +/// \param [in,out] config_tree The configuration tree into which the values set +/// in the configuration file will be stored. +config::parser::parser(tree& config_tree) : + _pimpl(new impl(this, config_tree)) +{ + lutok::stack_cleaner cleaner(_pimpl->_state); + + _pimpl->_state.push_cxx_function(lua_syntax); + _pimpl->_state.set_global("syntax"); + *_pimpl->_state.new_userdata< config::parser::impl* >() = _pimpl.get(); + _pimpl->_state.set_global("_config_parser"); +} + + +/// Destructor. +config::parser::~parser(void) +{ +} + + +/// Parses a configuration file. +/// +/// \post The tree registered during the construction of this class is updated +/// to contain the values read from the configuration file. If the processing +/// fails, the state of the output tree is undefined. +/// +/// \param file The path to the file to process. +/// +/// \throw syntax_error If there is any problem processing the file. +void +config::parser::parse(const fs::path& file) +{ + try { + lutok::do_file(_pimpl->_state, file.str(), 0, 0, 0); + } catch (const lutok::error& e) { + throw syntax_error(e.what()); + } + + if (!_pimpl->_syntax_called) + throw syntax_error("No syntax defined (no call to syntax() found)"); +} diff --git a/utils/config/parser.hpp b/utils/config/parser.hpp new file mode 100644 index 000000000000..cb69e756cbe8 --- /dev/null +++ b/utils/config/parser.hpp @@ -0,0 +1,95 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/parser.hpp +/// Utilities to read a configuration file into memory. + +#if !defined(UTILS_CONFIG_PARSER_HPP) +#define UTILS_CONFIG_PARSER_HPP + +#include "utils/config/parser_fwd.hpp" + +#include + +#include "utils/config/tree_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/noncopyable.hpp" + +namespace utils { +namespace config { + + +/// A configuration parser. +/// +/// This parser is a class rather than a function because we need to support +/// callbacks to perform the initialization of the config file schema. The +/// configuration files always start with a call to syntax(), which define the +/// particular version of the schema being used. Depending on such version, the +/// layout of the internal tree representation needs to be different. +/// +/// A parser implementation must provide a setup() method to set up the +/// configuration schema based on the particular combination of syntax format +/// and version specified on the file. +/// +/// Parser objects are not supposed to be reused, and specific trees are not +/// supposed to be passed to multiple parsers (even if sequentially). Doing so +/// will cause all kinds of inconsistencies in the managed tree itself or in the +/// Lua state. +class parser : noncopyable { +public: + struct impl; + +private: + /// Pointer to the internal implementation. + std::auto_ptr< impl > _pimpl; + + /// Hook to initialize the tree keys before reading the file. + /// + /// This hook gets called when the configuration file defines its specific + /// format by calling the syntax() function. We have to delay the tree + /// initialization until this point because, before we know what version of + /// a configuration file we are parsing, we cannot know what keys are valid. + /// + /// \param [in,out] config_tree The tree in which to define the key + /// structure. + /// \param syntax_version The version of the file format as specified in the + /// configuration file. + virtual void setup(tree& config_tree, const int syntax_version) = 0; + +public: + explicit parser(tree&); + virtual ~parser(void); + + void parse(const fs::path&); +}; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_PARSER_HPP) diff --git a/utils/config/parser_fwd.hpp b/utils/config/parser_fwd.hpp new file mode 100644 index 000000000000..6278b6c95c12 --- /dev/null +++ b/utils/config/parser_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/parser_fwd.hpp +/// Forward declarations for utils/config/parser.hpp + +#if !defined(UTILS_CONFIG_PARSER_FWD_HPP) +#define UTILS_CONFIG_PARSER_FWD_HPP + +namespace utils { +namespace config { + + +class parser; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_PARSER_FWD_HPP) diff --git a/utils/config/parser_test.cpp b/utils/config/parser_test.cpp new file mode 100644 index 000000000000..f5445f55c490 --- /dev/null +++ b/utils/config/parser_test.cpp @@ -0,0 +1,252 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/parser.hpp" + +#include + +#include + +#include "utils/config/exceptions.hpp" +#include "utils/config/parser.hpp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Implementation of a parser for testing purposes. +class mock_parser : public config::parser { + /// Initializes the tree keys before reading the file. + /// + /// \param [in,out] tree The tree in which to define the key structure. + /// \param syntax_version The version of the file format as specified in the + /// configuration file. + void + setup(config::tree& tree, const int syntax_version) + { + if (syntax_version == 1) { + // Do nothing on config_tree. + } else if (syntax_version == 2) { + tree.define< config::string_node >("top_string"); + tree.define< config::int_node >("inner.int"); + tree.define_dynamic("inner.dynamic"); + } else { + throw std::runtime_error(F("Unknown syntax version %s") % + syntax_version); + } + } + +public: + /// Initializes a parser. + /// + /// \param tree The mock config tree to parse. + mock_parser(config::tree& tree) : + config::parser(tree) + { + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(no_keys__ok); +ATF_TEST_CASE_BODY(no_keys__ok) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "local foo = 'value'\n"); + + config::tree tree; + mock_parser(tree).parse(fs::path("output.lua")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::string_node >("foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(no_keys__unknown_key); +ATF_TEST_CASE_BODY(no_keys__unknown_key) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "foo = 'value'\n"); + + config::tree tree; + ATF_REQUIRE_THROW_RE(config::syntax_error, "foo", + mock_parser(tree).parse(fs::path("output.lua"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_keys__ok); +ATF_TEST_CASE_BODY(some_keys__ok) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "top_string = 'foo'\n" + "inner.int = 12345\n" + "inner.dynamic.foo = 78\n" + "inner.dynamic.bar = 'some text'\n"); + + config::tree tree; + mock_parser(tree).parse(fs::path("output.lua")); + ATF_REQUIRE_EQ("foo", tree.lookup< config::string_node >("top_string")); + ATF_REQUIRE_EQ(12345, tree.lookup< config::int_node >("inner.int")); + ATF_REQUIRE_EQ("78", + tree.lookup< config::string_node >("inner.dynamic.foo")); + ATF_REQUIRE_EQ("some text", + tree.lookup< config::string_node >("inner.dynamic.bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_keys__not_strict); +ATF_TEST_CASE_BODY(some_keys__not_strict) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "top_string = 'foo'\n" + "unknown_string = 'bar'\n" + "top_string = 'baz'\n"); + + config::tree tree(false); + mock_parser(tree).parse(fs::path("output.lua")); + ATF_REQUIRE_EQ("baz", tree.lookup< config::string_node >("top_string")); + ATF_REQUIRE(!tree.is_set("unknown_string")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some_keys__unknown_key); +ATF_TEST_CASE_BODY(some_keys__unknown_key) +{ + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "top_string2 = 'foo'\n"); + config::tree tree1; + ATF_REQUIRE_THROW_RE(config::syntax_error, + "Unknown configuration property 'top_string2'", + mock_parser(tree1).parse(fs::path("output.lua"))); + + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "inner.int2 = 12345\n"); + config::tree tree2; + ATF_REQUIRE_THROW_RE(config::syntax_error, + "Unknown configuration property 'inner.int2'", + mock_parser(tree2).parse(fs::path("output.lua"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_syntax); +ATF_TEST_CASE_BODY(invalid_syntax) +{ + config::tree tree; + + atf::utils::create_file("output.lua", "syntax(56)\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, + "Unknown syntax version 56", + mock_parser(tree).parse(fs::path("output.lua"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_deprecated_format); +ATF_TEST_CASE_BODY(syntax_deprecated_format) +{ + config::tree tree; + + atf::utils::create_file("output.lua", "syntax('config', 1)\n"); + (void)mock_parser(tree).parse(fs::path("output.lua")); + + atf::utils::create_file("output.lua", "syntax('foo', 1)\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, "must be 'config'", + mock_parser(tree).parse(fs::path("output.lua"))); + + atf::utils::create_file("output.lua", "syntax('config', 2)\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, "only takes one argument", + mock_parser(tree).parse(fs::path("output.lua"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_not_called); +ATF_TEST_CASE_BODY(syntax_not_called) +{ + config::tree tree; + tree.define< config::int_node >("var"); + + atf::utils::create_file("output.lua", "var = 3\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, "No syntax defined", + mock_parser(tree).parse(fs::path("output.lua"))); + + ATF_REQUIRE(!tree.is_set("var")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_called_more_than_once); +ATF_TEST_CASE_BODY(syntax_called_more_than_once) +{ + config::tree tree; + tree.define< config::int_node >("var"); + + atf::utils::create_file( + "output.lua", + "syntax(2)\n" + "var = 3\n" + "syntax(2)\n" + "var = 5\n"); + ATF_REQUIRE_THROW_RE(config::syntax_error, + "syntax\\(\\) can only be called once", + mock_parser(tree).parse(fs::path("output.lua"))); + + ATF_REQUIRE_EQ(3, tree.lookup< config::int_node >("var")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, no_keys__ok); + ATF_ADD_TEST_CASE(tcs, no_keys__unknown_key); + + ATF_ADD_TEST_CASE(tcs, some_keys__ok); + ATF_ADD_TEST_CASE(tcs, some_keys__not_strict); + ATF_ADD_TEST_CASE(tcs, some_keys__unknown_key); + + ATF_ADD_TEST_CASE(tcs, invalid_syntax); + ATF_ADD_TEST_CASE(tcs, syntax_deprecated_format); + ATF_ADD_TEST_CASE(tcs, syntax_not_called); + ATF_ADD_TEST_CASE(tcs, syntax_called_more_than_once); +} diff --git a/utils/config/tree.cpp b/utils/config/tree.cpp new file mode 100644 index 000000000000..1aa2d85b89cd --- /dev/null +++ b/utils/config/tree.cpp @@ -0,0 +1,338 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/tree.ipp" + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/config/nodes.ipp" +#include "utils/format/macros.hpp" + +namespace config = utils::config; + + +/// Constructor. +/// +/// \param strict Whether keys must be validated at "set" time. +config::tree::tree(const bool strict) : + _strict(strict), _root(new detail::static_inner_node()) +{ +} + + +/// Constructor with a non-empty root. +/// +/// \param strict Whether keys must be validated at "set" time. +/// \param root The root to the tree to be owned by this instance. +config::tree::tree(const bool strict, detail::static_inner_node* root) : + _strict(strict), _root(root) +{ +} + + +/// Destructor. +config::tree::~tree(void) +{ +} + + +/// Generates a deep copy of the input tree. +/// +/// \return A new tree that is an exact copy of this tree. +config::tree +config::tree::deep_copy(void) const +{ + detail::static_inner_node* new_root = + dynamic_cast< detail::static_inner_node* >(_root->deep_copy()); + return config::tree(_strict, new_root); +} + + +/// Combines two trees. +/// +/// By combination we understand a new tree that contains the full key space of +/// the two input trees and, for the keys that match, respects the value of the +/// right-hand side (aka "other") tree. +/// +/// Any nodes marked as dynamic "win" over non-dynamic nodes and the resulting +/// tree will have the dynamic property set on those. +/// +/// \param overrides The tree to use as value overrides. +/// +/// \return The combined tree. +/// +/// \throw bad_combination_error If the two trees cannot be combined; for +/// example, if a single key represents an inner node in one tree but a leaf +/// node in the other one. +config::tree +config::tree::combine(const tree& overrides) const +{ + const detail::static_inner_node* other_root = + dynamic_cast< const detail::static_inner_node * >( + overrides._root.get()); + + detail::static_inner_node* new_root = + dynamic_cast< detail::static_inner_node* >( + _root->combine(detail::tree_key(), other_root)); + return config::tree(_strict, new_root); +} + + +/// Registers a node as being dynamic. +/// +/// This operation creates the given key as an inner node. Further set +/// operations that trespass this node will automatically create any missing +/// keys. +/// +/// This method does not raise errors on invalid/unknown keys or other +/// tree-related issues. The reasons is that define() is a method that does not +/// depend on user input: it is intended to pre-populate the tree with a +/// specific structure, and that happens once at coding time. +/// +/// \param dotted_key The key to be registered in dotted representation. +void +config::tree::define_dynamic(const std::string& dotted_key) +{ + try { + const detail::tree_key key = detail::parse_key(dotted_key); + _root->define(key, 0, detail::new_node< detail::dynamic_inner_node >); + } catch (const error& e) { + UNREACHABLE_MSG("define() failing due to key errors is a programming " + "mistake: " + std::string(e.what())); + } +} + + +/// Checks if a given node is set. +/// +/// \param dotted_key The key to be checked. +/// +/// \return True if the key is set to a specific value (not just defined). +/// False if the key is not set or if the key does not exist. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +bool +config::tree::is_set(const std::string& dotted_key) const +{ + const detail::tree_key key = detail::parse_key(dotted_key); + try { + const detail::base_node* raw_node = _root->lookup_ro(key, 0); + try { + const leaf_node& child = dynamic_cast< const leaf_node& >( + *raw_node); + return child.is_set(); + } catch (const std::bad_cast& unused_error) { + return false; + } + } catch (const unknown_key_error& unused_error) { + return false; + } +} + + +/// Pushes a leaf node's value onto the Lua stack. +/// +/// \param dotted_key The key to be pushed. +/// \param state The Lua state into which to push the key's value. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +void +config::tree::push_lua(const std::string& dotted_key, lutok::state& state) const +{ + const detail::tree_key key = detail::parse_key(dotted_key); + const detail::base_node* raw_node = _root->lookup_ro(key, 0); + try { + const leaf_node& child = dynamic_cast< const leaf_node& >(*raw_node); + child.push_lua(state); + } catch (const std::bad_cast& unused_error) { + throw unknown_key_error(key); + } +} + + +/// Sets a leaf node's value from a value in the Lua stack. +/// +/// \param dotted_key The key to be set. +/// \param state The Lua state from which to retrieve the value. +/// \param value_index The position in the Lua stack holding the value. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw invalid_key_value If the value mismatches the node type. +/// \throw unknown_key_error If the provided key is unknown. +void +config::tree::set_lua(const std::string& dotted_key, lutok::state& state, + const int value_index) +{ + const detail::tree_key key = detail::parse_key(dotted_key); + try { + detail::base_node* raw_node = _root->lookup_rw( + key, 0, detail::new_node< string_node >); + leaf_node& child = dynamic_cast< leaf_node& >(*raw_node); + child.set_lua(state, value_index); + } catch (const unknown_key_error& e) { + if (_strict) + throw e; + } catch (const value_error& e) { + throw invalid_key_value(key, e.what()); + } catch (const std::bad_cast& unused_error) { + throw invalid_key_value(key, "Type mismatch"); + } +} + + +/// Gets the value of a node as a plain string. +/// +/// \param dotted_key The key to be looked up. +/// +/// \return The value of the located node as a string. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +std::string +config::tree::lookup_string(const std::string& dotted_key) const +{ + const detail::tree_key key = detail::parse_key(dotted_key); + const detail::base_node* raw_node = _root->lookup_ro(key, 0); + try { + const leaf_node& child = dynamic_cast< const leaf_node& >(*raw_node); + return child.to_string(); + } catch (const std::bad_cast& unused_error) { + throw unknown_key_error(key); + } +} + + +/// Sets the value of a leaf addressed by its key from a string value. +/// +/// This respects the native types of all the nodes that have been predefined. +/// For new nodes under a dynamic subtree, this has no mechanism of determining +/// what type they need to have, so they are created as plain string nodes. +/// +/// \param dotted_key The key to be registered in dotted representation. +/// \param raw_value The string representation of the value to set the node to. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw invalid_key_value If the value mismatches the node type. +/// \throw unknown_key_error If the provided key is unknown. +void +config::tree::set_string(const std::string& dotted_key, + const std::string& raw_value) +{ + const detail::tree_key key = detail::parse_key(dotted_key); + try { + detail::base_node* raw_node = _root->lookup_rw( + key, 0, detail::new_node< string_node >); + leaf_node& child = dynamic_cast< leaf_node& >(*raw_node); + child.set_string(raw_value); + } catch (const unknown_key_error& e) { + if (_strict) + throw e; + } catch (const value_error& e) { + throw invalid_key_value(key, e.what()); + } catch (const std::bad_cast& unused_error) { + throw invalid_key_value(key, "Type mismatch"); + } +} + + +/// Converts the tree to a collection of key/value string pairs. +/// +/// \param dotted_key Subtree from which to start the export. +/// \param strip_key If true, remove the dotted_key prefix from the resulting +/// properties. +/// +/// \return A map of keys to values in their textual representation. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +/// \throw value_error If the provided key points to a leaf. +config::properties_map +config::tree::all_properties(const std::string& dotted_key, + const bool strip_key) const +{ + PRE(!strip_key || !dotted_key.empty()); + + properties_map properties; + + detail::tree_key key; + const detail::base_node* raw_node; + if (dotted_key.empty()) { + raw_node = _root.get(); + } else { + key = detail::parse_key(dotted_key); + raw_node = _root->lookup_ro(key, 0); + } + try { + const detail::inner_node& child = + dynamic_cast< const detail::inner_node& >(*raw_node); + child.all_properties(properties, key); + } catch (const std::bad_cast& unused_error) { + INV(!dotted_key.empty()); + throw value_error(F("Cannot export properties from a leaf node; " + "'%s' given") % dotted_key); + } + + if (strip_key) { + properties_map stripped; + for (properties_map::const_iterator iter = properties.begin(); + iter != properties.end(); ++iter) { + stripped[(*iter).first.substr(dotted_key.length() + 1)] = + (*iter).second; + } + properties = stripped; + } + + return properties; +} + + +/// Equality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +bool +config::tree::operator==(const tree& other) const +{ + // TODO(jmmv): Would be nicer to perform the comparison directly on the + // nodes, instead of exporting the values to strings first. + return _root == other._root || all_properties() == other.all_properties(); +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +bool +config::tree::operator!=(const tree& other) const +{ + return !(*this == other); +} diff --git a/utils/config/tree.hpp b/utils/config/tree.hpp new file mode 100644 index 000000000000..cad0a9b4fc0b --- /dev/null +++ b/utils/config/tree.hpp @@ -0,0 +1,128 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/tree.hpp +/// Data type to represent a tree of arbitrary values with string keys. + +#if !defined(UTILS_CONFIG_TREE_HPP) +#define UTILS_CONFIG_TREE_HPP + +#include "utils/config/tree_fwd.hpp" + +#include +#include + +#include + +#include "utils/config/keys_fwd.hpp" +#include "utils/config/nodes_fwd.hpp" + +namespace utils { +namespace config { + + +/// Representation of a tree. +/// +/// The string keys of the tree are in dotted notation and actually represent +/// path traversals through the nodes. +/// +/// Our trees are "strictly-keyed": keys must be defined as "existent" before +/// their values can be set. Defining a key is a separate action from setting +/// its value. The rationale is that we want to be able to control what keys +/// get defined: because trees are used to hold configuration, we want to catch +/// typos as early as possible. Also, users cannot set keys unless the types +/// are known in advance because our leaf nodes are strictly typed. +/// +/// However, there is an exception to the strict keys: the inner nodes of the +/// tree can be static or dynamic. Static inner nodes have a known subset of +/// children and attempting to set keys not previously defined will result in an +/// error. Dynamic inner nodes do not have a predefined set of keys and can be +/// used to accept arbitrary user input. +/// +/// For simplicity reasons, we force the root of the tree to be a static inner +/// node. In other words, the root can never contain a value by itself and this +/// is not a problem because the root is not addressable by the key space. +/// Additionally, the root is strict so all of its direct children must be +/// explicitly defined. +/// +/// This is, effectively, a simple wrapper around the node representing the +/// root. Having a separate class aids in clearly representing the concept of a +/// tree and all of its public methods. Also, the tree accepts dotted notations +/// for the keys while the internal structures do not. +/// +/// Note that trees are shallow-copied unless a deep copy is requested with +/// deep_copy(). +class tree { + /// Whether keys must be validated at "set" time. + bool _strict; + + /// The root of the tree. + std::shared_ptr< detail::static_inner_node > _root; + + tree(const bool, detail::static_inner_node*); + +public: + tree(const bool = true); + ~tree(void); + + tree deep_copy(void) const; + tree combine(const tree&) const; + + template< class LeafType > + void define(const std::string&); + + void define_dynamic(const std::string&); + + bool is_set(const std::string&) const; + + template< class LeafType > + const typename LeafType::value_type& lookup(const std::string&) const; + template< class LeafType > + typename LeafType::value_type& lookup_rw(const std::string&); + + template< class LeafType > + void set(const std::string&, const typename LeafType::value_type&); + + void push_lua(const std::string&, lutok::state&) const; + void set_lua(const std::string&, lutok::state&, const int); + + std::string lookup_string(const std::string&) const; + void set_string(const std::string&, const std::string&); + + properties_map all_properties(const std::string& = "", + const bool = false) const; + + bool operator==(const tree&) const; + bool operator!=(const tree&) const; +}; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_TREE_HPP) diff --git a/utils/config/tree.ipp b/utils/config/tree.ipp new file mode 100644 index 000000000000..a79acc3be184 --- /dev/null +++ b/utils/config/tree.ipp @@ -0,0 +1,156 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/tree.hpp" + +#if !defined(UTILS_CONFIG_TREE_IPP) +#define UTILS_CONFIG_TREE_IPP + +#include + +#include "utils/config/exceptions.hpp" +#include "utils/config/keys.hpp" +#include "utils/config/nodes.ipp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" + +namespace utils { + + +/// Registers a key as valid and having a specific type. +/// +/// This method does not raise errors on invalid/unknown keys or other +/// tree-related issues. The reasons is that define() is a method that does not +/// depend on user input: it is intended to pre-populate the tree with a +/// specific structure, and that happens once at coding time. +/// +/// \tparam LeafType The node type of the leaf we are defining. +/// \param dotted_key The key to be registered in dotted representation. +template< class LeafType > +void +config::tree::define(const std::string& dotted_key) +{ + try { + const detail::tree_key key = detail::parse_key(dotted_key); + _root->define(key, 0, detail::new_node< LeafType >); + } catch (const error& e) { + UNREACHABLE_MSG(F("define() failing due to key errors is a programming " + "mistake: %s") % e.what()); + } +} + + +/// Gets a read-only reference to the value of a leaf addressed by its key. +/// +/// \tparam LeafType The node type of the leaf we are querying. +/// \param dotted_key The key to be registered in dotted representation. +/// +/// \return A reference to the value in the located leaf, if successful. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +template< class LeafType > +const typename LeafType::value_type& +config::tree::lookup(const std::string& dotted_key) const +{ + const detail::tree_key key = detail::parse_key(dotted_key); + const detail::base_node* raw_node = _root->lookup_ro(key, 0); + try { + const LeafType& child = dynamic_cast< const LeafType& >(*raw_node); + if (child.is_set()) + return child.value(); + else + throw unknown_key_error(key); + } catch (const std::bad_cast& unused_error) { + throw unknown_key_error(key); + } +} + + +/// Gets a read-write reference to the value of a leaf addressed by its key. +/// +/// \tparam LeafType The node type of the leaf we are querying. +/// \param dotted_key The key to be registered in dotted representation. +/// +/// \return A reference to the value in the located leaf, if successful. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw unknown_key_error If the provided key is unknown. +template< class LeafType > +typename LeafType::value_type& +config::tree::lookup_rw(const std::string& dotted_key) +{ + const detail::tree_key key = detail::parse_key(dotted_key); + detail::base_node* raw_node = _root->lookup_rw( + key, 0, detail::new_node< LeafType >); + try { + LeafType& child = dynamic_cast< LeafType& >(*raw_node); + if (child.is_set()) + return child.value(); + else + throw unknown_key_error(key); + } catch (const std::bad_cast& unused_error) { + throw unknown_key_error(key); + } +} + + +/// Sets the value of a leaf addressed by its key. +/// +/// \tparam LeafType The node type of the leaf we are setting. +/// \param dotted_key The key to be registered in dotted representation. +/// \param value The value to set into the node. +/// +/// \throw invalid_key_error If the provided key has an invalid format. +/// \throw invalid_key_value If the value mismatches the node type. +/// \throw unknown_key_error If the provided key is unknown. +template< class LeafType > +void +config::tree::set(const std::string& dotted_key, + const typename LeafType::value_type& value) +{ + const detail::tree_key key = detail::parse_key(dotted_key); + try { + leaf_node* raw_node = _root->lookup_rw(key, 0, + detail::new_node< LeafType >); + LeafType& child = dynamic_cast< LeafType& >(*raw_node); + child.set(value); + } catch (const unknown_key_error& e) { + if (_strict) + throw e; + } catch (const value_error& e) { + throw invalid_key_value(key, e.what()); + } catch (const std::bad_cast& unused_error) { + throw invalid_key_value(key, "Type mismatch"); + } +} + + +} // namespace utils + +#endif // !defined(UTILS_CONFIG_TREE_IPP) diff --git a/utils/config/tree_fwd.hpp b/utils/config/tree_fwd.hpp new file mode 100644 index 000000000000..e494d8c0f4ee --- /dev/null +++ b/utils/config/tree_fwd.hpp @@ -0,0 +1,52 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/config/tree_fwd.hpp +/// Forward declarations for utils/config/tree.hpp + +#if !defined(UTILS_CONFIG_TREE_FWD_HPP) +#define UTILS_CONFIG_TREE_FWD_HPP + +#include +#include + +namespace utils { +namespace config { + + +/// Flat representation of all properties as strings. +typedef std::map< std::string, std::string > properties_map; + + +class tree; + + +} // namespace config +} // namespace utils + +#endif // !defined(UTILS_CONFIG_TREE_FWD_HPP) diff --git a/utils/config/tree_test.cpp b/utils/config/tree_test.cpp new file mode 100644 index 000000000000..b6efd64a84a6 --- /dev/null +++ b/utils/config/tree_test.cpp @@ -0,0 +1,1086 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/config/tree.ipp" + +#include + +#include "utils/config/nodes.ipp" +#include "utils/format/macros.hpp" +#include "utils/text/operations.ipp" + +namespace config = utils::config; +namespace text = utils::text; + + +namespace { + + +/// Simple wrapper around an integer value without default constructors. +/// +/// The purpose of this type is to have a simple class without default +/// constructors to validate that we can use it as a leaf of a tree. +class int_wrapper { + /// The wrapped integer value. + int _value; + +public: + /// Constructs a new wrapped integer. + /// + /// \param value_ The value to store in the object. + explicit int_wrapper(int value_) : + _value(value_) + { + } + + /// \return The integer value stored by the object. + int + value(void) const + { + return _value; + } +}; + + +/// Custom tree leaf type for an object without defualt constructors. +class wrapped_int_node : public config::typed_leaf_node< int_wrapper > { +public: + /// Copies the node. + /// + /// \return A dynamically-allocated node. + virtual base_node* + deep_copy(void) const + { + std::auto_ptr< wrapped_int_node > new_node(new wrapped_int_node()); + new_node->_value = _value; + return new_node.release(); + } + + /// Pushes the node's value onto the Lua stack. + /// + /// \param state The Lua state onto which to push the value. + void + push_lua(lutok::state& state) const + { + state.push_integer( + config::typed_leaf_node< int_wrapper >::value().value()); + } + + /// Sets the value of the node from an entry in the Lua stack. + /// + /// \param state The Lua state from which to get the value. + /// \param value_index The stack index in which the value resides. + void + set_lua(lutok::state& state, const int value_index) + { + ATF_REQUIRE(state.is_number(value_index)); + int_wrapper new_value(state.to_integer(value_index)); + config::typed_leaf_node< int_wrapper >::set(new_value); + } + + /// Sets the value of the node from a raw string representation. + /// + /// \param raw_value The value to set the node to. + void + set_string(const std::string& raw_value) + { + int_wrapper new_value(text::to_type< int >(raw_value)); + config::typed_leaf_node< int_wrapper >::set(new_value); + } + + /// Converts the contents of the node to a string. + /// + /// \return A string representation of the value held by the node. + std::string + to_string(void) const + { + return F("%s") % + config::typed_leaf_node< int_wrapper >::value().value(); + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(define_set_lookup__one_level); +ATF_TEST_CASE_BODY(define_set_lookup__one_level) +{ + config::tree tree; + + tree.define< config::int_node >("var1"); + tree.define< config::string_node >("var2"); + tree.define< config::bool_node >("var3"); + + tree.set< config::int_node >("var1", 42); + tree.set< config::string_node >("var2", "hello"); + tree.set< config::bool_node >("var3", false); + + ATF_REQUIRE_EQ(42, tree.lookup< config::int_node >("var1")); + ATF_REQUIRE_EQ("hello", tree.lookup< config::string_node >("var2")); + ATF_REQUIRE(!tree.lookup< config::bool_node >("var3")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(define_set_lookup__multiple_levels); +ATF_TEST_CASE_BODY(define_set_lookup__multiple_levels) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar.1"); + tree.define< config::string_node >("foo.bar.2"); + tree.define< config::bool_node >("foo.3"); + tree.define_dynamic("sub.tree"); + + tree.set< config::int_node >("foo.bar.1", 42); + tree.set< config::string_node >("foo.bar.2", "hello"); + tree.set< config::bool_node >("foo.3", true); + tree.set< config::string_node >("sub.tree.1", "bye"); + tree.set< config::int_node >("sub.tree.2", 4); + tree.set< config::int_node >("sub.tree.3.4", 123); + + ATF_REQUIRE_EQ(42, tree.lookup< config::int_node >("foo.bar.1")); + ATF_REQUIRE_EQ("hello", tree.lookup< config::string_node >("foo.bar.2")); + ATF_REQUIRE(tree.lookup< config::bool_node >("foo.3")); + ATF_REQUIRE_EQ(4, tree.lookup< config::int_node >("sub.tree.2")); + ATF_REQUIRE_EQ(123, tree.lookup< config::int_node >("sub.tree.3.4")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(deep_copy__empty); +ATF_TEST_CASE_BODY(deep_copy__empty) +{ + config::tree tree1; + config::tree tree2 = tree1.deep_copy(); + + tree1.define< config::bool_node >("var1"); + // This would crash if the copy shared the internal data. + tree2.define< config::int_node >("var1"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(deep_copy__some); +ATF_TEST_CASE_BODY(deep_copy__some) +{ + config::tree tree1; + tree1.define< config::bool_node >("this.is.a.var"); + tree1.set< config::bool_node >("this.is.a.var", true); + tree1.define< config::int_node >("this.is.another.var"); + tree1.set< config::int_node >("this.is.another.var", 34); + tree1.define< config::int_node >("and.another"); + tree1.set< config::int_node >("and.another", 123); + + config::tree tree2 = tree1.deep_copy(); + tree2.set< config::bool_node >("this.is.a.var", false); + tree2.set< config::int_node >("this.is.another.var", 43); + + ATF_REQUIRE( tree1.lookup< config::bool_node >("this.is.a.var")); + ATF_REQUIRE(!tree2.lookup< config::bool_node >("this.is.a.var")); + + ATF_REQUIRE_EQ(34, tree1.lookup< config::int_node >("this.is.another.var")); + ATF_REQUIRE_EQ(43, tree2.lookup< config::int_node >("this.is.another.var")); + + ATF_REQUIRE_EQ(123, tree1.lookup< config::int_node >("and.another")); + ATF_REQUIRE_EQ(123, tree2.lookup< config::int_node >("and.another")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__empty); +ATF_TEST_CASE_BODY(combine__empty) +{ + const config::tree t1, t2; + const config::tree combined = t1.combine(t2); + + const config::tree expected; + ATF_REQUIRE(expected == combined); +} + + +static void +init_tree_for_combine_test(config::tree& tree) +{ + tree.define< config::int_node >("int-node"); + tree.define< config::string_node >("string-node"); + tree.define< config::int_node >("unused.node"); + tree.define< config::int_node >("deeper.int.node"); + tree.define_dynamic("deeper.dynamic"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__same_layout__no_overrides); +ATF_TEST_CASE_BODY(combine__same_layout__no_overrides) +{ + config::tree t1, t2; + init_tree_for_combine_test(t1); + init_tree_for_combine_test(t2); + t1.set< config::int_node >("int-node", 3); + t1.set< config::string_node >("string-node", "foo"); + t1.set< config::int_node >("deeper.int.node", 15); + t1.set_string("deeper.dynamic.first", "value1"); + t1.set_string("deeper.dynamic.second", "value2"); + const config::tree combined = t1.combine(t2); + + ATF_REQUIRE(t1 == combined); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__same_layout__no_base); +ATF_TEST_CASE_BODY(combine__same_layout__no_base) +{ + config::tree t1, t2; + init_tree_for_combine_test(t1); + init_tree_for_combine_test(t2); + t2.set< config::int_node >("int-node", 3); + t2.set< config::string_node >("string-node", "foo"); + t2.set< config::int_node >("deeper.int.node", 15); + t2.set_string("deeper.dynamic.first", "value1"); + t2.set_string("deeper.dynamic.second", "value2"); + const config::tree combined = t1.combine(t2); + + ATF_REQUIRE(t2 == combined); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__same_layout__mix); +ATF_TEST_CASE_BODY(combine__same_layout__mix) +{ + config::tree t1, t2; + init_tree_for_combine_test(t1); + init_tree_for_combine_test(t2); + t1.set< config::int_node >("int-node", 3); + t2.set< config::int_node >("int-node", 5); + t1.set< config::string_node >("string-node", "foo"); + t2.set< config::int_node >("deeper.int.node", 15); + t1.set_string("deeper.dynamic.first", "value1"); + t1.set_string("deeper.dynamic.second", "value2.1"); + t2.set_string("deeper.dynamic.second", "value2.2"); + t2.set_string("deeper.dynamic.third", "value3"); + const config::tree combined = t1.combine(t2); + + config::tree expected; + init_tree_for_combine_test(expected); + expected.set< config::int_node >("int-node", 5); + expected.set< config::string_node >("string-node", "foo"); + expected.set< config::int_node >("deeper.int.node", 15); + expected.set_string("deeper.dynamic.first", "value1"); + expected.set_string("deeper.dynamic.second", "value2.2"); + expected.set_string("deeper.dynamic.third", "value3"); + ATF_REQUIRE(expected == combined); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__different_layout); +ATF_TEST_CASE_BODY(combine__different_layout) +{ + config::tree t1; + t1.define< config::int_node >("common.base1"); + t1.define< config::int_node >("common.base2"); + t1.define_dynamic("dynamic.base"); + t1.define< config::int_node >("unset.base"); + + config::tree t2; + t2.define< config::int_node >("common.base2"); + t2.define< config::int_node >("common.base3"); + t2.define_dynamic("dynamic.other"); + t2.define< config::int_node >("unset.other"); + + t1.set< config::int_node >("common.base1", 1); + t1.set< config::int_node >("common.base2", 2); + t1.set_string("dynamic.base.first", "foo"); + t1.set_string("dynamic.base.second", "bar"); + + t2.set< config::int_node >("common.base2", 4); + t2.set< config::int_node >("common.base3", 3); + t2.set_string("dynamic.other.first", "FOO"); + t2.set_string("dynamic.other.second", "BAR"); + + config::tree combined = t1.combine(t2); + + config::tree expected; + expected.define< config::int_node >("common.base1"); + expected.define< config::int_node >("common.base2"); + expected.define< config::int_node >("common.base3"); + expected.define_dynamic("dynamic.base"); + expected.define_dynamic("dynamic.other"); + expected.define< config::int_node >("unset.base"); + expected.define< config::int_node >("unset.other"); + + expected.set< config::int_node >("common.base1", 1); + expected.set< config::int_node >("common.base2", 4); + expected.set< config::int_node >("common.base3", 3); + expected.set_string("dynamic.base.first", "foo"); + expected.set_string("dynamic.base.second", "bar"); + expected.set_string("dynamic.other.first", "FOO"); + expected.set_string("dynamic.other.second", "BAR"); + + ATF_REQUIRE(expected == combined); + + // The combined tree should have respected existing but unset nodes. Check + // that these calls do not crash. + combined.set< config::int_node >("unset.base", 5); + combined.set< config::int_node >("unset.other", 5); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__dynamic_wins); +ATF_TEST_CASE_BODY(combine__dynamic_wins) +{ + config::tree t1; + t1.define< config::int_node >("inner.leaf1"); + t1.set< config::int_node >("inner.leaf1", 3); + + config::tree t2; + t2.define_dynamic("inner"); + t2.set_string("inner.leaf2", "4"); + + config::tree combined = t1.combine(t2); + + config::tree expected; + expected.define_dynamic("inner"); + expected.set_string("inner.leaf1", "3"); + expected.set_string("inner.leaf2", "4"); + + ATF_REQUIRE(expected == combined); + + // The combined inner node should have become dynamic so this call should + // not fail. + combined.set_string("inner.leaf3", "5"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(combine__inner_leaf_mismatch); +ATF_TEST_CASE_BODY(combine__inner_leaf_mismatch) +{ + config::tree t1; + t1.define< config::int_node >("top.foo.bar"); + + config::tree t2; + t2.define< config::int_node >("top.foo"); + + ATF_REQUIRE_THROW_RE(config::bad_combination_error, + "'top.foo' is an inner node in the base tree but a " + "leaf node in the overrides tree", + t1.combine(t2)); + + ATF_REQUIRE_THROW_RE(config::bad_combination_error, + "'top.foo' is a leaf node in the base tree but an " + "inner node in the overrides tree", + t2.combine(t1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup__invalid_key); +ATF_TEST_CASE_BODY(lookup__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, + tree.lookup< config::int_node >(".")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup__unknown_key); +ATF_TEST_CASE_BODY(lookup__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set< config::int_node >("a.b.c", 123); + tree.set< config::int_node >("a.d.100", 0); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("abc")); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("foo")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("foo.bar")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("foo.bar.baz")); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.b")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.c")); + (void)tree.lookup< config::int_node >("a.b.c"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.b.c.d")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.d")); + (void)tree.lookup< config::int_node >("a.d.100"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.d.101")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.d.100.3")); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.lookup< config::int_node >("a.d.e")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_set__one_level); +ATF_TEST_CASE_BODY(is_set__one_level) +{ + config::tree tree; + + tree.define< config::int_node >("var1"); + tree.define< config::string_node >("var2"); + tree.define< config::bool_node >("var3"); + + tree.set< config::int_node >("var1", 42); + tree.set< config::bool_node >("var3", false); + + ATF_REQUIRE( tree.is_set("var1")); + ATF_REQUIRE(!tree.is_set("var2")); + ATF_REQUIRE( tree.is_set("var3")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_set__multiple_levels); +ATF_TEST_CASE_BODY(is_set__multiple_levels) +{ + config::tree tree; + + tree.define< config::int_node >("a.b.var1"); + tree.define< config::string_node >("a.b.var2"); + tree.define< config::bool_node >("e.var3"); + + tree.set< config::int_node >("a.b.var1", 42); + tree.set< config::bool_node >("e.var3", false); + + ATF_REQUIRE(!tree.is_set("a")); + ATF_REQUIRE(!tree.is_set("a.b")); + ATF_REQUIRE( tree.is_set("a.b.var1")); + ATF_REQUIRE(!tree.is_set("a.b.var1.trailing")); + + ATF_REQUIRE(!tree.is_set("a")); + ATF_REQUIRE(!tree.is_set("a.b")); + ATF_REQUIRE(!tree.is_set("a.b.var2")); + ATF_REQUIRE(!tree.is_set("a.b.var2.trailing")); + + ATF_REQUIRE(!tree.is_set("e")); + ATF_REQUIRE( tree.is_set("e.var3")); + ATF_REQUIRE(!tree.is_set("e.var3.trailing")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_set__invalid_key); +ATF_TEST_CASE_BODY(is_set__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, tree.is_set(".abc")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set__invalid_key); +ATF_TEST_CASE_BODY(set__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, + tree.set< config::int_node >("foo.", 54)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set__invalid_key_value); +ATF_TEST_CASE_BODY(set__invalid_key_value) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + tree.define_dynamic("a.d"); + + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set< config::int_node >("foo", 3)); + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set< config::int_node >("a", -10)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set__unknown_key); +ATF_TEST_CASE_BODY(set__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set< config::int_node >("a.b.c", 123); + tree.set< config::string_node >("a.d.3", "foo"); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("abc", 2)); + + tree.set< config::int_node >("foo.bar", 15); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("foo.bar.baz", 0)); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("a.c", 100)); + tree.set< config::int_node >("a.b.c", -3); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("a.b.c.d", 82)); + tree.set< config::string_node >("a.d.3", "bar"); + tree.set< config::string_node >("a.d.4", "bar"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set< config::int_node >("a.d.4.5", 82)); + tree.set< config::int_node >("a.d.5.6", 82); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set__unknown_key_not_strict); +ATF_TEST_CASE_BODY(set__unknown_key_not_strict) +{ + config::tree tree(false); + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set< config::int_node >("a.b.c", 123); + tree.set< config::string_node >("a.d.3", "foo"); + + tree.set< config::int_node >("abc", 2); + ATF_REQUIRE(!tree.is_set("abc")); + + tree.set< config::int_node >("foo.bar", 15); + tree.set< config::int_node >("foo.bar.baz", 0); + ATF_REQUIRE(!tree.is_set("foo.bar.baz")); + + tree.set< config::int_node >("a.c", 100); + ATF_REQUIRE(!tree.is_set("a.c")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(push_lua__ok); +ATF_TEST_CASE_BODY(push_lua__ok) +{ + config::tree tree; + + tree.define< config::int_node >("top.integer"); + tree.define< wrapped_int_node >("top.custom"); + tree.define_dynamic("dynamic"); + tree.set< config::int_node >("top.integer", 5); + tree.set< wrapped_int_node >("top.custom", int_wrapper(10)); + tree.set_string("dynamic.first", "foo"); + + lutok::state state; + tree.push_lua("top.integer", state); + tree.push_lua("top.custom", state); + tree.push_lua("dynamic.first", state); + ATF_REQUIRE(state.is_number(-3)); + ATF_REQUIRE_EQ(5, state.to_integer(-3)); + ATF_REQUIRE(state.is_number(-2)); + ATF_REQUIRE_EQ(10, state.to_integer(-2)); + ATF_REQUIRE(state.is_string(-1)); + ATF_REQUIRE_EQ("foo", state.to_string(-1)); + state.pop(3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_lua__ok); +ATF_TEST_CASE_BODY(set_lua__ok) +{ + config::tree tree; + + tree.define< config::int_node >("top.integer"); + tree.define< wrapped_int_node >("top.custom"); + tree.define_dynamic("dynamic"); + + { + lutok::state state; + state.push_integer(5); + state.push_integer(10); + state.push_string("foo"); + tree.set_lua("top.integer", state, -3); + tree.set_lua("top.custom", state, -2); + tree.set_lua("dynamic.first", state, -1); + state.pop(3); + } + + ATF_REQUIRE_EQ(5, tree.lookup< config::int_node >("top.integer")); + ATF_REQUIRE_EQ(10, tree.lookup< wrapped_int_node >("top.custom").value()); + ATF_REQUIRE_EQ("foo", tree.lookup< config::string_node >("dynamic.first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup_rw); +ATF_TEST_CASE_BODY(lookup_rw) +{ + config::tree tree; + + tree.define< config::int_node >("var1"); + tree.define< config::bool_node >("var3"); + + tree.set< config::int_node >("var1", 42); + tree.set< config::bool_node >("var3", false); + + tree.lookup_rw< config::int_node >("var1") += 10; + ATF_REQUIRE_EQ(52, tree.lookup< config::int_node >("var1")); + ATF_REQUIRE(!tree.lookup< config::bool_node >("var3")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup_string__ok); +ATF_TEST_CASE_BODY(lookup_string__ok) +{ + config::tree tree; + + tree.define< config::int_node >("var1"); + tree.define< config::string_node >("b.var2"); + tree.define< config::bool_node >("c.d.var3"); + + tree.set< config::int_node >("var1", 42); + tree.set< config::string_node >("b.var2", "hello"); + tree.set< config::bool_node >("c.d.var3", false); + + ATF_REQUIRE_EQ("42", tree.lookup_string("var1")); + ATF_REQUIRE_EQ("hello", tree.lookup_string("b.var2")); + ATF_REQUIRE_EQ("false", tree.lookup_string("c.d.var3")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup_string__invalid_key); +ATF_TEST_CASE_BODY(lookup_string__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, tree.lookup_string("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lookup_string__unknown_key); +ATF_TEST_CASE_BODY(lookup_string__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("a.b.c"); + + ATF_REQUIRE_THROW(config::unknown_key_error, tree.lookup_string("a.b")); + ATF_REQUIRE_THROW(config::unknown_key_error, tree.lookup_string("a.b.c.d")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__ok); +ATF_TEST_CASE_BODY(set_string__ok) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar.1"); + tree.define< config::string_node >("foo.bar.2"); + tree.define_dynamic("sub.tree"); + + tree.set_string("foo.bar.1", "42"); + tree.set_string("foo.bar.2", "hello"); + tree.set_string("sub.tree.2", "15"); + tree.set_string("sub.tree.3.4", "bye"); + + ATF_REQUIRE_EQ(42, tree.lookup< config::int_node >("foo.bar.1")); + ATF_REQUIRE_EQ("hello", tree.lookup< config::string_node >("foo.bar.2")); + ATF_REQUIRE_EQ("15", tree.lookup< config::string_node >("sub.tree.2")); + ATF_REQUIRE_EQ("bye", tree.lookup< config::string_node >("sub.tree.3.4")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__invalid_key); +ATF_TEST_CASE_BODY(set_string__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, tree.set_string(".", "foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__invalid_key_value); +ATF_TEST_CASE_BODY(set_string__invalid_key_value) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set_string("foo", "abc")); + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set_string("foo.bar", " -3")); + ATF_REQUIRE_THROW(config::invalid_key_value, + tree.set_string("foo.bar", "3 ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__unknown_key); +ATF_TEST_CASE_BODY(set_string__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set_string("a.b.c", "123"); + tree.set_string("a.d.3", "foo"); + + ATF_REQUIRE_THROW(config::unknown_key_error, tree.set_string("abc", "2")); + + tree.set_string("foo.bar", "15"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set_string("foo.bar.baz", "0")); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set_string("a.c", "100")); + tree.set_string("a.b.c", "-3"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set_string("a.b.c.d", "82")); + tree.set_string("a.d.3", "bar"); + tree.set_string("a.d.4", "bar"); + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.set_string("a.d.4.5", "82")); + tree.set_string("a.d.5.6", "82"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_string__unknown_key_not_strict); +ATF_TEST_CASE_BODY(set_string__unknown_key_not_strict) +{ + config::tree tree(false); + + tree.define< config::int_node >("foo.bar"); + tree.define< config::int_node >("a.b.c"); + tree.define_dynamic("a.d"); + tree.set_string("a.b.c", "123"); + tree.set_string("a.d.3", "foo"); + + tree.set_string("abc", "2"); + ATF_REQUIRE(!tree.is_set("abc")); + + tree.set_string("foo.bar", "15"); + tree.set_string("foo.bar.baz", "0"); + ATF_REQUIRE(!tree.is_set("foo.bar.baz")); + + tree.set_string("a.c", "100"); + ATF_REQUIRE(!tree.is_set("a.c")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__none); +ATF_TEST_CASE_BODY(all_properties__none) +{ + const config::tree tree; + ATF_REQUIRE(tree.all_properties().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__all_set); +ATF_TEST_CASE_BODY(all_properties__all_set) +{ + config::tree tree; + + tree.define< config::int_node >("plain"); + tree.set< config::int_node >("plain", 1234); + + tree.define< config::int_node >("static.first"); + tree.set< config::int_node >("static.first", -3); + tree.define< config::string_node >("static.second"); + tree.set< config::string_node >("static.second", "some text"); + + tree.define_dynamic("dynamic"); + tree.set< config::string_node >("dynamic.first", "hello"); + tree.set< config::string_node >("dynamic.second", "bye"); + + config::properties_map exp_properties; + exp_properties["plain"] = "1234"; + exp_properties["static.first"] = "-3"; + exp_properties["static.second"] = "some text"; + exp_properties["dynamic.first"] = "hello"; + exp_properties["dynamic.second"] = "bye"; + + const config::properties_map properties = tree.all_properties(); + ATF_REQUIRE(exp_properties == properties); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__some_unset); +ATF_TEST_CASE_BODY(all_properties__some_unset) +{ + config::tree tree; + + tree.define< config::int_node >("static.first"); + tree.set< config::int_node >("static.first", -3); + tree.define< config::string_node >("static.second"); + + tree.define_dynamic("dynamic"); + + config::properties_map exp_properties; + exp_properties["static.first"] = "-3"; + + const config::properties_map properties = tree.all_properties(); + ATF_REQUIRE(exp_properties == properties); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__inner); +ATF_TEST_CASE_BODY(all_properties__subtree__inner) +{ + config::tree tree; + + tree.define< config::int_node >("root.a.b.c.first"); + tree.define< config::int_node >("root.a.b.c.second"); + tree.define< config::int_node >("root.a.d.first"); + + tree.set< config::int_node >("root.a.b.c.first", 1); + tree.set< config::int_node >("root.a.b.c.second", 2); + tree.set< config::int_node >("root.a.d.first", 3); + + { + config::properties_map exp_properties; + exp_properties["root.a.b.c.first"] = "1"; + exp_properties["root.a.b.c.second"] = "2"; + exp_properties["root.a.d.first"] = "3"; + ATF_REQUIRE(exp_properties == tree.all_properties("root")); + ATF_REQUIRE(exp_properties == tree.all_properties("root.a")); + } + + { + config::properties_map exp_properties; + exp_properties["root.a.b.c.first"] = "1"; + exp_properties["root.a.b.c.second"] = "2"; + ATF_REQUIRE(exp_properties == tree.all_properties("root.a.b")); + ATF_REQUIRE(exp_properties == tree.all_properties("root.a.b.c")); + } + + { + config::properties_map exp_properties; + exp_properties["root.a.d.first"] = "3"; + ATF_REQUIRE(exp_properties == tree.all_properties("root.a.d")); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__leaf); +ATF_TEST_CASE_BODY(all_properties__subtree__leaf) +{ + config::tree tree; + + tree.define< config::int_node >("root.a.b.c.first"); + tree.set< config::int_node >("root.a.b.c.first", 1); + ATF_REQUIRE_THROW_RE(config::value_error, "Cannot export.*leaf", + tree.all_properties("root.a.b.c.first")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__strip_key); +ATF_TEST_CASE_BODY(all_properties__subtree__strip_key) +{ + config::tree tree; + + tree.define< config::int_node >("root.a.b.c.first"); + tree.define< config::int_node >("root.a.b.c.second"); + tree.define< config::int_node >("root.a.d.first"); + + tree.set< config::int_node >("root.a.b.c.first", 1); + tree.set< config::int_node >("root.a.b.c.second", 2); + tree.set< config::int_node >("root.a.d.first", 3); + + config::properties_map exp_properties; + exp_properties["b.c.first"] = "1"; + exp_properties["b.c.second"] = "2"; + exp_properties["d.first"] = "3"; + ATF_REQUIRE(exp_properties == tree.all_properties("root.a", true)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__invalid_key); +ATF_TEST_CASE_BODY(all_properties__subtree__invalid_key) +{ + config::tree tree; + + ATF_REQUIRE_THROW(config::invalid_key_error, tree.all_properties(".")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_properties__subtree__unknown_key); +ATF_TEST_CASE_BODY(all_properties__subtree__unknown_key) +{ + config::tree tree; + + tree.define< config::int_node >("root.a.b.c.first"); + tree.set< config::int_node >("root.a.b.c.first", 1); + tree.define< config::int_node >("root.a.b.c.unset"); + + ATF_REQUIRE_THROW(config::unknown_key_error, + tree.all_properties("root.a.b.c.first.foo")); + ATF_REQUIRE_THROW_RE(config::value_error, "Cannot export.*leaf", + tree.all_properties("root.a.b.c.unset")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__empty); +ATF_TEST_CASE_BODY(operators_eq_and_ne__empty) +{ + config::tree t1; + config::tree t2; + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__shallow_copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__shallow_copy) +{ + config::tree t1; + t1.define< config::int_node >("root.a.b.c.first"); + t1.set< config::int_node >("root.a.b.c.first", 1); + config::tree t2 = t1; + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__deep_copy); +ATF_TEST_CASE_BODY(operators_eq_and_ne__deep_copy) +{ + config::tree t1; + t1.define< config::int_node >("root.a.b.c.first"); + t1.set< config::int_node >("root.a.b.c.first", 1); + config::tree t2 = t1.deep_copy(); + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__some_contents); +ATF_TEST_CASE_BODY(operators_eq_and_ne__some_contents) +{ + config::tree t1, t2; + + t1.define< config::int_node >("root.a.b.c.first"); + t1.set< config::int_node >("root.a.b.c.first", 1); + ATF_REQUIRE(!(t1 == t2)); + ATF_REQUIRE( t1 != t2); + + t2.define< config::int_node >("root.a.b.c.first"); + t2.set< config::int_node >("root.a.b.c.first", 1); + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); + + t1.set< config::int_node >("root.a.b.c.first", 2); + ATF_REQUIRE(!(t1 == t2)); + ATF_REQUIRE( t1 != t2); + + t2.set< config::int_node >("root.a.b.c.first", 2); + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); + + t1.define< config::string_node >("another.key"); + t1.set< config::string_node >("another.key", "some text"); + ATF_REQUIRE(!(t1 == t2)); + ATF_REQUIRE( t1 != t2); + + t2.define< config::string_node >("another.key"); + t2.set< config::string_node >("another.key", "some text"); + ATF_REQUIRE( t1 == t2); + ATF_REQUIRE(!(t1 != t2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(custom_leaf__no_default_ctor); +ATF_TEST_CASE_BODY(custom_leaf__no_default_ctor) +{ + config::tree tree; + + tree.define< wrapped_int_node >("test1"); + tree.define< wrapped_int_node >("test2"); + tree.set< wrapped_int_node >("test1", int_wrapper(5)); + tree.set< wrapped_int_node >("test2", int_wrapper(10)); + const int_wrapper& test1 = tree.lookup< wrapped_int_node >("test1"); + ATF_REQUIRE_EQ(5, test1.value()); + const int_wrapper& test2 = tree.lookup< wrapped_int_node >("test2"); + ATF_REQUIRE_EQ(10, test2.value()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, define_set_lookup__one_level); + ATF_ADD_TEST_CASE(tcs, define_set_lookup__multiple_levels); + + ATF_ADD_TEST_CASE(tcs, deep_copy__empty); + ATF_ADD_TEST_CASE(tcs, deep_copy__some); + + ATF_ADD_TEST_CASE(tcs, combine__empty); + ATF_ADD_TEST_CASE(tcs, combine__same_layout__no_overrides); + ATF_ADD_TEST_CASE(tcs, combine__same_layout__no_base); + ATF_ADD_TEST_CASE(tcs, combine__same_layout__mix); + ATF_ADD_TEST_CASE(tcs, combine__different_layout); + ATF_ADD_TEST_CASE(tcs, combine__dynamic_wins); + ATF_ADD_TEST_CASE(tcs, combine__inner_leaf_mismatch); + + ATF_ADD_TEST_CASE(tcs, lookup__invalid_key); + ATF_ADD_TEST_CASE(tcs, lookup__unknown_key); + + ATF_ADD_TEST_CASE(tcs, is_set__one_level); + ATF_ADD_TEST_CASE(tcs, is_set__multiple_levels); + ATF_ADD_TEST_CASE(tcs, is_set__invalid_key); + + ATF_ADD_TEST_CASE(tcs, set__invalid_key); + ATF_ADD_TEST_CASE(tcs, set__invalid_key_value); + ATF_ADD_TEST_CASE(tcs, set__unknown_key); + ATF_ADD_TEST_CASE(tcs, set__unknown_key_not_strict); + + ATF_ADD_TEST_CASE(tcs, push_lua__ok); + ATF_ADD_TEST_CASE(tcs, set_lua__ok); + + ATF_ADD_TEST_CASE(tcs, lookup_rw); + + ATF_ADD_TEST_CASE(tcs, lookup_string__ok); + ATF_ADD_TEST_CASE(tcs, lookup_string__invalid_key); + ATF_ADD_TEST_CASE(tcs, lookup_string__unknown_key); + + ATF_ADD_TEST_CASE(tcs, set_string__ok); + ATF_ADD_TEST_CASE(tcs, set_string__invalid_key); + ATF_ADD_TEST_CASE(tcs, set_string__invalid_key_value); + ATF_ADD_TEST_CASE(tcs, set_string__unknown_key); + ATF_ADD_TEST_CASE(tcs, set_string__unknown_key_not_strict); + + ATF_ADD_TEST_CASE(tcs, all_properties__none); + ATF_ADD_TEST_CASE(tcs, all_properties__all_set); + ATF_ADD_TEST_CASE(tcs, all_properties__some_unset); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__inner); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__leaf); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__strip_key); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__invalid_key); + ATF_ADD_TEST_CASE(tcs, all_properties__subtree__unknown_key); + + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__empty); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__shallow_copy); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__deep_copy); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__some_contents); + + ATF_ADD_TEST_CASE(tcs, custom_leaf__no_default_ctor); +} diff --git a/utils/datetime.cpp b/utils/datetime.cpp new file mode 100644 index 000000000000..ae3fdb62fe55 --- /dev/null +++ b/utils/datetime.cpp @@ -0,0 +1,613 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/datetime.hpp" + +extern "C" { +#include + +#include +} + +#include + +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" + +namespace datetime = utils::datetime; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Fake value for the current time. +static optional< datetime::timestamp > mock_now = none; + + +} // anonymous namespace + + +/// Creates a zero time delta. +datetime::delta::delta(void) : + seconds(0), + useconds(0) +{ +} + + +/// Creates a time delta. +/// +/// \param seconds_ The seconds in the delta. +/// \param useconds_ The microseconds in the delta. +/// +/// \throw std::runtime_error If the input delta is negative. +datetime::delta::delta(const int64_t seconds_, + const unsigned long useconds_) : + seconds(seconds_), + useconds(useconds_) +{ + if (seconds_ < 0) { + throw std::runtime_error(F("Negative deltas are not supported by the " + "datetime::delta class; got: %s") % (*this)); + } +} + + +/// Converts a time expressed in microseconds to a delta. +/// +/// \param useconds The amount of microseconds representing the delta. +/// +/// \return A new delta object. +/// +/// \throw std::runtime_error If the input delta is negative. +datetime::delta +datetime::delta::from_microseconds(const int64_t useconds) +{ + if (useconds < 0) { + throw std::runtime_error(F("Negative deltas are not supported by the " + "datetime::delta class; got: %sus") % + useconds); + } + + return delta(useconds / 1000000, useconds % 1000000); +} + + +/// Convers the delta to a flat representation expressed in microseconds. +/// +/// \return The amount of microseconds that corresponds to this delta. +int64_t +datetime::delta::to_microseconds(void) const +{ + return seconds * 1000000 + useconds; +} + + +/// Checks if two time deltas are equal. +/// +/// \param other The object to compare to. +/// +/// \return True if the two time deltas are equals; false otherwise. +bool +datetime::delta::operator==(const datetime::delta& other) const +{ + return seconds == other.seconds && useconds == other.useconds; +} + + +/// Checks if two time deltas are different. +/// +/// \param other The object to compare to. +/// +/// \return True if the two time deltas are different; false otherwise. +bool +datetime::delta::operator!=(const datetime::delta& other) const +{ + return !(*this == other); +} + + +/// Checks if this time delta is shorter than another one. +/// +/// \param other The object to compare to. +/// +/// \return True if this time delta is shorter than other; false otherwise. +bool +datetime::delta::operator<(const datetime::delta& other) const +{ + return seconds < other.seconds || + (seconds == other.seconds && useconds < other.useconds); +} + + +/// Checks if this time delta is shorter than or equal to another one. +/// +/// \param other The object to compare to. +/// +/// \return True if this time delta is shorter than or equal to; false +/// otherwise. +bool +datetime::delta::operator<=(const datetime::delta& other) const +{ + return (*this) < other || (*this) == other; +} + + +/// Checks if this time delta is larger than another one. +/// +/// \param other The object to compare to. +/// +/// \return True if this time delta is larger than other; false otherwise. +bool +datetime::delta::operator>(const datetime::delta& other) const +{ + return seconds > other.seconds || + (seconds == other.seconds && useconds > other.useconds); +} + + +/// Checks if this time delta is larger than or equal to another one. +/// +/// \param other The object to compare to. +/// +/// \return True if this time delta is larger than or equal to; false +/// otherwise. +bool +datetime::delta::operator>=(const datetime::delta& other) const +{ + return (*this) > other || (*this) == other; +} + + +/// Adds a time delta to this one. +/// +/// \param other The time delta to add. +/// +/// \return The addition of this time delta with the other time delta. +datetime::delta +datetime::delta::operator+(const datetime::delta& other) const +{ + return delta::from_microseconds(to_microseconds() + + other.to_microseconds()); +} + + +/// Adds a time delta to this one and updates this with the result. +/// +/// \param other The time delta to add. +/// +/// \return The addition of this time delta with the other time delta. +datetime::delta& +datetime::delta::operator+=(const datetime::delta& other) +{ + *this = *this + other; + return *this; +} + + +/// Scales this delta by a positive integral factor. +/// +/// \param factor The scaling factor. +/// +/// \return The scaled delta. +datetime::delta +datetime::delta::operator*(const std::size_t factor) const +{ + return delta::from_microseconds(to_microseconds() * factor); +} + + +/// Scales this delta by and updates this delta with the result. +/// +/// \param factor The scaling factor. +/// +/// \return The scaled delta as a reference to the input object. +datetime::delta& +datetime::delta::operator*=(const std::size_t factor) +{ + *this = *this * factor; + return *this; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +datetime::operator<<(std::ostream& output, const delta& object) +{ + return (output << object.to_microseconds() << "us"); +} + + +namespace utils { +namespace datetime { + + +/// Internal representation for datetime::timestamp. +struct timestamp::impl : utils::noncopyable { + /// The raw timestamp as provided by libc. + ::timeval data; + + /// Constructs an impl object from initialized data. + /// + /// \param data_ The raw timestamp to use. + impl(const ::timeval& data_) : data(data_) + { + } +}; + + +} // namespace datetime +} // namespace utils + + +/// Constructs a new timestamp. +/// +/// \param pimpl_ An existing impl representation. +datetime::timestamp::timestamp(std::shared_ptr< impl > pimpl_) : + _pimpl(pimpl_) +{ +} + + +/// Constructs a timestamp from the amount of microseconds since the epoch. +/// +/// \param value Microseconds since the epoch in UTC. Must be positive. +/// +/// \return A new timestamp. +datetime::timestamp +datetime::timestamp::from_microseconds(const int64_t value) +{ + PRE(value >= 0); + ::timeval data; + data.tv_sec = static_cast< time_t >(value / 1000000); + data.tv_usec = static_cast< suseconds_t >(value % 1000000); + return timestamp(std::shared_ptr< impl >(new impl(data))); +} + + +/// Constructs a timestamp based on user-friendly values. +/// +/// \param year The year in the [1900,inf) range. +/// \param month The month in the [1,12] range. +/// \param day The day in the [1,30] range. +/// \param hour The hour in the [0,23] range. +/// \param minute The minute in the [0,59] range. +/// \param second The second in the [0,60] range. Yes, that is 60, which can be +/// the case on leap seconds. +/// \param microsecond The microsecond in the [0,999999] range. +/// +/// \return A new timestamp. +datetime::timestamp +datetime::timestamp::from_values(const int year, const int month, + const int day, const int hour, + const int minute, const int second, + const int microsecond) +{ + PRE(year >= 1900); + PRE(month >= 1 && month <= 12); + PRE(day >= 1 && day <= 30); + PRE(hour >= 0 && hour <= 23); + PRE(minute >= 0 && minute <= 59); + PRE(second >= 0 && second <= 60); + PRE(microsecond >= 0 && microsecond <= 999999); + + // The code below is quite convoluted. The problem is that we can't assume + // that some fields (like tm_zone) of ::tm exist, and thus we can't blindly + // set them from the code. Instead of detecting their presence in the + // configure script, we just query the current time to initialize such + // fields and then we override the ones we are interested in. (There might + // be some better way to do this, but I don't know it and the documentation + // does not shed much light into how to create your own fake date.) + + const time_t current_time = ::time(NULL); + + ::tm timedata; + if (::gmtime_r(¤t_time, &timedata) == NULL) + UNREACHABLE; + + timedata.tm_sec = second; + timedata.tm_min = minute; + timedata.tm_hour = hour; + timedata.tm_mday = day; + timedata.tm_mon = month - 1; + timedata.tm_year = year - 1900; + // Ignored: timedata.tm_wday + // Ignored: timedata.tm_yday + + ::timeval data; + data.tv_sec = ::mktime(&timedata); + data.tv_usec = static_cast< suseconds_t >(microsecond); + return timestamp(std::shared_ptr< impl >(new impl(data))); +} + + +/// Constructs a new timestamp representing the current time in UTC. +/// +/// \return A new timestamp. +datetime::timestamp +datetime::timestamp::now(void) +{ + if (mock_now) + return mock_now.get(); + + ::timeval data; + { + const int ret = ::gettimeofday(&data, NULL); + INV(ret != -1); + } + + return timestamp(std::shared_ptr< impl >(new impl(data))); +} + + +/// Formats a timestamp. +/// +/// \param format The format string to use as consumed by strftime(3). +/// +/// \return The formatted time. +std::string +datetime::timestamp::strftime(const std::string& format) const +{ + ::tm timedata; + // This conversion to time_t is necessary because tv_sec is not guaranteed + // to be a time_t. For example, it isn't in NetBSD 5.x + ::time_t epoch_seconds; + epoch_seconds = _pimpl->data.tv_sec; + if (::gmtime_r(&epoch_seconds, &timedata) == NULL) + UNREACHABLE_MSG("gmtime_r(3) did not accept the value returned by " + "gettimeofday(2)"); + + char buf[128]; + if (::strftime(buf, sizeof(buf), format.c_str(), &timedata) == 0) + UNREACHABLE_MSG("Arbitrary-long format strings are unimplemented"); + return buf; +} + + +/// Formats a timestamp with the ISO 8601 standard and in UTC. +/// +/// \return A string with the formatted timestamp. +std::string +datetime::timestamp::to_iso8601_in_utc(void) const +{ + return F("%s.%06sZ") % strftime("%Y-%m-%dT%H:%M:%S") % _pimpl->data.tv_usec; +} + + +/// Returns the number of microseconds since the epoch in UTC. +/// +/// \return A number of microseconds. +int64_t +datetime::timestamp::to_microseconds(void) const +{ + return static_cast< int64_t >(_pimpl->data.tv_sec) * 1000000 + + _pimpl->data.tv_usec; +} + + +/// Returns the number of seconds since the epoch in UTC. +/// +/// \return A number of seconds. +int64_t +datetime::timestamp::to_seconds(void) const +{ + return static_cast< int64_t >(_pimpl->data.tv_sec); +} + + +/// Sets the current time for testing purposes. +void +datetime::set_mock_now(const int year, const int month, + const int day, const int hour, + const int minute, const int second, + const int microsecond) +{ + mock_now = timestamp::from_values(year, month, day, hour, minute, second, + microsecond); +} + + +/// Sets the current time for testing purposes. +/// +/// \param mock_now_ The mock timestamp to set the time to. +void +datetime::set_mock_now(const timestamp& mock_now_) +{ + mock_now = mock_now_; +} + + +/// Checks if two timestamps are equal. +/// +/// \param other The object to compare to. +/// +/// \return True if the two timestamps are equals; false otherwise. +bool +datetime::timestamp::operator==(const datetime::timestamp& other) const +{ + return _pimpl->data.tv_sec == other._pimpl->data.tv_sec && + _pimpl->data.tv_usec == other._pimpl->data.tv_usec; +} + + +/// Checks if two timestamps are different. +/// +/// \param other The object to compare to. +/// +/// \return True if the two timestamps are different; false otherwise. +bool +datetime::timestamp::operator!=(const datetime::timestamp& other) const +{ + return !(*this == other); +} + + +/// Checks if a timestamp is before another. +/// +/// \param other The object to compare to. +/// +/// \return True if this timestamp comes before other; false otherwise. +bool +datetime::timestamp::operator<(const datetime::timestamp& other) const +{ + return to_microseconds() < other.to_microseconds(); +} + + +/// Checks if a timestamp is before or equal to another. +/// +/// \param other The object to compare to. +/// +/// \return True if this timestamp comes before other or is equal to it; +/// false otherwise. +bool +datetime::timestamp::operator<=(const datetime::timestamp& other) const +{ + return to_microseconds() <= other.to_microseconds(); +} + + +/// Checks if a timestamp is after another. +/// +/// \param other The object to compare to. +/// +/// \return True if this timestamp comes after other; false otherwise; +bool +datetime::timestamp::operator>(const datetime::timestamp& other) const +{ + return to_microseconds() > other.to_microseconds(); +} + + +/// Checks if a timestamp is after or equal to another. +/// +/// \param other The object to compare to. +/// +/// \return True if this timestamp comes after other or is equal to it; +/// false otherwise. +bool +datetime::timestamp::operator>=(const datetime::timestamp& other) const +{ + return to_microseconds() >= other.to_microseconds(); +} + + +/// Calculates the addition of a delta to a timestamp. +/// +/// \param other The delta to add. +/// +/// \return A new timestamp in the future. +datetime::timestamp +datetime::timestamp::operator+(const datetime::delta& other) const +{ + return datetime::timestamp::from_microseconds(to_microseconds() + + other.to_microseconds()); +} + + +/// Calculates the addition of a delta to this timestamp. +/// +/// \param other The delta to add. +/// +/// \return A reference to the modified timestamp. +datetime::timestamp& +datetime::timestamp::operator+=(const datetime::delta& other) +{ + *this = *this + other; + return *this; +} + + +/// Calculates the subtraction of a delta from a timestamp. +/// +/// \param other The delta to subtract. +/// +/// \return A new timestamp in the past. +datetime::timestamp +datetime::timestamp::operator-(const datetime::delta& other) const +{ + return datetime::timestamp::from_microseconds(to_microseconds() - + other.to_microseconds()); +} + + +/// Calculates the subtraction of a delta from this timestamp. +/// +/// \param other The delta to subtract. +/// +/// \return A reference to the modified timestamp. +datetime::timestamp& +datetime::timestamp::operator-=(const datetime::delta& other) +{ + *this = *this - other; + return *this; +} + + +/// Calculates the delta between two timestamps. +/// +/// \param other The subtrahend. +/// +/// \return The difference between this object and the other object. +/// +/// \throw std::runtime_error If the subtraction would result in a negative time +/// delta, which are currently not supported. +datetime::delta +datetime::timestamp::operator-(const datetime::timestamp& other) const +{ + if ((*this) < other) { + throw std::runtime_error( + F("Cannot subtract %s from %s as it would result in a negative " + "datetime::delta, which are not supported") % other % (*this)); + } + return datetime::delta::from_microseconds(to_microseconds() - + other.to_microseconds()); +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +std::ostream& +datetime::operator<<(std::ostream& output, const timestamp& object) +{ + return (output << object.to_microseconds() << "us"); +} diff --git a/utils/datetime.hpp b/utils/datetime.hpp new file mode 100644 index 000000000000..0c24f332f6d3 --- /dev/null +++ b/utils/datetime.hpp @@ -0,0 +1,140 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/datetime.hpp +/// Provides date and time-related classes and functions. + +#if !defined(UTILS_DATETIME_HPP) +#define UTILS_DATETIME_HPP + +#include "utils/datetime_fwd.hpp" + +extern "C" { +#include +} + +#include +#include +#include +#include + + +namespace utils { +namespace datetime { + + +/// Represents a time delta to describe deadlines. +/// +/// Because we use this class to handle deadlines, we currently do not support +/// negative deltas. +class delta { +public: + /// The amount of seconds in the time delta. + int64_t seconds; + + /// The amount of microseconds in the time delta. + unsigned long useconds; + + delta(void); + delta(const int64_t, const unsigned long); + + static delta from_microseconds(const int64_t); + int64_t to_microseconds(void) const; + + bool operator==(const delta&) const; + bool operator!=(const delta&) const; + bool operator<(const delta&) const; + bool operator<=(const delta&) const; + bool operator>(const delta&) const; + bool operator>=(const delta&) const; + + delta operator+(const delta&) const; + delta& operator+=(const delta&); + // operator- and operator-= do not exist because we do not support negative + // deltas. See class docstring. + delta operator*(const std::size_t) const; + delta& operator*=(const std::size_t); +}; + + +std::ostream& operator<<(std::ostream&, const delta&); + + +/// Represents a fixed date/time. +/// +/// Timestamps are immutable objects and therefore we can simply use a shared +/// pointer to hide the implementation type of the date. By not using an auto +/// pointer, we don't have to worry about providing our own copy constructor and +/// assignment opertor. +class timestamp { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + timestamp(std::shared_ptr< impl >); + +public: + static timestamp from_microseconds(const int64_t); + static timestamp from_values(const int, const int, const int, + const int, const int, const int, + const int); + static timestamp now(void); + + std::string strftime(const std::string&) const; + std::string to_iso8601_in_utc(void) const; + int64_t to_microseconds(void) const; + int64_t to_seconds(void) const; + + bool operator==(const timestamp&) const; + bool operator!=(const timestamp&) const; + bool operator<(const timestamp&) const; + bool operator<=(const timestamp&) const; + bool operator>(const timestamp&) const; + bool operator>=(const timestamp&) const; + + timestamp operator+(const delta&) const; + timestamp& operator+=(const delta&); + timestamp operator-(const delta&) const; + timestamp& operator-=(const delta&); + delta operator-(const timestamp&) const; +}; + + +std::ostream& operator<<(std::ostream&, const timestamp&); + + +void set_mock_now(const int, const int, const int, const int, const int, + const int, const int); +void set_mock_now(const timestamp&); + + +} // namespace datetime +} // namespace utils + +#endif // !defined(UTILS_DATETIME_HPP) diff --git a/utils/datetime_fwd.hpp b/utils/datetime_fwd.hpp new file mode 100644 index 000000000000..1dd886070a34 --- /dev/null +++ b/utils/datetime_fwd.hpp @@ -0,0 +1,46 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/datetime_fwd.hpp +/// Forward declarations for utils/datetime.hpp + +#if !defined(UTILS_DATETIME_FWD_HPP) +#define UTILS_DATETIME_FWD_HPP + +namespace utils { +namespace datetime { + + +class delta; +class timestamp; + + +} // namespace datetime +} // namespace utils + +#endif // !defined(UTILS_DATETIME_FWD_HPP) diff --git a/utils/datetime_test.cpp b/utils/datetime_test.cpp new file mode 100644 index 000000000000..9f8ff50cd0f8 --- /dev/null +++ b/utils/datetime_test.cpp @@ -0,0 +1,593 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/datetime.hpp" + +extern "C" { +#include +#include +} + +#include +#include + +#include + +namespace datetime = utils::datetime; + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__defaults); +ATF_TEST_CASE_BODY(delta__defaults) +{ + const datetime::delta delta; + ATF_REQUIRE_EQ(0, delta.seconds); + ATF_REQUIRE_EQ(0, delta.useconds); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__overrides); +ATF_TEST_CASE_BODY(delta__overrides) +{ + const datetime::delta delta(1, 2); + ATF_REQUIRE_EQ(1, delta.seconds); + ATF_REQUIRE_EQ(2, delta.useconds); + + ATF_REQUIRE_THROW_RE( + std::runtime_error, "Negative.*not supported.*-4999997us", + datetime::delta(-5, 3)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__from_microseconds); +ATF_TEST_CASE_BODY(delta__from_microseconds) +{ + { + const datetime::delta delta = datetime::delta::from_microseconds(0); + ATF_REQUIRE_EQ(0, delta.seconds); + ATF_REQUIRE_EQ(0, delta.useconds); + } + { + const datetime::delta delta = datetime::delta::from_microseconds( + 999999); + ATF_REQUIRE_EQ(0, delta.seconds); + ATF_REQUIRE_EQ(999999, delta.useconds); + } + { + const datetime::delta delta = datetime::delta::from_microseconds( + 1000000); + ATF_REQUIRE_EQ(1, delta.seconds); + ATF_REQUIRE_EQ(0, delta.useconds); + } + { + const datetime::delta delta = datetime::delta::from_microseconds( + 10576293); + ATF_REQUIRE_EQ(10, delta.seconds); + ATF_REQUIRE_EQ(576293, delta.useconds); + } + { + const datetime::delta delta = datetime::delta::from_microseconds( + 123456789123456LL); + ATF_REQUIRE_EQ(123456789, delta.seconds); + ATF_REQUIRE_EQ(123456, delta.useconds); + } + + ATF_REQUIRE_THROW_RE( + std::runtime_error, "Negative.*not supported.*-12345us", + datetime::delta::from_microseconds(-12345)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__to_microseconds); +ATF_TEST_CASE_BODY(delta__to_microseconds) +{ + ATF_REQUIRE_EQ(0, datetime::delta(0, 0).to_microseconds()); + ATF_REQUIRE_EQ(999999, datetime::delta(0, 999999).to_microseconds()); + ATF_REQUIRE_EQ(1000000, datetime::delta(1, 0).to_microseconds()); + ATF_REQUIRE_EQ(10576293, datetime::delta(10, 576293).to_microseconds()); + ATF_REQUIRE_EQ(11576293, datetime::delta(10, 1576293).to_microseconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__equals); +ATF_TEST_CASE_BODY(delta__equals) +{ + ATF_REQUIRE(datetime::delta() == datetime::delta()); + ATF_REQUIRE(datetime::delta() == datetime::delta(0, 0)); + ATF_REQUIRE(datetime::delta(1, 2) == datetime::delta(1, 2)); + + ATF_REQUIRE(!(datetime::delta() == datetime::delta(0, 1))); + ATF_REQUIRE(!(datetime::delta() == datetime::delta(1, 0))); + ATF_REQUIRE(!(datetime::delta(1, 2) == datetime::delta(2, 1))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__differs); +ATF_TEST_CASE_BODY(delta__differs) +{ + ATF_REQUIRE(!(datetime::delta() != datetime::delta())); + ATF_REQUIRE(!(datetime::delta() != datetime::delta(0, 0))); + ATF_REQUIRE(!(datetime::delta(1, 2) != datetime::delta(1, 2))); + + ATF_REQUIRE(datetime::delta() != datetime::delta(0, 1)); + ATF_REQUIRE(datetime::delta() != datetime::delta(1, 0)); + ATF_REQUIRE(datetime::delta(1, 2) != datetime::delta(2, 1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__sorting); +ATF_TEST_CASE_BODY(delta__sorting) +{ + ATF_REQUIRE(!(datetime::delta() < datetime::delta())); + ATF_REQUIRE( datetime::delta() <= datetime::delta()); + ATF_REQUIRE(!(datetime::delta() > datetime::delta())); + ATF_REQUIRE( datetime::delta() >= datetime::delta()); + + ATF_REQUIRE(!(datetime::delta(9, 8) < datetime::delta(9, 8))); + ATF_REQUIRE( datetime::delta(9, 8) <= datetime::delta(9, 8)); + ATF_REQUIRE(!(datetime::delta(9, 8) > datetime::delta(9, 8))); + ATF_REQUIRE( datetime::delta(9, 8) >= datetime::delta(9, 8)); + + ATF_REQUIRE( datetime::delta(2, 5) < datetime::delta(4, 8)); + ATF_REQUIRE( datetime::delta(2, 5) <= datetime::delta(4, 8)); + ATF_REQUIRE(!(datetime::delta(2, 5) > datetime::delta(4, 8))); + ATF_REQUIRE(!(datetime::delta(2, 5) >= datetime::delta(4, 8))); + + ATF_REQUIRE( datetime::delta(2, 5) < datetime::delta(2, 8)); + ATF_REQUIRE( datetime::delta(2, 5) <= datetime::delta(2, 8)); + ATF_REQUIRE(!(datetime::delta(2, 5) > datetime::delta(2, 8))); + ATF_REQUIRE(!(datetime::delta(2, 5) >= datetime::delta(2, 8))); + + ATF_REQUIRE(!(datetime::delta(4, 8) < datetime::delta(2, 5))); + ATF_REQUIRE(!(datetime::delta(4, 8) <= datetime::delta(2, 5))); + ATF_REQUIRE( datetime::delta(4, 8) > datetime::delta(2, 5)); + ATF_REQUIRE( datetime::delta(4, 8) >= datetime::delta(2, 5)); + + ATF_REQUIRE(!(datetime::delta(2, 8) < datetime::delta(2, 5))); + ATF_REQUIRE(!(datetime::delta(2, 8) <= datetime::delta(2, 5))); + ATF_REQUIRE( datetime::delta(2, 8) > datetime::delta(2, 5)); + ATF_REQUIRE( datetime::delta(2, 8) >= datetime::delta(2, 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__addition); +ATF_TEST_CASE_BODY(delta__addition) +{ + using datetime::delta; + + ATF_REQUIRE_EQ(delta(), delta() + delta()); + ATF_REQUIRE_EQ(delta(0, 10), delta() + delta(0, 10)); + ATF_REQUIRE_EQ(delta(10, 0), delta(10, 0) + delta()); + + ATF_REQUIRE_EQ(delta(1, 234567), delta(0, 1234567) + delta()); + ATF_REQUIRE_EQ(delta(12, 34), delta(10, 20) + delta(2, 14)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__addition_and_set); +ATF_TEST_CASE_BODY(delta__addition_and_set) +{ + using datetime::delta; + + { + delta d; + d += delta(3, 5); + ATF_REQUIRE_EQ(delta(3, 5), d); + } + { + delta d(1, 2); + d += delta(3, 5); + ATF_REQUIRE_EQ(delta(4, 7), d); + } + { + delta d(1, 2); + ATF_REQUIRE_EQ(delta(4, 7), (d += delta(3, 5))); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__scale); +ATF_TEST_CASE_BODY(delta__scale) +{ + using datetime::delta; + + ATF_REQUIRE_EQ(delta(), delta() * 0); + ATF_REQUIRE_EQ(delta(), delta() * 5); + + ATF_REQUIRE_EQ(delta(0, 30), delta(0, 10) * 3); + ATF_REQUIRE_EQ(delta(17, 500000), delta(3, 500000) * 5); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__scale_and_set); +ATF_TEST_CASE_BODY(delta__scale_and_set) +{ + using datetime::delta; + + { + delta d(3, 5); + d *= 2; + ATF_REQUIRE_EQ(delta(6, 10), d); + } + { + delta d(8, 0); + d *= 8; + ATF_REQUIRE_EQ(delta(64, 0), d); + } + { + delta d(3, 5); + ATF_REQUIRE_EQ(delta(9, 15), (d *= 3)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(delta__output); +ATF_TEST_CASE_BODY(delta__output) +{ + { + std::ostringstream str; + str << datetime::delta(15, 8791); + ATF_REQUIRE_EQ("15008791us", str.str()); + } + { + std::ostringstream str; + str << datetime::delta(12345678, 0); + ATF_REQUIRE_EQ("12345678000000us", str.str()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__copy); +ATF_TEST_CASE_BODY(timestamp__copy) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2011, 2, 16, 19, 15, 30, 0); + { + const datetime::timestamp ts2 = ts1; + const datetime::timestamp ts3 = datetime::timestamp::from_values( + 2012, 2, 16, 19, 15, 30, 0); + ATF_REQUIRE_EQ("2011", ts1.strftime("%Y")); + ATF_REQUIRE_EQ("2011", ts2.strftime("%Y")); + ATF_REQUIRE_EQ("2012", ts3.strftime("%Y")); + } + ATF_REQUIRE_EQ("2011", ts1.strftime("%Y")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__from_microseconds); +ATF_TEST_CASE_BODY(timestamp__from_microseconds) +{ + const datetime::timestamp ts = datetime::timestamp::from_microseconds( + 1328829351987654LL); + ATF_REQUIRE_EQ("2012-02-09 23:15:51", ts.strftime("%Y-%m-%d %H:%M:%S")); + ATF_REQUIRE_EQ(1328829351987654LL, ts.to_microseconds()); + ATF_REQUIRE_EQ(1328829351, ts.to_seconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__now__mock); +ATF_TEST_CASE_BODY(timestamp__now__mock) +{ + datetime::set_mock_now(2011, 2, 21, 18, 5, 10, 0); + ATF_REQUIRE_EQ("2011-02-21 18:05:10", + datetime::timestamp::now().strftime("%Y-%m-%d %H:%M:%S")); + + datetime::set_mock_now(datetime::timestamp::from_values( + 2012, 3, 22, 19, 6, 11, 54321)); + ATF_REQUIRE_EQ("2012-03-22 19:06:11", + datetime::timestamp::now().strftime("%Y-%m-%d %H:%M:%S")); + ATF_REQUIRE_EQ("2012-03-22 19:06:11", + datetime::timestamp::now().strftime("%Y-%m-%d %H:%M:%S")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__now__real); +ATF_TEST_CASE_BODY(timestamp__now__real) +{ + // This test is might fail if we happen to run at the crossing of one + // day to the other and the two measures we pick of the current time + // differ. This is so unlikely that I haven't bothered to do this in any + // other way. + + const time_t just_before = ::time(NULL); + const datetime::timestamp now = datetime::timestamp::now(); + + ::tm data; + char buf[1024]; + ATF_REQUIRE(::gmtime_r(&just_before, &data) != 0); + ATF_REQUIRE(::strftime(buf, sizeof(buf), "%Y-%m-%d", &data) != 0); + ATF_REQUIRE_EQ(buf, now.strftime("%Y-%m-%d")); + + ATF_REQUIRE(now.strftime("%Z") == "GMT" || now.strftime("%Z") == "UTC"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__now__granularity); +ATF_TEST_CASE_BODY(timestamp__now__granularity) +{ + const datetime::timestamp first = datetime::timestamp::now(); + ::usleep(1); + const datetime::timestamp second = datetime::timestamp::now(); + ATF_REQUIRE(first.to_microseconds() != second.to_microseconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__strftime); +ATF_TEST_CASE_BODY(timestamp__strftime) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2010, 12, 10, 8, 45, 50, 0); + ATF_REQUIRE_EQ("2010-12-10", ts1.strftime("%Y-%m-%d")); + ATF_REQUIRE_EQ("08:45:50", ts1.strftime("%H:%M:%S")); + + const datetime::timestamp ts2 = datetime::timestamp::from_values( + 2011, 2, 16, 19, 15, 30, 0); + ATF_REQUIRE_EQ("2011-02-16T19:15:30", ts2.strftime("%Y-%m-%dT%H:%M:%S")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__to_iso8601_in_utc); +ATF_TEST_CASE_BODY(timestamp__to_iso8601_in_utc) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2010, 12, 10, 8, 45, 50, 0); + ATF_REQUIRE_EQ("2010-12-10T08:45:50.000000Z", ts1.to_iso8601_in_utc()); + + const datetime::timestamp ts2= datetime::timestamp::from_values( + 2016, 7, 11, 17, 51, 28, 123456); + ATF_REQUIRE_EQ("2016-07-11T17:51:28.123456Z", ts2.to_iso8601_in_utc()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__to_microseconds); +ATF_TEST_CASE_BODY(timestamp__to_microseconds) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2010, 12, 10, 8, 45, 50, 123456); + ATF_REQUIRE_EQ(1291970750123456LL, ts1.to_microseconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__to_seconds); +ATF_TEST_CASE_BODY(timestamp__to_seconds) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2010, 12, 10, 8, 45, 50, 123456); + ATF_REQUIRE_EQ(1291970750, ts1.to_seconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__leap_second); +ATF_TEST_CASE_BODY(timestamp__leap_second) +{ + // This is actually a test for from_values(), which is the function that + // includes assertions to validate the input parameters. + const datetime::timestamp ts1 = datetime::timestamp::from_values( + 2012, 6, 30, 23, 59, 60, 543); + ATF_REQUIRE_EQ(1341100800, ts1.to_seconds()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__equals); +ATF_TEST_CASE_BODY(timestamp__equals) +{ + ATF_REQUIRE(datetime::timestamp::from_microseconds(1291970750123456LL) == + datetime::timestamp::from_microseconds(1291970750123456LL)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__differs); +ATF_TEST_CASE_BODY(timestamp__differs) +{ + ATF_REQUIRE(datetime::timestamp::from_microseconds(1291970750123456LL) != + datetime::timestamp::from_microseconds(1291970750123455LL)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__sorting); +ATF_TEST_CASE_BODY(timestamp__sorting) +{ + { + const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( + 1291970750123455LL); + const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( + 1291970750123455LL); + + ATF_REQUIRE(!(ts1 < ts2)); + ATF_REQUIRE( ts1 <= ts2); + ATF_REQUIRE(!(ts1 > ts2)); + ATF_REQUIRE( ts1 >= ts2); + } + { + const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( + 1291970750123455LL); + const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( + 1291970759123455LL); + + ATF_REQUIRE( ts1 < ts2); + ATF_REQUIRE( ts1 <= ts2); + ATF_REQUIRE(!(ts1 > ts2)); + ATF_REQUIRE(!(ts1 >= ts2)); + } + { + const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( + 1291970759123455LL); + const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( + 1291970750123455LL); + + ATF_REQUIRE(!(ts1 < ts2)); + ATF_REQUIRE(!(ts1 <= ts2)); + ATF_REQUIRE( ts1 > ts2); + ATF_REQUIRE( ts1 >= ts2); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__add_delta); +ATF_TEST_CASE_BODY(timestamp__add_delta) +{ + using datetime::delta; + using datetime::timestamp; + + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 30, 1234), + timestamp::from_values(2014, 12, 11, 21, 43, 0, 0) + + delta(30, 1234)); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 22, 43, 7, 100), + timestamp::from_values(2014, 12, 11, 21, 43, 0, 0) + + delta(3602, 5000100)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__add_delta_and_set); +ATF_TEST_CASE_BODY(timestamp__add_delta_and_set) +{ + using datetime::delta; + using datetime::timestamp; + + { + timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 0, 0); + ts += delta(30, 1234); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 30, 1234), + ts); + } + { + timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 0, 0); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 22, 43, 7, 100), + ts += delta(3602, 5000100)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__subtract_delta); +ATF_TEST_CASE_BODY(timestamp__subtract_delta) +{ + using datetime::delta; + using datetime::timestamp; + + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 10, 4321), + timestamp::from_values(2014, 12, 11, 21, 43, 40, 5555) - + delta(30, 1234)); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 20, 43, 1, 300), + timestamp::from_values(2014, 12, 11, 21, 43, 8, 400) - + delta(3602, 5000100)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__subtract_delta_and_set); +ATF_TEST_CASE_BODY(timestamp__subtract_delta_and_set) +{ + using datetime::delta; + using datetime::timestamp; + + { + timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 40, 5555); + ts -= delta(30, 1234); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 21, 43, 10, 4321), + ts); + } + { + timestamp ts = timestamp::from_values(2014, 12, 11, 21, 43, 8, 400); + ATF_REQUIRE_EQ(timestamp::from_values(2014, 12, 11, 20, 43, 1, 300), + ts -= delta(3602, 5000100)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__subtraction); +ATF_TEST_CASE_BODY(timestamp__subtraction) +{ + const datetime::timestamp ts1 = datetime::timestamp::from_microseconds( + 1291970750123456LL); + const datetime::timestamp ts2 = datetime::timestamp::from_microseconds( + 1291970750123468LL); + const datetime::timestamp ts3 = datetime::timestamp::from_microseconds( + 1291970850123456LL); + + ATF_REQUIRE_EQ(datetime::delta(0, 0), ts1 - ts1); + ATF_REQUIRE_EQ(datetime::delta(0, 12), ts2 - ts1); + ATF_REQUIRE_EQ(datetime::delta(100, 0), ts3 - ts1); + ATF_REQUIRE_EQ(datetime::delta(99, 999988), ts3 - ts2); + + ATF_REQUIRE_THROW_RE( + std::runtime_error, + "Cannot subtract 1291970850123456us from 1291970750123468us " + ".*negative datetime::delta.*not supported", + ts2 - ts3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(timestamp__output); +ATF_TEST_CASE_BODY(timestamp__output) +{ + { + std::ostringstream str; + str << datetime::timestamp::from_microseconds(1291970750123456LL); + ATF_REQUIRE_EQ("1291970750123456us", str.str()); + } + { + std::ostringstream str; + str << datetime::timestamp::from_microseconds(1028309798759812LL); + ATF_REQUIRE_EQ("1028309798759812us", str.str()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, delta__defaults); + ATF_ADD_TEST_CASE(tcs, delta__overrides); + ATF_ADD_TEST_CASE(tcs, delta__from_microseconds); + ATF_ADD_TEST_CASE(tcs, delta__to_microseconds); + ATF_ADD_TEST_CASE(tcs, delta__equals); + ATF_ADD_TEST_CASE(tcs, delta__differs); + ATF_ADD_TEST_CASE(tcs, delta__sorting); + ATF_ADD_TEST_CASE(tcs, delta__addition); + ATF_ADD_TEST_CASE(tcs, delta__addition_and_set); + ATF_ADD_TEST_CASE(tcs, delta__scale); + ATF_ADD_TEST_CASE(tcs, delta__scale_and_set); + ATF_ADD_TEST_CASE(tcs, delta__output); + + ATF_ADD_TEST_CASE(tcs, timestamp__copy); + ATF_ADD_TEST_CASE(tcs, timestamp__from_microseconds); + ATF_ADD_TEST_CASE(tcs, timestamp__now__mock); + ATF_ADD_TEST_CASE(tcs, timestamp__now__real); + ATF_ADD_TEST_CASE(tcs, timestamp__now__granularity); + ATF_ADD_TEST_CASE(tcs, timestamp__strftime); + ATF_ADD_TEST_CASE(tcs, timestamp__to_iso8601_in_utc); + ATF_ADD_TEST_CASE(tcs, timestamp__to_microseconds); + ATF_ADD_TEST_CASE(tcs, timestamp__to_seconds); + ATF_ADD_TEST_CASE(tcs, timestamp__leap_second); + ATF_ADD_TEST_CASE(tcs, timestamp__equals); + ATF_ADD_TEST_CASE(tcs, timestamp__differs); + ATF_ADD_TEST_CASE(tcs, timestamp__sorting); + ATF_ADD_TEST_CASE(tcs, timestamp__add_delta); + ATF_ADD_TEST_CASE(tcs, timestamp__add_delta_and_set); + ATF_ADD_TEST_CASE(tcs, timestamp__subtract_delta); + ATF_ADD_TEST_CASE(tcs, timestamp__subtract_delta_and_set); + ATF_ADD_TEST_CASE(tcs, timestamp__subtraction); + ATF_ADD_TEST_CASE(tcs, timestamp__output); +} diff --git a/utils/defs.hpp.in b/utils/defs.hpp.in new file mode 100644 index 000000000000..62fc50d0e525 --- /dev/null +++ b/utils/defs.hpp.in @@ -0,0 +1,57 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/defs.hpp +/// +/// Definitions for compiler and system features autodetected at configuration +/// time. + +#if !defined(UTILS_DEFS_HPP) +#define UTILS_DEFS_HPP + + +/// Attribute to mark a function as non-returning. +#define UTILS_NORETURN @ATTRIBUTE_NORETURN@ + + +/// Attribute to mark a function as pure. +#define UTILS_PURE @ATTRIBUTE_PURE@ + + +/// Attribute to mark an entity as unused. +#define UTILS_UNUSED @ATTRIBUTE_UNUSED@ + + +/// Unconstifies a pointer. +/// +/// \param type The target type of the conversion. +/// \param ptr The pointer to be unconstified. +#define UTILS_UNCONST(type, ptr) ((type*)(unsigned long)(const void*)(ptr)) + + +#endif // !defined(UTILS_DEFS_HPP) diff --git a/utils/env.cpp b/utils/env.cpp new file mode 100644 index 000000000000..b0d995c0ff31 --- /dev/null +++ b/utils/env.cpp @@ -0,0 +1,200 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/env.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include +#include +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" + +namespace fs = utils::fs; + +using utils::none; +using utils::optional; + + +extern "C" { + extern char** environ; +} + + +/// Gets all environment variables. +/// +/// \return A mapping of (name, value) pairs describing the environment +/// variables. +std::map< std::string, std::string > +utils::getallenv(void) +{ + std::map< std::string, std::string > allenv; + for (char** envp = environ; *envp != NULL; envp++) { + const std::string oneenv = *envp; + const std::string::size_type pos = oneenv.find('='); + const std::string name = oneenv.substr(0, pos); + const std::string value = oneenv.substr(pos + 1); + + PRE(allenv.find(name) == allenv.end()); + allenv[name] = value; + } + return allenv; +} + + +/// Gets the value of an environment variable. +/// +/// \param name The name of the environment variable to query. +/// +/// \return The value of the environment variable if it is defined, or none +/// otherwise. +optional< std::string > +utils::getenv(const std::string& name) +{ + const char* value = std::getenv(name.c_str()); + if (value == NULL) { + LD(F("Environment variable '%s' is not defined") % name); + return none; + } else { + LD(F("Environment variable '%s' is '%s'") % name % value); + return utils::make_optional(std::string(value)); + } +} + + +/// Gets the value of an environment variable with a default fallback. +/// +/// \param name The name of the environment variable to query. +/// \param default_value The value to return if the variable is not defined. +/// +/// \return The value of the environment variable. +std::string +utils::getenv_with_default(const std::string& name, + const std::string& default_value) +{ + const char* value = std::getenv(name.c_str()); + if (value == NULL) { + LD(F("Environment variable '%s' is not defined; using default '%s'") % + name % default_value); + return default_value; + } else { + LD(F("Environment variable '%s' is '%s'") % name % value); + return value; + } +} + + +/// Gets the value of the HOME environment variable with path validation. +/// +/// \return The value of the HOME environment variable if it is a valid path; +/// none if it is not defined or if it contains an invalid path. +optional< fs::path > +utils::get_home(void) +{ + const optional< std::string > home = utils::getenv("HOME"); + if (home) { + try { + return utils::make_optional(fs::path(home.get())); + } catch (const fs::error& e) { + LW(F("Invalid value '%s' in HOME environment variable: %s") % + home.get() % e.what()); + return none; + } + } else { + return none; + } +} + + +/// Sets the value of an environment variable. +/// +/// \param name The name of the environment variable to set. +/// \param val The value to set the environment variable to. May be empty. +/// +/// \throw std::runtime_error If there is an error setting the environment +/// variable. +void +utils::setenv(const std::string& name, const std::string& val) +{ + LD(F("Setting environment variable '%s' to '%s'") % name % val); +#if defined(HAVE_SETENV) + if (::setenv(name.c_str(), val.c_str(), 1) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to set environment variable '%s' to " + "'%s': %s") % + name % val % std::strerror(original_errno)); + } +#elif defined(HAVE_PUTENV) + if (::putenv((F("%s=%s") % name % val).c_str()) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to set environment variable '%s' to " + "'%s': %s") % + name % val % std::strerror(original_errno)); + } +#else +# error "Don't know how to set an environment variable." +#endif +} + + +/// Unsets an environment variable. +/// +/// \param name The name of the environment variable to unset. +/// +/// \throw std::runtime_error If there is an error unsetting the environment +/// variable. +void +utils::unsetenv(const std::string& name) +{ + LD(F("Unsetting environment variable '%s'") % name); +#if defined(HAVE_UNSETENV) + if (::unsetenv(name.c_str()) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to unset environment variable " + "'%s'") % + name % std::strerror(original_errno)); + } +#elif defined(HAVE_PUTENV) + if (::putenv((F("%s=") % name).c_str()) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to unset environment variable " + "'%s'") % + name % std::strerror(original_errno)); + } +#else +# error "Don't know how to unset an environment variable." +#endif +} diff --git a/utils/env.hpp b/utils/env.hpp new file mode 100644 index 000000000000..2370ee490dc1 --- /dev/null +++ b/utils/env.hpp @@ -0,0 +1,58 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/env.hpp +/// Querying and manipulation of environment variables. +/// +/// These utility functions wrap the system functions to manipulate the +/// environment in a portable way and expose their arguments and return values +/// in a C++-friendly manner. + +#if !defined(UTILS_ENV_HPP) +#define UTILS_ENV_HPP + +#include +#include + +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace utils { + + +std::map< std::string, std::string > getallenv(void); +optional< std::string > getenv(const std::string&); +std::string getenv_with_default(const std::string&, const std::string&); +optional< utils::fs::path > get_home(void); +void setenv(const std::string&, const std::string&); +void unsetenv(const std::string&); + + +} // namespace utils + +#endif // !defined(UTILS_ENV_HPP) diff --git a/utils/env_test.cpp b/utils/env_test.cpp new file mode 100644 index 000000000000..1b16266443af --- /dev/null +++ b/utils/env_test.cpp @@ -0,0 +1,167 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/env.hpp" + +#include + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" + +namespace fs = utils::fs; + +using utils::optional; + + +ATF_TEST_CASE_WITHOUT_HEAD(getallenv); +ATF_TEST_CASE_BODY(getallenv) +{ + utils::unsetenv("test-missing"); + utils::setenv("test-empty", ""); + utils::setenv("test-text", "some-value"); + + const std::map< std::string, std::string > allenv = utils::getallenv(); + + { + const std::map< std::string, std::string >::const_iterator iter = + allenv.find("test-missing"); + ATF_REQUIRE(iter == allenv.end()); + } + + { + const std::map< std::string, std::string >::const_iterator iter = + allenv.find("test-empty"); + ATF_REQUIRE(iter != allenv.end()); + ATF_REQUIRE((*iter).second.empty()); + } + + { + const std::map< std::string, std::string >::const_iterator iter = + allenv.find("test-text"); + ATF_REQUIRE(iter != allenv.end()); + ATF_REQUIRE_EQ("some-value", (*iter).second); + } + + if (utils::getenv("PATH")) { + const std::map< std::string, std::string >::const_iterator iter = + allenv.find("PATH"); + ATF_REQUIRE(iter != allenv.end()); + ATF_REQUIRE_EQ(utils::getenv("PATH").get(), (*iter).second); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(getenv); +ATF_TEST_CASE_BODY(getenv) +{ + const optional< std::string > path = utils::getenv("PATH"); + ATF_REQUIRE(path); + ATF_REQUIRE(!path.get().empty()); + + ATF_REQUIRE(!utils::getenv("__UNDEFINED_VARIABLE__")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(getenv_with_default); +ATF_TEST_CASE_BODY(getenv_with_default) +{ + ATF_REQUIRE("don't use" != + utils::getenv_with_default("PATH", "don't use")); + + ATF_REQUIRE_EQ("foo", + utils::getenv_with_default("__UNDEFINED_VARIABLE__", "foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(get_home__ok); +ATF_TEST_CASE_BODY(get_home__ok) +{ + const fs::path home("/foo/bar"); + utils::setenv("HOME", home.str()); + const optional< fs::path > computed = utils::get_home(); + ATF_REQUIRE(computed); + ATF_REQUIRE_EQ(home, computed.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(get_home__missing); +ATF_TEST_CASE_BODY(get_home__missing) +{ + utils::unsetenv("HOME"); + ATF_REQUIRE(!utils::get_home()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(get_home__invalid); +ATF_TEST_CASE_BODY(get_home__invalid) +{ + utils::setenv("HOME", ""); + ATF_REQUIRE(!utils::get_home()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(setenv); +ATF_TEST_CASE_BODY(setenv) +{ + ATF_REQUIRE(utils::getenv("PATH")); + const std::string oldval = utils::getenv("PATH").get(); + utils::setenv("PATH", "foo-bar"); + ATF_REQUIRE(utils::getenv("PATH").get() != oldval); + ATF_REQUIRE_EQ("foo-bar", utils::getenv("PATH").get()); + + ATF_REQUIRE(!utils::getenv("__UNDEFINED_VARIABLE__")); + utils::setenv("__UNDEFINED_VARIABLE__", "foo2-bar2"); + ATF_REQUIRE_EQ("foo2-bar2", utils::getenv("__UNDEFINED_VARIABLE__").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unsetenv); +ATF_TEST_CASE_BODY(unsetenv) +{ + ATF_REQUIRE(utils::getenv("PATH")); + utils::unsetenv("PATH"); + ATF_REQUIRE(!utils::getenv("PATH")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, getallenv); + + ATF_ADD_TEST_CASE(tcs, getenv); + + ATF_ADD_TEST_CASE(tcs, getenv_with_default); + + ATF_ADD_TEST_CASE(tcs, get_home__ok); + ATF_ADD_TEST_CASE(tcs, get_home__missing); + ATF_ADD_TEST_CASE(tcs, get_home__invalid); + + ATF_ADD_TEST_CASE(tcs, setenv); + + ATF_ADD_TEST_CASE(tcs, unsetenv); +} diff --git a/utils/format/Kyuafile b/utils/format/Kyuafile new file mode 100644 index 000000000000..344ae455422c --- /dev/null +++ b/utils/format/Kyuafile @@ -0,0 +1,7 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="containers_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="formatter_test"} diff --git a/utils/format/Makefile.am.inc b/utils/format/Makefile.am.inc new file mode 100644 index 000000000000..a37fc4057079 --- /dev/null +++ b/utils/format/Makefile.am.inc @@ -0,0 +1,59 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +libutils_a_SOURCES += utils/format/containers.hpp +libutils_a_SOURCES += utils/format/containers.ipp +libutils_a_SOURCES += utils/format/exceptions.cpp +libutils_a_SOURCES += utils/format/exceptions.hpp +libutils_a_SOURCES += utils/format/formatter.cpp +libutils_a_SOURCES += utils/format/formatter.hpp +libutils_a_SOURCES += utils/format/formatter_fwd.hpp +libutils_a_SOURCES += utils/format/formatter.ipp +libutils_a_SOURCES += utils/format/macros.hpp + +if WITH_ATF +tests_utils_formatdir = $(pkgtestsdir)/utils/format + +tests_utils_format_DATA = utils/format/Kyuafile +EXTRA_DIST += $(tests_utils_format_DATA) + +tests_utils_format_PROGRAMS = utils/format/containers_test +utils_format_containers_test_SOURCES = utils/format/containers_test.cpp +utils_format_containers_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_format_containers_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_format_PROGRAMS += utils/format/exceptions_test +utils_format_exceptions_test_SOURCES = utils/format/exceptions_test.cpp +utils_format_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_format_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_format_PROGRAMS += utils/format/formatter_test +utils_format_formatter_test_SOURCES = utils/format/formatter_test.cpp +utils_format_formatter_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_format_formatter_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/format/containers.hpp b/utils/format/containers.hpp new file mode 100644 index 000000000000..7334c250de4e --- /dev/null +++ b/utils/format/containers.hpp @@ -0,0 +1,66 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/format/containers.hpp +/// Overloads to support formatting various base container types. + +#if !defined(UTILS_FORMAT_CONTAINERS_HPP) +#define UTILS_FORMAT_CONTAINERS_HPP + +#include +#include +#include +#include +#include +#include + + +// This is ugly but necessary for C++ name resolution. Unsure if we'd do it +// differently... +namespace std { + + +template< typename K, typename V > +std::ostream& operator<<(std::ostream&, const std::map< K, V >&); + +template< typename T1, typename T2 > +std::ostream& operator<<(std::ostream&, const std::pair< T1, T2 >&); + +template< typename T > +std::ostream& operator<<(std::ostream&, const std::shared_ptr< T >); + +template< typename T > +std::ostream& operator<<(std::ostream&, const std::set< T >&); + +template< typename T > +std::ostream& operator<<(std::ostream&, const std::vector< T >&); + + +} // namespace std + +#endif // !defined(UTILS_FORMAT_CONTAINERS_HPP) diff --git a/utils/format/containers.ipp b/utils/format/containers.ipp new file mode 100644 index 000000000000..11d8e2914149 --- /dev/null +++ b/utils/format/containers.ipp @@ -0,0 +1,138 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_FORMAT_CONTAINERS_IPP) +#define UTILS_FORMAT_CONTAINERS_IPP + +#include "utils/format/containers.hpp" + +#include + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename K, typename V > +std::ostream& +std::operator<<(std::ostream& output, const std::map< K, V >& object) +{ + output << "map("; + typename std::map< K, V >::size_type counter = 0; + for (typename std::map< K, V >::const_iterator iter = object.begin(); + iter != object.end(); ++iter, ++counter) { + if (counter != 0) + output << ", "; + output << (*iter).first << "=" << (*iter).second; + } + output << ")"; + return output; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename T1, typename T2 > +std::ostream& +std::operator<<(std::ostream& output, const std::pair< T1, T2 >& object) +{ + output << "pair(" << object.first << ", " << object.second << ")"; + return output; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename T > +std::ostream& +std::operator<<(std::ostream& output, const std::shared_ptr< T > object) +{ + if (object.get() == NULL) { + output << ""; + } else { + output << *object; + } + return output; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename T > +std::ostream& +std::operator<<(std::ostream& output, const std::set< T >& object) +{ + output << "set("; + typename std::set< T >::size_type counter = 0; + for (typename std::set< T >::const_iterator iter = object.begin(); + iter != object.end(); ++iter, ++counter) { + if (counter != 0) + output << ", "; + output << (*iter); + } + output << ")"; + return output; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< typename T > +std::ostream& +std::operator<<(std::ostream& output, const std::vector< T >& object) +{ + output << "["; + for (typename std::vector< T >::size_type i = 0; i < object.size(); ++i) { + if (i != 0) + output << ", "; + output << object[i]; + } + output << "]"; + return output; +} + + +#endif // !defined(UTILS_FORMAT_CONTAINERS_IPP) diff --git a/utils/format/containers_test.cpp b/utils/format/containers_test.cpp new file mode 100644 index 000000000000..e1c452da2df6 --- /dev/null +++ b/utils/format/containers_test.cpp @@ -0,0 +1,190 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/format/containers.ipp" + +#include +#include +#include +#include +#include +#include + +#include + + + +namespace { + + +/// Formats a value and compares it to an expected string. +/// +/// \tparam T The type of the value to format. +/// \param expected Expected formatted text. +/// \param actual The value to format. +/// +/// \post Fails the test case if the formatted actual value does not match +/// the provided expected string. +template< typename T > +static void +do_check(const char* expected, const T& actual) +{ + std::ostringstream str; + str << actual; + ATF_REQUIRE_EQ(expected, str.str()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(std_map__empty); +ATF_TEST_CASE_BODY(std_map__empty) +{ + do_check("map()", std::map< char, char >()); + do_check("map()", std::map< int, long >()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_map__some); +ATF_TEST_CASE_BODY(std_map__some) +{ + { + std::map< char, int > v; + v['b'] = 123; + v['z'] = 321; + do_check("map(b=123, z=321)", v); + } + + { + std::map< int, std::string > v; + v[5] = "first"; + v[2] = "second"; + v[8] = "third"; + do_check("map(2=second, 5=first, 8=third)", v); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_pair); +ATF_TEST_CASE_BODY(std_pair) +{ + do_check("pair(5, b)", std::pair< int, char >(5, 'b')); + do_check("pair(foo bar, baz)", + std::pair< std::string, std::string >("foo bar", "baz")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_shared_ptr__null); +ATF_TEST_CASE_BODY(std_shared_ptr__null) +{ + do_check("", std::shared_ptr< char >()); + do_check("", std::shared_ptr< int >()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_shared_ptr__not_null); +ATF_TEST_CASE_BODY(std_shared_ptr__not_null) +{ + do_check("f", std::shared_ptr< char >(new char('f'))); + do_check("8", std::shared_ptr< int >(new int(8))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_set__empty); +ATF_TEST_CASE_BODY(std_set__empty) +{ + do_check("set()", std::set< char >()); + do_check("set()", std::set< int >()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_set__some); +ATF_TEST_CASE_BODY(std_set__some) +{ + { + std::set< char > v; + v.insert('b'); + v.insert('z'); + do_check("set(b, z)", v); + } + + { + std::set< int > v; + v.insert(5); + v.insert(2); + v.insert(8); + do_check("set(2, 5, 8)", v); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_vector__empty); +ATF_TEST_CASE_BODY(std_vector__empty) +{ + do_check("[]", std::vector< char >()); + do_check("[]", std::vector< int >()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(std_vector__some); +ATF_TEST_CASE_BODY(std_vector__some) +{ + { + std::vector< char > v; + v.push_back('b'); + v.push_back('z'); + do_check("[b, z]", v); + } + + { + std::vector< int > v; + v.push_back(5); + v.push_back(2); + v.push_back(8); + do_check("[5, 2, 8]", v); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, std_map__empty); + ATF_ADD_TEST_CASE(tcs, std_map__some); + + ATF_ADD_TEST_CASE(tcs, std_pair); + + ATF_ADD_TEST_CASE(tcs, std_shared_ptr__null); + ATF_ADD_TEST_CASE(tcs, std_shared_ptr__not_null); + + ATF_ADD_TEST_CASE(tcs, std_set__empty); + ATF_ADD_TEST_CASE(tcs, std_set__some); + + ATF_ADD_TEST_CASE(tcs, std_vector__empty); + ATF_ADD_TEST_CASE(tcs, std_vector__some); +} diff --git a/utils/format/exceptions.cpp b/utils/format/exceptions.cpp new file mode 100644 index 000000000000..299b1d23cd8d --- /dev/null +++ b/utils/format/exceptions.cpp @@ -0,0 +1,110 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/format/exceptions.hpp" + +using utils::format::bad_format_error; +using utils::format::error; +using utils::format::extra_args_error; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +error::~error(void) throw() +{ +} + + +/// Constructs a new bad_format_error. +/// +/// \param format_ The invalid format string. +/// \param message Description of the error in the format string. +bad_format_error::bad_format_error(const std::string& format_, + const std::string& message) : + error("Invalid formatting string '" + format_ + "': " + message), + _format(format_) +{ +} + + +/// Destructor for the error. +bad_format_error::~bad_format_error(void) throw() +{ +} + + +/// \return The format string that caused the error. +const std::string& +bad_format_error::format(void) const +{ + return _format; +} + + +/// Constructs a new extra_args_error. +/// +/// \param format_ The format string. +/// \param arg_ The first extra argument passed to the format string. +extra_args_error::extra_args_error(const std::string& format_, + const std::string& arg_) : + error("Not enough fields in formatting string '" + format_ + "' to place " + "argument '" + arg_ + "'"), + _format(format_), + _arg(arg_) +{ +} + + +/// Destructor for the error. +extra_args_error::~extra_args_error(void) throw() +{ +} + + +/// \return The format string that was passed too many arguments. +const std::string& +extra_args_error::format(void) const +{ + return _format; +} + + +/// \return The first argument that caused the error. +const std::string& +extra_args_error::arg(void) const +{ + return _arg; +} diff --git a/utils/format/exceptions.hpp b/utils/format/exceptions.hpp new file mode 100644 index 000000000000..a28376df9c08 --- /dev/null +++ b/utils/format/exceptions.hpp @@ -0,0 +1,84 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/format/exceptions.hpp +/// Exception types raised by the format module. + +#if !defined(UTILS_FORMAT_EXCEPTIONS_HPP) +#define UTILS_FORMAT_EXCEPTIONS_HPP + +#include +#include + +namespace utils { +namespace format { + + +/// Base exception for format errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// Error denoting a bad format string. +class bad_format_error : public error { + /// The format string that caused the error. + std::string _format; + +public: + explicit bad_format_error(const std::string&, const std::string&); + virtual ~bad_format_error(void) throw(); + + const std::string& format(void) const; +}; + + +/// Error denoting too many arguments for the format string. +class extra_args_error : public error { + /// The format string that was passed too many arguments. + std::string _format; + + /// The first argument that caused the error. + std::string _arg; + +public: + explicit extra_args_error(const std::string&, const std::string&); + virtual ~extra_args_error(void) throw(); + + const std::string& format(void) const; + const std::string& arg(void) const; +}; + + +} // namespace format +} // namespace utils + + +#endif // !defined(UTILS_FORMAT_EXCEPTIONS_HPP) diff --git a/utils/format/exceptions_test.cpp b/utils/format/exceptions_test.cpp new file mode 100644 index 000000000000..28d401e57dad --- /dev/null +++ b/utils/format/exceptions_test.cpp @@ -0,0 +1,74 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/format/exceptions.hpp" + +#include + +#include + +using utils::format::bad_format_error; +using utils::format::error; +using utils::format::extra_args_error; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bad_format_error); +ATF_TEST_CASE_BODY(bad_format_error) +{ + const bad_format_error e("format-string", "the-error"); + ATF_REQUIRE(std::strcmp("Invalid formatting string 'format-string': " + "the-error", e.what()) == 0); + ATF_REQUIRE_EQ("format-string", e.format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(extra_args_error); +ATF_TEST_CASE_BODY(extra_args_error) +{ + const extra_args_error e("fmt", "extra"); + ATF_REQUIRE(std::strcmp("Not enough fields in formatting string 'fmt' to " + "place argument 'extra'", e.what()) == 0); + ATF_REQUIRE_EQ("fmt", e.format()); + ATF_REQUIRE_EQ("extra", e.arg()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, bad_format_error); + ATF_ADD_TEST_CASE(tcs, extra_args_error); +} diff --git a/utils/format/formatter.cpp b/utils/format/formatter.cpp new file mode 100644 index 000000000000..99cfd40f03ab --- /dev/null +++ b/utils/format/formatter.cpp @@ -0,0 +1,293 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/format/formatter.hpp" + +#include +#include +#include + +#include "utils/format/exceptions.hpp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace format = utils::format; +namespace text = utils::text; + + +namespace { + + +/// Finds the next placeholder in a string. +/// +/// \param format The original format string provided by the user; needed for +/// error reporting purposes only. +/// \param expansion The string containing the placeholder to look for. Any +/// '%%' in the string will be skipped, and they must be stripped later by +/// strip_double_percent(). +/// \param begin The position from which to start looking for the next +/// placeholder. +/// +/// \return The position in the string in which the placeholder is located and +/// the placeholder itself. If there are no placeholders left, this returns +/// the length of the string and an empty string. +/// +/// \throw bad_format_error If the input string contains a trailing formatting +/// character. We cannot detect any other kind of invalid formatter because +/// we do not implement a full parser for them. +static std::pair< std::string::size_type, std::string > +find_next_placeholder(const std::string& format, + const std::string& expansion, + std::string::size_type begin) +{ + begin = expansion.find('%', begin); + while (begin != std::string::npos && expansion[begin + 1] == '%') + begin = expansion.find('%', begin + 2); + if (begin == std::string::npos) + return std::make_pair(expansion.length(), ""); + if (begin == expansion.length() - 1) + throw format::bad_format_error(format, "Trailing %"); + + std::string::size_type end = begin + 1; + while (end < expansion.length() && expansion[end] != 's') + end++; + const std::string placeholder = expansion.substr(begin, end - begin + 1); + if (end == expansion.length() || + placeholder.find('%', 1) != std::string::npos) + throw format::bad_format_error(format, "Unterminated placeholder '" + + placeholder + "'"); + return std::make_pair(begin, placeholder); +} + + +/// Converts a string to an integer. +/// +/// \param format The format string; for error reporting purposes only. +/// \param str The string to conver. +/// \param what The name of the field this integer belongs to; for error +/// reporting purposes only. +/// +/// \return An integer representing the input string. +inline int +to_int(const std::string& format, const std::string& str, const char* what) +{ + try { + return text::to_type< int >(str); + } catch (const text::value_error& e) { + throw format::bad_format_error(format, "Invalid " + std::string(what) + + "specifier"); + } +} + + +/// Constructs an std::ostringstream based on a formatting placeholder. +/// +/// \param format The format placeholder; may be empty. +/// +/// \return A new std::ostringstream that is prepared to format a single +/// object in the manner specified by the format placeholder. +/// +/// \throw bad_format_error If the format string is bad. We do minimal +/// validation on this string though. +static std::ostringstream* +new_ostringstream(const std::string& format) +{ + std::auto_ptr< std::ostringstream > output(new std::ostringstream()); + + if (format.length() <= 2) { + // If the format is empty, we create a new stream so that we don't have + // to check for NULLs later on. We rarely should hit this condition + // (and when we do it's a bug in the caller), so this is not a big deal. + // + // Otherwise, if the format is a regular '%s', then we don't have to do + // any processing for additional formatters. So this is just a "fast + // path". + } else { + std::string partial = format.substr(1, format.length() - 2); + if (partial[0] == '0') { + output->fill('0'); + partial.erase(0, 1); + } + if (!partial.empty()) { + const std::string::size_type dot = partial.find('.'); + if (dot != 0) + output->width(to_int(format, partial.substr(0, dot), "width")); + if (dot != std::string::npos) { + output->setf(std::ios::fixed, std::ios::floatfield); + output->precision(to_int(format, partial.substr(dot + 1), + "precision")); + } + } + } + + return output.release(); +} + + +/// Replaces '%%' by '%' in a given string range. +/// +/// \param in The input string to be rewritten. +/// \param begin The position at which to start the replacement. +/// \param end The position at which to end the replacement. +/// +/// \return The modified string and the amount of characters removed. +static std::pair< std::string, int > +strip_double_percent(const std::string& in, const std::string::size_type begin, + std::string::size_type end) +{ + std::string part = in.substr(begin, end - begin); + + int removed = 0; + std::string::size_type pos = part.find("%%"); + while (pos != std::string::npos) { + part.erase(pos, 1); + ++removed; + pos = part.find("%%", pos + 1); + } + + return std::make_pair(in.substr(0, begin) + part + in.substr(end), removed); +} + + +} // anonymous namespace + + +/// Performs internal initialization of the formatter. +/// +/// This is separate from the constructor just because it is shared by different +/// overloaded constructors. +void +format::formatter::init(void) +{ + const std::pair< std::string::size_type, std::string > placeholder = + find_next_placeholder(_format, _expansion, _last_pos); + const std::pair< std::string, int > no_percents = + strip_double_percent(_expansion, _last_pos, placeholder.first); + + _oss = new_ostringstream(placeholder.second); + + _expansion = no_percents.first; + _placeholder_pos = placeholder.first - no_percents.second; + _placeholder = placeholder.second; +} + + +/// Constructs a new formatter object (internal). +/// +/// \param format The format string. +/// \param expansion The format string with any replacements performed so far. +/// \param last_pos The position from which to start looking for formatting +/// placeholders. This must be maintained in case one of the replacements +/// introduced a new placeholder, which must be ignored. Think, for +/// example, replacing a "%s" string with "foo %s". +format::formatter::formatter(const std::string& format, + const std::string& expansion, + const std::string::size_type last_pos) : + _format(format), + _expansion(expansion), + _last_pos(last_pos), + _oss(NULL) +{ + init(); +} + + +/// Constructs a new formatter object. +/// +/// \param format The format string. The formatters in the string are not +/// validated during construction, but will cause errors when used later if +/// they are invalid. +format::formatter::formatter(const std::string& format) : + _format(format), + _expansion(format), + _last_pos(0), + _oss(NULL) +{ + init(); +} + + +format::formatter::~formatter(void) +{ + delete _oss; +} + + +/// Returns the formatted string. +/// +/// \return A string representation of the formatted string. +const std::string& +format::formatter::str(void) const +{ + return _expansion; +} + + +/// Automatic conversion of formatter objects to strings. +/// +/// This is provided to allow painless injection of formatter objects into +/// streams, without having to manually call the str() method. +format::formatter::operator const std::string&(void) const +{ + return _expansion; +} + + +/// Specialization of operator% for booleans. +/// +/// \param value The boolean to inject into the format string. +/// +/// \return A new formatter that has one less format placeholder. +format::formatter +format::formatter::operator%(const bool& value) const +{ + (*_oss) << (value ? "true" : "false"); + return replace(_oss->str()); +} + + +/// Replaces the first formatting placeholder with a value. +/// +/// \param arg The replacement string. +/// +/// \return A new formatter in which the first formatting placeholder has been +/// replaced by arg and is ready to replace the next item. +/// +/// \throw utils::format::extra_args_error If there are no more formatting +/// placeholders in the input string, or if the placeholder is invalid. +format::formatter +format::formatter::replace(const std::string& arg) const +{ + if (_placeholder_pos == _expansion.length()) + throw format::extra_args_error(_format, arg); + + const std::string expansion = _expansion.substr(0, _placeholder_pos) + + arg + _expansion.substr(_placeholder_pos + _placeholder.length()); + return formatter(_format, expansion, _placeholder_pos + arg.length()); +} diff --git a/utils/format/formatter.hpp b/utils/format/formatter.hpp new file mode 100644 index 000000000000..8c6188745a2e --- /dev/null +++ b/utils/format/formatter.hpp @@ -0,0 +1,123 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/format/formatter.hpp +/// Provides the definition of the utils::format::formatter class. +/// +/// The utils::format::formatter class is a poor man's replacement for the +/// Boost.Format library, as it is much simpler and has less dependencies. +/// +/// Be aware that the formatting supported by this module is NOT compatible +/// with printf(3) nor with Boost.Format. The general syntax for a +/// placeholder in a formatting string is: +/// +/// %[0][width][.precision]s +/// +/// In particular, note that the only valid formatting specifier is %s: the +/// library deduces what to print based on the type of the variable passed +/// in, not based on what the format string says. Also, note that the only +/// valid padding character is 0. + +#if !defined(UTILS_FORMAT_FORMATTER_HPP) +#define UTILS_FORMAT_FORMATTER_HPP + +#include "utils/format/formatter_fwd.hpp" + +#include +#include + +namespace utils { +namespace format { + + +/// Mechanism to format strings similar to printf. +/// +/// A formatter always maintains the original format string but also holds a +/// partial expansion. The partial expansion is immutable in the context of a +/// formatter instance, but calls to operator% return new formatter objects with +/// one less formatting placeholder. +/// +/// In general, one can format a string in the following manner: +/// +/// \code +/// const std::string s = (formatter("%s %s") % "foo" % 5).str(); +/// \endcode +/// +/// which, following the explanation above, would correspond to: +/// +/// \code +/// const formatter f1("%s %s"); +/// const formatter f2 = f1 % "foo"; +/// const formatter f3 = f2 % 5; +/// const std::string s = f3.str(); +/// \endcode +class formatter { + /// The original format string provided by the user. + std::string _format; + + /// The current "expansion" of the format string. + /// + /// This field gets updated on every call to operator%() to have one less + /// formatting placeholder. + std::string _expansion; + + /// The position of _expansion from which to scan for placeholders. + std::string::size_type _last_pos; + + /// The position of the first placeholder in the current expansion. + std::string::size_type _placeholder_pos; + + /// The first placeholder in the current expansion. + std::string _placeholder; + + /// Stream used to format any possible argument supplied by operator%(). + std::ostringstream* _oss; + + formatter replace(const std::string&) const; + + void init(void); + formatter(const std::string&, const std::string&, + const std::string::size_type); + +public: + explicit formatter(const std::string&); + ~formatter(void); + + const std::string& str(void) const; + operator const std::string&(void) const; + + template< typename Type > formatter operator%(const Type&) const; + formatter operator%(const bool&) const; +}; + + +} // namespace format +} // namespace utils + + +#endif // !defined(UTILS_FORMAT_FORMATTER_HPP) diff --git a/utils/format/formatter.ipp b/utils/format/formatter.ipp new file mode 100644 index 000000000000..6fad024b704f --- /dev/null +++ b/utils/format/formatter.ipp @@ -0,0 +1,76 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_FORMAT_FORMATTER_IPP) +#define UTILS_FORMAT_FORMATTER_IPP + +#include + +#include "utils/format/formatter.hpp" + +namespace utils { +namespace format { + + +/// Replaces the first format placeholder in a formatter. +/// +/// Constructs a new formatter object that has one less formatting placeholder, +/// as this has been replaced by the provided argument. Calling this operator +/// N times, where N is the number of formatting placeholders, effectively +/// formats the string. +/// +/// \param arg The argument to use as replacement for the format placeholder. +/// +/// \return A new formatter that has one less format placeholder. +template< typename Type > +inline formatter +formatter::operator%(const Type& arg) const +{ + (*_oss) << arg; + return replace(_oss->str()); +} + + +/// Inserts a formatter string into a stream. +/// +/// \param os The output stream. +/// \param f The formatter to process and inject into the stream. +/// +/// \return The output stream os. +inline std::ostream& +operator<<(std::ostream& os, const formatter& f) +{ + return (os << f.str()); +} + + +} // namespace format +} // namespace utils + + +#endif // !defined(UTILS_FORMAT_FORMATTER_IPP) diff --git a/utils/format/formatter_fwd.hpp b/utils/format/formatter_fwd.hpp new file mode 100644 index 000000000000..72c9e5ebf196 --- /dev/null +++ b/utils/format/formatter_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/format/formatter_fwd.hpp +/// Forward declarations for utils/format/formatter.hpp + +#if !defined(UTILS_FORMAT_FORMATTER_FWD_HPP) +#define UTILS_FORMAT_FORMATTER_FWD_HPP + +namespace utils { +namespace format { + + +class formatter; + + +} // namespace format +} // namespace utils + +#endif // !defined(UTILS_FORMAT_FORMATTER_FWD_HPP) diff --git a/utils/format/formatter_test.cpp b/utils/format/formatter_test.cpp new file mode 100644 index 000000000000..fdae785b1db7 --- /dev/null +++ b/utils/format/formatter_test.cpp @@ -0,0 +1,265 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/format/formatter.hpp" + +#include + +#include + +#include "utils/format/exceptions.hpp" +#include "utils/format/macros.hpp" + +namespace format = utils::format; + + +namespace { + + +/// Wraps an integer in a C++ class. +/// +/// This custom type exists to ensure that we can feed arbitrary objects that +/// support operator<< to the formatter; +class int_wrapper { + /// The wrapped integer. + int _value; + +public: + /// Constructs a new wrapper. + /// + /// \param value_ The value to wrap. + int_wrapper(const int value_) : _value(value_) + { + } + + /// Returns the wrapped value. + /// + /// \return An integer. + int + value(void) const + { + return _value; + } +}; + + +/// Writes a wrapped integer into an output stream. +/// +/// \param output The output stream into which to place the integer. +/// \param wrapper The wrapped integer. +/// +/// \return The output stream. +std::ostream& +operator<<(std::ostream& output, const int_wrapper& wrapper) +{ + return (output << wrapper.value()); +} + + +} // anonymous namespace + + +/// Calls ATF_REQUIRE_EQ on an expected string and a formatter. +/// +/// This is pure syntactic sugar to avoid calling the str() method on all the +/// individual tests below, which results in very long lines that require +/// wrapping and clutter readability. +/// +/// \param expected The expected string generated by the formatter. +/// \param formatter The formatter to test. +#define EQ(expected, formatter) ATF_REQUIRE_EQ(expected, (formatter).str()) + + +ATF_TEST_CASE_WITHOUT_HEAD(no_fields); +ATF_TEST_CASE_BODY(no_fields) +{ + EQ("Plain string", F("Plain string")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(one_field); +ATF_TEST_CASE_BODY(one_field) +{ + EQ("foo", F("%sfoo") % ""); + EQ(" foo", F("%sfoo") % " "); + EQ("foo ", F("foo %s") % ""); + EQ("foo bar", F("foo %s") % "bar"); + EQ("foo bar baz", F("foo %s baz") % "bar"); + EQ("foo %s %s", F("foo %s %s") % "%s" % "%s"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(many_fields); +ATF_TEST_CASE_BODY(many_fields) +{ + EQ("", F("%s%s") % "" % ""); + EQ("foo", F("%s%s%s") % "" % "foo" % ""); + EQ("some 5 text", F("%s %s %s") % "some" % 5 % "text"); + EQ("f%s 5 text", F("%s %s %s") % "f%s" % 5 % "text"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(escape); +ATF_TEST_CASE_BODY(escape) +{ + EQ("%", F("%%")); + EQ("% %", F("%% %%")); + EQ("%% %%", F("%%%% %%%%")); + + EQ("foo %", F("foo %%")); + EQ("foo bar %", F("foo %s %%") % "bar"); + EQ("foo % bar", F("foo %% %s") % "bar"); + + EQ("foo %%", F("foo %s") % "%%"); + EQ("foo a%%b", F("foo a%sb") % "%%"); + EQ("foo a%%b", F("foo %s") % "a%%b"); + + EQ("foo % bar %%", F("foo %% %s %%%%") % "bar"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(extra_args_error); +ATF_TEST_CASE_BODY(extra_args_error) +{ + using format::extra_args_error; + + ATF_REQUIRE_THROW(extra_args_error, F("foo") % "bar"); + ATF_REQUIRE_THROW(extra_args_error, F("foo %%") % "bar"); + ATF_REQUIRE_THROW(extra_args_error, F("foo %s") % "bar" % "baz"); + ATF_REQUIRE_THROW(extra_args_error, F("foo %s") % "%s" % "bar"); + ATF_REQUIRE_THROW(extra_args_error, F("%s foo %s") % "bar" % "baz" % "foo"); + + try { + F("foo %s %s") % "bar" % "baz" % "something extra"; + fail("extra_args_error not raised"); + } catch (const extra_args_error& e) { + ATF_REQUIRE_EQ("foo %s %s", e.format()); + ATF_REQUIRE_EQ("something extra", e.arg()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__class); +ATF_TEST_CASE_BODY(format__class) +{ + EQ("foo bar", F("%s") % std::string("foo bar")); + EQ("3", F("%s") % int_wrapper(3)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__pointer); +ATF_TEST_CASE_BODY(format__pointer) +{ + EQ("0xcafebabe", F("%s") % reinterpret_cast< void* >(0xcafebabe)); + EQ("foo bar", F("%s") % "foo bar"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__bool); +ATF_TEST_CASE_BODY(format__bool) +{ + EQ("true", F("%s") % true); + EQ("false", F("%s") % false); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__char); +ATF_TEST_CASE_BODY(format__char) +{ + EQ("Z", F("%s") % 'Z'); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__float); +ATF_TEST_CASE_BODY(format__float) +{ + EQ("3", F("%s") % 3.0); + EQ("3.0", F("%.1s") % 3.0); + EQ("3.0", F("%0.1s") % 3.0); + EQ(" 15.600", F("%8.3s") % 15.6); + EQ("01.52", F("%05.2s") % 1.52); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__int); +ATF_TEST_CASE_BODY(format__int) +{ + EQ("3", F("%s") % 3); + EQ("3", F("%0s") % 3); + EQ(" -123", F("%5s") % -123); + EQ("00078", F("%05s") % 78); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format__error); +ATF_TEST_CASE_BODY(format__error) +{ + using format::bad_format_error; + + ATF_REQUIRE_THROW_RE(bad_format_error, "Trailing %", F("%")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Trailing %", F("f%")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Trailing %", F("f%%%")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Trailing %", F("ab %s cd%") % "cd"); + + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid width", F("%1bs")); + + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid precision", F("%.s")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid precision", F("%0.s")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid precision", F("%123.s")); + ATF_REQUIRE_THROW_RE(bad_format_error, "Invalid precision", F("%.12bs")); + + ATF_REQUIRE_THROW_RE(bad_format_error, "Unterminated", F("%c") % 'Z'); + ATF_REQUIRE_THROW_RE(bad_format_error, "Unterminated", F("%d") % 5); + ATF_REQUIRE_THROW_RE(bad_format_error, "Unterminated", F("%.1f") % 3); + ATF_REQUIRE_THROW_RE(bad_format_error, "Unterminated", F("%d%s") % 3 % "a"); + + try { + F("foo %s%") % "bar"; + fail("bad_format_error not raised"); + } catch (const bad_format_error& e) { + ATF_REQUIRE_EQ("foo %s%", e.format()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, no_fields); + ATF_ADD_TEST_CASE(tcs, one_field); + ATF_ADD_TEST_CASE(tcs, many_fields); + ATF_ADD_TEST_CASE(tcs, escape); + ATF_ADD_TEST_CASE(tcs, extra_args_error); + + ATF_ADD_TEST_CASE(tcs, format__class); + ATF_ADD_TEST_CASE(tcs, format__pointer); + ATF_ADD_TEST_CASE(tcs, format__bool); + ATF_ADD_TEST_CASE(tcs, format__char); + ATF_ADD_TEST_CASE(tcs, format__float); + ATF_ADD_TEST_CASE(tcs, format__int); + ATF_ADD_TEST_CASE(tcs, format__error); +} diff --git a/utils/format/macros.hpp b/utils/format/macros.hpp new file mode 100644 index 000000000000..09ef14ea485e --- /dev/null +++ b/utils/format/macros.hpp @@ -0,0 +1,58 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/format/macros.hpp +/// Convenience macros to simplify usage of the format library. +/// +/// This file must not be included from other header files. + +#if !defined(UTILS_FORMAT_MACROS_HPP) +#define UTILS_FORMAT_MACROS_HPP + +// We include the .ipp file instead of .hpp because, after all, macros.hpp +// is provided purely for convenience and must not be included from other +// header files. Henceforth, we make things easier to the callers. +#include "utils/format/formatter.ipp" + + +/// Constructs a utils::format::formatter object with the given format string. +/// +/// This macro is just a wrapper to make the construction of +/// utils::format::formatter objects shorter, and thus to allow inlining these +/// calls right in where formatted strings are required. A typical usage would +/// look like: +/// +/// \code +/// std::cout << F("%s %d\n") % my_str % my_int; +/// \endcode +/// +/// \param fmt The format string. +#define F(fmt) utils::format::formatter(fmt) + + +#endif // !defined(UTILS_FORMAT_MACROS_HPP) diff --git a/utils/fs/Kyuafile b/utils/fs/Kyuafile new file mode 100644 index 000000000000..66cb918fca92 --- /dev/null +++ b/utils/fs/Kyuafile @@ -0,0 +1,10 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="auto_cleaners_test"} +atf_test_program{name="directory_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="lua_module_test"} +atf_test_program{name="operations_test"} +atf_test_program{name="path_test"} diff --git a/utils/fs/Makefile.am.inc b/utils/fs/Makefile.am.inc new file mode 100644 index 000000000000..2acdadafa79b --- /dev/null +++ b/utils/fs/Makefile.am.inc @@ -0,0 +1,84 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +UTILS_CFLAGS += $(LUTOK_CFLAGS) +UTILS_LIBS += $(LUTOK_LIBS) + +libutils_a_CPPFLAGS += $(LUTOK_CFLAGS) +libutils_a_SOURCES += utils/fs/auto_cleaners.cpp +libutils_a_SOURCES += utils/fs/auto_cleaners.hpp +libutils_a_SOURCES += utils/fs/auto_cleaners_fwd.hpp +libutils_a_SOURCES += utils/fs/directory.cpp +libutils_a_SOURCES += utils/fs/directory.hpp +libutils_a_SOURCES += utils/fs/directory_fwd.hpp +libutils_a_SOURCES += utils/fs/exceptions.cpp +libutils_a_SOURCES += utils/fs/exceptions.hpp +libutils_a_SOURCES += utils/fs/lua_module.cpp +libutils_a_SOURCES += utils/fs/lua_module.hpp +libutils_a_SOURCES += utils/fs/operations.cpp +libutils_a_SOURCES += utils/fs/operations.hpp +libutils_a_SOURCES += utils/fs/path.cpp +libutils_a_SOURCES += utils/fs/path.hpp +libutils_a_SOURCES += utils/fs/path_fwd.hpp + +if WITH_ATF +tests_utils_fsdir = $(pkgtestsdir)/utils/fs + +tests_utils_fs_DATA = utils/fs/Kyuafile +EXTRA_DIST += $(tests_utils_fs_DATA) + +tests_utils_fs_PROGRAMS = utils/fs/auto_cleaners_test +utils_fs_auto_cleaners_test_SOURCES = utils/fs/auto_cleaners_test.cpp +utils_fs_auto_cleaners_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_auto_cleaners_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/directory_test +utils_fs_directory_test_SOURCES = utils/fs/directory_test.cpp +utils_fs_directory_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_directory_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/exceptions_test +utils_fs_exceptions_test_SOURCES = utils/fs/exceptions_test.cpp +utils_fs_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/lua_module_test +utils_fs_lua_module_test_SOURCES = utils/fs/lua_module_test.cpp +utils_fs_lua_module_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_lua_module_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/operations_test +utils_fs_operations_test_SOURCES = utils/fs/operations_test.cpp +utils_fs_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_fs_PROGRAMS += utils/fs/path_test +utils_fs_path_test_SOURCES = utils/fs/path_test.cpp +utils_fs_path_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_fs_path_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/fs/auto_cleaners.cpp b/utils/fs/auto_cleaners.cpp new file mode 100644 index 000000000000..94ef94465e57 --- /dev/null +++ b/utils/fs/auto_cleaners.cpp @@ -0,0 +1,261 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/auto_cleaners.hpp" + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/interrupts.hpp" + +namespace fs = utils::fs; +namespace signals = utils::signals; + + +/// Shared implementation of the auto_directory. +struct utils::fs::auto_directory::impl : utils::noncopyable { + /// The path to the directory being managed. + fs::path _directory; + + /// Whether cleanup() has been already executed or not. + bool _cleaned; + + /// Constructor. + /// + /// \param directory_ The directory to grab the ownership of. + impl(const path& directory_) : + _directory(directory_), + _cleaned(false) + { + } + + /// Destructor. + ~impl(void) + { + try { + this->cleanup(); + } catch (const fs::error& e) { + LW(F("Failed to auto-cleanup directory '%s': %s") % _directory % + e.what()); + } + } + + /// Removes the directory. + /// + /// See the cleanup() method of the auto_directory class for details. + void + cleanup(void) + { + if (!_cleaned) { + // Mark this as cleaned first so that, in case of failure, we don't + // reraise the error from the destructor. + _cleaned = true; + + fs::rmdir(_directory); + } + } +}; + + +/// Constructs a new auto_directory and grabs ownership of a directory. +/// +/// \param directory_ The directory to grab the ownership of. +fs::auto_directory::auto_directory(const path& directory_) : + _pimpl(new impl(directory_)) +{ +} + + +/// Deletes the managed directory; must be empty. +/// +/// This should not be relied on because it cannot provide proper error +/// reporting. Instead, the caller should use the cleanup() method. +fs::auto_directory::~auto_directory(void) +{ +} + + +/// Creates a self-destructing temporary directory. +/// +/// See the notes for fs::mkdtemp_public() for details on the permissions +/// given to the temporary directory, which are looser than what the standard +/// mkdtemp would grant. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The self-destructing directory. +/// +/// \throw fs::error If the creation fails. +fs::auto_directory +fs::auto_directory::mkdtemp_public(const std::string& path_template) +{ + signals::interrupts_inhibiter inhibiter; + const fs::path directory_ = fs::mkdtemp_public(path_template); + try { + return auto_directory(directory_); + } catch (...) { + fs::rmdir(directory_); + throw; + } +} + + +/// Gets the directory managed by this auto_directory. +/// +/// \return The path to the managed directory. +const fs::path& +fs::auto_directory::directory(void) const +{ + return _pimpl->_directory; +} + + +/// Deletes the managed directory; must be empty. +/// +/// This operation is idempotent. +/// +/// \throw fs::error If there is a problem removing any directory or file. +void +fs::auto_directory::cleanup(void) +{ + _pimpl->cleanup(); +} + + +/// Shared implementation of the auto_file. +struct utils::fs::auto_file::impl : utils::noncopyable { + /// The path to the file being managed. + fs::path _file; + + /// Whether removed() has been already executed or not. + bool _removed; + + /// Constructor. + /// + /// \param file_ The file to grab the ownership of. + impl(const path& file_) : + _file(file_), + _removed(false) + { + } + + /// Destructor. + ~impl(void) + { + try { + this->remove(); + } catch (const fs::error& e) { + LW(F("Failed to auto-cleanup file '%s': %s") % _file % + e.what()); + } + } + + /// Removes the file. + /// + /// See the remove() method of the auto_file class for details. + void + remove(void) + { + if (!_removed) { + // Mark this as cleaned first so that, in case of failure, we don't + // reraise the error from the destructor. + _removed = true; + + fs::unlink(_file); + } + } +}; + + +/// Constructs a new auto_file and grabs ownership of a file. +/// +/// \param file_ The file to grab the ownership of. +fs::auto_file::auto_file(const path& file_) : + _pimpl(new impl(file_)) +{ +} + + +/// Deletes the managed file. +/// +/// This should not be relied on because it cannot provide proper error +/// reporting. Instead, the caller should use the remove() method. +fs::auto_file::~auto_file(void) +{ +} + + +/// Creates a self-destructing temporary file. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The self-destructing file. +/// +/// \throw fs::error If the creation fails. +fs::auto_file +fs::auto_file::mkstemp(const std::string& path_template) +{ + signals::interrupts_inhibiter inhibiter; + const fs::path file_ = fs::mkstemp(path_template); + try { + return auto_file(file_); + } catch (...) { + fs::unlink(file_); + throw; + } +} + + +/// Gets the file managed by this auto_file. +/// +/// \return The path to the managed file. +const fs::path& +fs::auto_file::file(void) const +{ + return _pimpl->_file; +} + + +/// Deletes the managed file. +/// +/// This operation is idempotent. +/// +/// \throw fs::error If there is a problem removing the file. +void +fs::auto_file::remove(void) +{ + _pimpl->remove(); +} diff --git a/utils/fs/auto_cleaners.hpp b/utils/fs/auto_cleaners.hpp new file mode 100644 index 000000000000..f3e6937e3cea --- /dev/null +++ b/utils/fs/auto_cleaners.hpp @@ -0,0 +1,89 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/fs/auto_cleaners.hpp +/// RAII wrappers to automatically remove file system entries. + +#if !defined(UTILS_FS_AUTO_CLEANERS_HPP) +#define UTILS_FS_AUTO_CLEANERS_HPP + +#include "utils/fs/auto_cleaners_fwd.hpp" + +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace fs { + + +/// Grabs ownership of a directory and removes it upon destruction. +/// +/// This class is reference-counted and therefore only the destruction of the +/// last instance will cause the removal of the directory. +class auto_directory { + struct impl; + /// Reference-counted, shared implementation. + std::shared_ptr< impl > _pimpl; + +public: + explicit auto_directory(const path&); + ~auto_directory(void); + + static auto_directory mkdtemp_public(const std::string&); + + const path& directory(void) const; + void cleanup(void); +}; + + +/// Grabs ownership of a file and removes it upon destruction. +/// +/// This class is reference-counted and therefore only the destruction of the +/// last instance will cause the removal of the file. +class auto_file { + struct impl; + /// Reference-counted, shared implementation. + std::shared_ptr< impl > _pimpl; + +public: + explicit auto_file(const path&); + ~auto_file(void); + + static auto_file mkstemp(const std::string&); + + const path& file(void) const; + void remove(void); +}; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_AUTO_CLEANERS_HPP) diff --git a/utils/fs/auto_cleaners_fwd.hpp b/utils/fs/auto_cleaners_fwd.hpp new file mode 100644 index 000000000000..c0cfa6333a1a --- /dev/null +++ b/utils/fs/auto_cleaners_fwd.hpp @@ -0,0 +1,46 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/fs/auto_cleaners_fwd.hpp +/// Forward declarations for utils/fs/auto_cleaners.hpp + +#if !defined(UTILS_FS_AUTO_CLEANERS_FWD_HPP) +#define UTILS_FS_AUTO_CLEANERS_FWD_HPP + +namespace utils { +namespace fs { + + +class auto_directory; +class auto_file; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_AUTO_CLEANERS_FWD_HPP) diff --git a/utils/fs/auto_cleaners_test.cpp b/utils/fs/auto_cleaners_test.cpp new file mode 100644 index 000000000000..da4bbeb2da68 --- /dev/null +++ b/utils/fs/auto_cleaners_test.cpp @@ -0,0 +1,167 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/auto_cleaners.hpp" + +extern "C" { +#include +} + +#include + +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_directory__automatic); +ATF_TEST_CASE_BODY(auto_directory__automatic) +{ + const fs::path root("root"); + fs::mkdir(root, 0755); + + { + fs::auto_directory dir(root); + ATF_REQUIRE_EQ(root, dir.directory()); + + ATF_REQUIRE(::access("root", X_OK) == 0); + + { + fs::auto_directory dir_copy(dir); + } + // Should still exist after a copy is destructed. + ATF_REQUIRE(::access("root", X_OK) == 0); + } + ATF_REQUIRE(::access("root", X_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_directory__explicit); +ATF_TEST_CASE_BODY(auto_directory__explicit) +{ + const fs::path root("root"); + fs::mkdir(root, 0755); + + fs::auto_directory dir(root); + ATF_REQUIRE_EQ(root, dir.directory()); + + ATF_REQUIRE(::access("root", X_OK) == 0); + dir.cleanup(); + dir.cleanup(); + ATF_REQUIRE(::access("root", X_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_directory__mkdtemp_public); +ATF_TEST_CASE_BODY(auto_directory__mkdtemp_public) +{ + utils::setenv("TMPDIR", (fs::current_path() / "tmp").str()); + fs::mkdir(fs::path("tmp"), 0755); + + const std::string path_template("test.XXXXXX"); + { + fs::auto_directory auto_directory = fs::auto_directory::mkdtemp_public( + path_template); + ATF_REQUIRE(::access((fs::path("tmp") / path_template).c_str(), + X_OK) == -1); + ATF_REQUIRE(::rmdir("tmp") == -1); + + ATF_REQUIRE(::access(auto_directory.directory().c_str(), X_OK) == 0); + } + ATF_REQUIRE(::rmdir("tmp") != -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_file__automatic); +ATF_TEST_CASE_BODY(auto_file__automatic) +{ + const fs::path file("foo"); + atf::utils::create_file(file.str(), ""); + { + fs::auto_file auto_file(file); + ATF_REQUIRE_EQ(file, auto_file.file()); + + ATF_REQUIRE(::access(file.c_str(), R_OK) == 0); + + { + fs::auto_file auto_file_copy(auto_file); + } + // Should still exist after a copy is destructed. + ATF_REQUIRE(::access(file.c_str(), R_OK) == 0); + } + ATF_REQUIRE(::access(file.c_str(), R_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_file__explicit); +ATF_TEST_CASE_BODY(auto_file__explicit) +{ + const fs::path file("bar"); + atf::utils::create_file(file.str(), ""); + + fs::auto_file auto_file(file); + ATF_REQUIRE_EQ(file, auto_file.file()); + + ATF_REQUIRE(::access(file.c_str(), R_OK) == 0); + auto_file.remove(); + auto_file.remove(); + ATF_REQUIRE(::access(file.c_str(), R_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(auto_file__mkstemp); +ATF_TEST_CASE_BODY(auto_file__mkstemp) +{ + utils::setenv("TMPDIR", (fs::current_path() / "tmp").str()); + fs::mkdir(fs::path("tmp"), 0755); + + const std::string path_template("test.XXXXXX"); + { + fs::auto_file auto_file = fs::auto_file::mkstemp(path_template); + ATF_REQUIRE(::access((fs::path("tmp") / path_template).c_str(), + X_OK) == -1); + ATF_REQUIRE(::rmdir("tmp") == -1); + + ATF_REQUIRE(::access(auto_file.file().c_str(), R_OK) == 0); + } + ATF_REQUIRE(::rmdir("tmp") != -1); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, auto_directory__automatic); + ATF_ADD_TEST_CASE(tcs, auto_directory__explicit); + ATF_ADD_TEST_CASE(tcs, auto_directory__mkdtemp_public); + + ATF_ADD_TEST_CASE(tcs, auto_file__automatic); + ATF_ADD_TEST_CASE(tcs, auto_file__explicit); + ATF_ADD_TEST_CASE(tcs, auto_file__mkstemp); +} diff --git a/utils/fs/directory.cpp b/utils/fs/directory.cpp new file mode 100644 index 000000000000..ff7ad5e34357 --- /dev/null +++ b/utils/fs/directory.cpp @@ -0,0 +1,360 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/directory.hpp" + +extern "C" { +#include + +#include +} + +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace detail = utils::fs::detail; +namespace fs = utils::fs; +namespace text = utils::text; + + +/// Constructs a new directory entry. +/// +/// \param name_ Name of the directory entry. +fs::directory_entry::directory_entry(const std::string& name_) : name(name_) +{ +} + + +/// Checks if two directory entries are equal. +/// +/// \param other The entry to compare to. +/// +/// \return True if the two entries are equal; false otherwise. +bool +fs::directory_entry::operator==(const directory_entry& other) const +{ + return name == other.name; +} + + +/// Checks if two directory entries are different. +/// +/// \param other The entry to compare to. +/// +/// \return True if the two entries are different; false otherwise. +bool +fs::directory_entry::operator!=(const directory_entry& other) const +{ + return !(*this == other); +} + + +/// Checks if this entry sorts before another entry. +/// +/// \param other The entry to compare to. +/// +/// \return True if this entry sorts before the other entry; false otherwise. +bool +fs::directory_entry::operator<(const directory_entry& other) const +{ + return name < other.name; +} + + +/// Formats a directory entry. +/// +/// \param output Stream into which to inject the formatted entry. +/// \param entry The entry to format. +/// +/// \return A reference to output. +std::ostream& +fs::operator<<(std::ostream& output, const directory_entry& entry) +{ + output << F("directory_entry{name=%s}") % text::quote(entry.name, '\''); + return output; +} + + +/// Internal implementation details for the directory_iterator. +/// +/// In order to support multiple concurrent iterators over the same directory +/// object, this class is the one that performs all directory-level accesses. +/// In particular, even if it may seem surprising, this is the class that +/// handles the DIR object for the directory. +/// +/// Note that iterators implemented by this class do not rely on the container +/// directory class at all. This should not be relied on for object lifecycle +/// purposes. +struct utils::fs::detail::directory_iterator::impl : utils::noncopyable { + /// Path of the directory accessed by this iterator. + const fs::path _path; + + /// Raw pointer to the system representation of the directory. + /// + /// We also use this to determine if the iterator is valid (at the end) or + /// not. A null pointer means an invalid iterator. + ::DIR* _dirp; + + /// Raw representation of the system directory entry. + /// + /// We need to keep this at the class level so that we can use the + /// readdir_r(3) function. + ::dirent _dirent; + + /// Custom representation of the directory entry. + /// + /// This is separate from _dirent because this is the type we return to the + /// user. We must keep this as a pointer so that we can support the common + /// operators (* and ->) over iterators. + std::auto_ptr< directory_entry > _entry; + + /// Constructs an iterator pointing to the "end" of the directory. + impl(void) : _path("invalid-directory-entry"), _dirp(NULL) + { + } + + /// Constructs a new iterator to start scanning a directory. + /// + /// \param path The directory that will be scanned. + /// + /// \throw system_error If there is a problem opening the directory. + explicit impl(const path& path) : _path(path) + { + DIR* dirp = ::opendir(_path.c_str()); + if (dirp == NULL) { + const int original_errno = errno; + throw fs::system_error(F("opendir(%s) failed") % _path, + original_errno); + } + _dirp = dirp; + + // Initialize our first directory entry. Note that this may actually + // close the directory we just opened if the directory happens to be + // empty -- but directories are never empty because they at least have + // '.' and '..' entries. + next(); + } + + /// Destructor. + /// + /// This closes the directory if still open. + ~impl(void) + { + if (_dirp != NULL) + close(); + } + + /// Closes the directory and invalidates the iterator. + void + close(void) + { + PRE(_dirp != NULL); + if (::closedir(_dirp) == -1) { + UNREACHABLE_MSG("Invalid dirp provided to closedir(3)"); + } + _dirp = NULL; + } + + /// Advances the directory entry to the next one. + /// + /// It is possible to use this function on a new directory_entry object to + /// initialize the first entry. + /// + /// \throw system_error If the call to readdir_r fails. + void + next(void) + { + ::dirent* result; + + if (::readdir_r(_dirp, &_dirent, &result) == -1) { + const int original_errno = errno; + throw fs::system_error(F("readdir_r(%s) failed") % _path, + original_errno); + } + if (result == NULL) { + _entry.reset(NULL); + close(); + } else { + _entry.reset(new directory_entry(_dirent.d_name)); + } + } +}; + + +/// Constructs a new directory iterator. +/// +/// \param pimpl The constructed internal implementation structure to use. +detail::directory_iterator::directory_iterator(std::shared_ptr< impl > pimpl) : + _pimpl(pimpl) +{ +} + + +/// Destructor. +detail::directory_iterator::~directory_iterator(void) +{ +} + + +/// Creates a new directory iterator for a directory. +/// +/// \return The directory iterator. Note that the result may be invalid. +/// +/// \throw system_error If opening the directory or reading its first entry +/// fails. +detail::directory_iterator +detail::directory_iterator::new_begin(const path& path) +{ + return directory_iterator(std::shared_ptr< impl >(new impl(path))); +} + + +/// Creates a new invalid directory iterator. +/// +/// \return The invalid directory iterator. +detail::directory_iterator +detail::directory_iterator::new_end(void) +{ + return directory_iterator(std::shared_ptr< impl >(new impl())); +} + + +/// Checks if two iterators are equal. +/// +/// We consider two iterators to be equal if both of them are invalid or, +/// otherwise, if they have the exact same internal representation (as given by +/// equality of the pimpl pointers). +/// +/// \param other The object to compare to. +/// +/// \return True if the two iterators are equal; false otherwise. +bool +detail::directory_iterator::operator==(const directory_iterator& other) const +{ + return (_pimpl->_dirp == NULL && other._pimpl->_dirp == NULL) || + _pimpl == other._pimpl; +} + + +/// Checks if two iterators are different. +/// +/// \param other The object to compare to. +/// +/// \return True if the two iterators are different; false otherwise. +bool +detail::directory_iterator::operator!=(const directory_iterator& other) const +{ + return !(*this == other); +} + + +/// Moves the iterator one element forward. +/// +/// \return A reference to the iterator. +/// +/// \throw system_error If advancing the iterator fails. +detail::directory_iterator& +detail::directory_iterator::operator++(void) +{ + _pimpl->next(); + return *this; +} + + +/// Dereferences the iterator to its contents. +/// +/// \return A reference to the directory entry pointed to by the iterator. +const fs::directory_entry& +detail::directory_iterator::operator*(void) const +{ + PRE(_pimpl->_entry.get() != NULL); + return *_pimpl->_entry; +} + + +/// Dereferences the iterator to its contents. +/// +/// \return A pointer to the directory entry pointed to by the iterator. +const fs::directory_entry* +detail::directory_iterator::operator->(void) const +{ + PRE(_pimpl->_entry.get() != NULL); + return _pimpl->_entry.get(); +} + + +/// Internal implementation details for the directory. +struct utils::fs::directory::impl : utils::noncopyable { + /// Path to the directory to scan. + fs::path _path; + + /// Constructs a new directory. + /// + /// \param path_ Path to the directory to scan. + impl(const fs::path& path_) : _path(path_) + { + } +}; + + +/// Constructs a new directory. +/// +/// \param path_ Path to the directory to scan. +fs::directory::directory(const path& path_) : _pimpl(new impl(path_)) +{ +} + + +/// Returns an iterator to start scanning the directory. +/// +/// \return An iterator on the directory. +/// +/// \throw system_error If the directory cannot be opened to obtain its first +/// entry. +fs::directory::const_iterator +fs::directory::begin(void) const +{ + return const_iterator::new_begin(_pimpl->_path); +} + + +/// Returns an invalid iterator to check for the end of an scan. +/// +/// \return An invalid iterator. +fs::directory::const_iterator +fs::directory::end(void) const +{ + return const_iterator::new_end(); +} diff --git a/utils/fs/directory.hpp b/utils/fs/directory.hpp new file mode 100644 index 000000000000..53c37ec86450 --- /dev/null +++ b/utils/fs/directory.hpp @@ -0,0 +1,120 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/fs/directory.hpp +/// Provides the utils::fs::directory class. + +#if !defined(UTILS_FS_DIRECTORY_HPP) +#define UTILS_FS_DIRECTORY_HPP + +#include "utils/fs/directory_fwd.hpp" + +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace fs { + + +/// Representation of a single directory entry. +struct directory_entry { + /// Name of the directory entry. + std::string name; + + explicit directory_entry(const std::string&); + + bool operator==(const directory_entry&) const; + bool operator!=(const directory_entry&) const; + bool operator<(const directory_entry&) const; +}; + + +std::ostream& operator<<(std::ostream&, const directory_entry&); + + +namespace detail { + + +/// Forward directory iterator. +class directory_iterator { + struct impl; + + /// Internal implementation details. + std::shared_ptr< impl > _pimpl; + + directory_iterator(std::shared_ptr< impl >); + + friend class fs::directory; + static directory_iterator new_begin(const path&); + static directory_iterator new_end(void); + +public: + ~directory_iterator(); + + bool operator==(const directory_iterator&) const; + bool operator!=(const directory_iterator&) const; + directory_iterator& operator++(void); + + const directory_entry& operator*(void) const; + const directory_entry* operator->(void) const; +}; + + +} // namespace detail + + +/// Representation of a local filesystem directory. +/// +/// This class is pretty much stateless. All the directory manipulation +/// operations happen within the iterator. +class directory { +public: + /// Public type for a constant forward directory iterator. + typedef detail::directory_iterator const_iterator; + +private: + struct impl; + + /// Internal implementation details. + std::shared_ptr< impl > _pimpl; + +public: + explicit directory(const path&); + + const_iterator begin(void) const; + const_iterator end(void) const; +}; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_DIRECTORY_HPP) diff --git a/utils/fs/directory_fwd.hpp b/utils/fs/directory_fwd.hpp new file mode 100644 index 000000000000..50886551ca88 --- /dev/null +++ b/utils/fs/directory_fwd.hpp @@ -0,0 +1,55 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/fs/directory_fwd.hpp +/// Forward declarations for utils/fs/directory.hpp + +#if !defined(UTILS_FS_DIRECTORY_FWD_HPP) +#define UTILS_FS_DIRECTORY_FWD_HPP + +namespace utils { +namespace fs { + + +namespace detail { + + +class directory_iterator; + + +} // namespace detail + + +struct directory_entry; +class directory; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_DIRECTORY_FWD_HPP) diff --git a/utils/fs/directory_test.cpp b/utils/fs/directory_test.cpp new file mode 100644 index 000000000000..4c1aa2d010f4 --- /dev/null +++ b/utils/fs/directory_test.cpp @@ -0,0 +1,190 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/directory.hpp" + +#include + +#include + +#include "utils/format/containers.ipp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__public_fields); +ATF_TEST_CASE_BODY(directory_entry__public_fields) +{ + const fs::directory_entry entry("name"); + ATF_REQUIRE_EQ("name", entry.name); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__equality); +ATF_TEST_CASE_BODY(directory_entry__equality) +{ + const fs::directory_entry entry1("name"); + const fs::directory_entry entry2("other-name"); + + ATF_REQUIRE( entry1 == entry1); + ATF_REQUIRE(!(entry1 != entry1)); + + ATF_REQUIRE(!(entry1 == entry2)); + ATF_REQUIRE( entry1 != entry2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__sorting); +ATF_TEST_CASE_BODY(directory_entry__sorting) +{ + const fs::directory_entry entry1("name"); + const fs::directory_entry entry2("other-name"); + + ATF_REQUIRE(!(entry1 < entry1)); + ATF_REQUIRE(!(entry2 < entry2)); + ATF_REQUIRE( entry1 < entry2); + ATF_REQUIRE(!(entry2 < entry1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(directory_entry__format); +ATF_TEST_CASE_BODY(directory_entry__format) +{ + const fs::directory_entry entry("this is the name"); + std::ostringstream output; + output << entry; + ATF_REQUIRE_EQ("directory_entry{name='this is the name'}", output.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__empty); +ATF_TEST_CASE_BODY(integration__empty) +{ + fs::mkdir(fs::path("empty"), 0755); + + std::set< fs::directory_entry > contents; + const fs::directory dir(fs::path("empty")); + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + contents.insert(*iter); + // While we are here, make sure both * and -> represent the same. + ATF_REQUIRE((*iter).name == iter->name); + } + + std::set< fs::directory_entry > exp_contents; + exp_contents.insert(fs::directory_entry(".")); + exp_contents.insert(fs::directory_entry("..")); + + ATF_REQUIRE_EQ(exp_contents, contents); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__some_contents); +ATF_TEST_CASE_BODY(integration__some_contents) +{ + fs::mkdir(fs::path("full"), 0755); + atf::utils::create_file("full/a file", ""); + atf::utils::create_file("full/something-else", ""); + atf::utils::create_file("full/.hidden", ""); + fs::mkdir(fs::path("full/subdir"), 0755); + atf::utils::create_file("full/subdir/not-listed", ""); + + std::set< fs::directory_entry > contents; + const fs::directory dir(fs::path("full")); + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + contents.insert(*iter); + // While we are here, make sure both * and -> represent the same. + ATF_REQUIRE((*iter).name == iter->name); + } + + std::set< fs::directory_entry > exp_contents; + exp_contents.insert(fs::directory_entry(".")); + exp_contents.insert(fs::directory_entry("..")); + exp_contents.insert(fs::directory_entry(".hidden")); + exp_contents.insert(fs::directory_entry("a file")); + exp_contents.insert(fs::directory_entry("something-else")); + exp_contents.insert(fs::directory_entry("subdir")); + + ATF_REQUIRE_EQ(exp_contents, contents); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__open_failure); +ATF_TEST_CASE_BODY(integration__open_failure) +{ + const fs::directory directory(fs::path("non-existent")); + ATF_REQUIRE_THROW_RE(fs::system_error, "opendir(.*non-existent.*) failed", + directory.begin()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__iterators_equality); +ATF_TEST_CASE_BODY(integration__iterators_equality) +{ + const fs::directory directory(fs::path(".")); + + fs::directory::const_iterator iter_ok1 = directory.begin(); + fs::directory::const_iterator iter_ok2 = directory.begin(); + fs::directory::const_iterator iter_end = directory.end(); + + ATF_REQUIRE( iter_ok1 == iter_ok1); + ATF_REQUIRE(!(iter_ok1 != iter_ok1)); + + ATF_REQUIRE( iter_ok2 == iter_ok2); + ATF_REQUIRE(!(iter_ok2 != iter_ok2)); + + ATF_REQUIRE(!(iter_ok1 == iter_ok2)); + ATF_REQUIRE( iter_ok1 != iter_ok2); + + ATF_REQUIRE(!(iter_ok1 == iter_end)); + ATF_REQUIRE( iter_ok1 != iter_end); + + ATF_REQUIRE(!(iter_ok2 == iter_end)); + ATF_REQUIRE( iter_ok2 != iter_end); + + ATF_REQUIRE( iter_end == iter_end); + ATF_REQUIRE(!(iter_end != iter_end)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, directory_entry__public_fields); + ATF_ADD_TEST_CASE(tcs, directory_entry__equality); + ATF_ADD_TEST_CASE(tcs, directory_entry__sorting); + ATF_ADD_TEST_CASE(tcs, directory_entry__format); + + ATF_ADD_TEST_CASE(tcs, integration__empty); + ATF_ADD_TEST_CASE(tcs, integration__some_contents); + ATF_ADD_TEST_CASE(tcs, integration__open_failure); + ATF_ADD_TEST_CASE(tcs, integration__iterators_equality); +} diff --git a/utils/fs/exceptions.cpp b/utils/fs/exceptions.cpp new file mode 100644 index 000000000000..102e9069ee3c --- /dev/null +++ b/utils/fs/exceptions.cpp @@ -0,0 +1,162 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/exceptions.hpp" + +#include + +#include "utils/format/macros.hpp" + +namespace fs = utils::fs; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +fs::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +fs::error::~error(void) throw() +{ +} + + +/// Constructs a new invalid_path_error. +/// +/// \param textual_path Textual representation of the invalid path. +/// \param reason Description of the error in the path. +fs::invalid_path_error::invalid_path_error(const std::string& textual_path, + const std::string& reason) : + error(F("Invalid path '%s': %s") % textual_path % reason), + _textual_path(textual_path) +{ +} + + +/// Destructor for the error. +fs::invalid_path_error::~invalid_path_error(void) throw() +{ +} + + +/// Returns the invalid path related to the exception. +/// +/// \return The textual representation of the invalid path. +const std::string& +fs::invalid_path_error::invalid_path(void) const +{ + return _textual_path; +} + + +/// Constructs a new join_error. +/// +/// \param textual_path1_ Textual representation of the first path. +/// \param textual_path2_ Textual representation of the second path. +/// \param reason Description of the error in the join operation. +fs::join_error::join_error(const std::string& textual_path1_, + const std::string& textual_path2_, + const std::string& reason) : + error(F("Cannot join paths '%s' and '%s': %s") % textual_path1_ % + textual_path2_ % reason), + _textual_path1(textual_path1_), + _textual_path2(textual_path2_) +{ +} + + +/// Destructor for the error. +fs::join_error::~join_error(void) throw() +{ +} + + +/// Gets the first path that caused the error in a join operation. +/// +/// \return The textual representation of the path. +const std::string& +fs::join_error::textual_path1(void) const +{ + return _textual_path1; +} + + +/// Gets the second path that caused the error in a join operation. +/// +/// \return The textual representation of the path. +const std::string& +fs::join_error::textual_path2(void) const +{ + return _textual_path2; +} + + +/// Constructs a new error based on an errno code. +/// +/// \param message_ The message describing what caused the error. +/// \param errno_ The error code. +fs::system_error::system_error(const std::string& message_, const int errno_) : + error(F("%s: %s") % message_ % std::strerror(errno_)), + _original_errno(errno_) +{ +} + + +/// Destructor for the error. +fs::system_error::~system_error(void) throw() +{ +} + + + +/// \return The original errno code. +int +fs::system_error::original_errno(void) const throw() +{ + return _original_errno; +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +fs::unsupported_operation_error::unsupported_operation_error( + const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +fs::unsupported_operation_error::~unsupported_operation_error(void) throw() +{ +} diff --git a/utils/fs/exceptions.hpp b/utils/fs/exceptions.hpp new file mode 100644 index 000000000000..32b4af2ce463 --- /dev/null +++ b/utils/fs/exceptions.hpp @@ -0,0 +1,110 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/fs/exceptions.hpp +/// Exception types raised by the fs module. + +#if !defined(UTILS_FS_EXCEPTIONS_HPP) +#define UTILS_FS_EXCEPTIONS_HPP + +#include +#include + +namespace utils { +namespace fs { + + +/// Base exception for fs errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + virtual ~error(void) throw(); +}; + + +/// Error denoting an invalid path while constructing a fs::path object. +class invalid_path_error : public error { + /// Raw value of the invalid path. + std::string _textual_path; + +public: + explicit invalid_path_error(const std::string&, const std::string&); + virtual ~invalid_path_error(void) throw(); + + const std::string& invalid_path(void) const; +}; + + +/// Paths cannot be joined. +class join_error : public error { + /// Raw value of the first path in the join operation. + std::string _textual_path1; + + /// Raw value of the second path in the join operation. + std::string _textual_path2; + +public: + explicit join_error(const std::string&, const std::string&, + const std::string&); + virtual ~join_error(void) throw(); + + const std::string& textual_path1(void) const; + const std::string& textual_path2(void) const; +}; + + +/// Exceptions for errno-based errors. +/// +/// TODO(jmmv): This code is duplicated in, at least, utils::process. Figure +/// out a way to reuse this exception while maintaining the correct inheritance +/// (i.e. be able to keep it as a child of fs::error). +class system_error : public error { + /// Error number describing this libc error condition. + int _original_errno; + +public: + explicit system_error(const std::string&, const int); + ~system_error(void) throw(); + + int original_errno(void) const throw(); +}; + + +/// Exception to denote an unsupported operation. +class unsupported_operation_error : public error { +public: + explicit unsupported_operation_error(const std::string&); + virtual ~unsupported_operation_error(void) throw(); +}; + + +} // namespace fs +} // namespace utils + + +#endif // !defined(UTILS_FS_EXCEPTIONS_HPP) diff --git a/utils/fs/exceptions_test.cpp b/utils/fs/exceptions_test.cpp new file mode 100644 index 000000000000..e67a846506cc --- /dev/null +++ b/utils/fs/exceptions_test.cpp @@ -0,0 +1,95 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/exceptions.hpp" + +#include +#include + +#include + +#include "utils/format/macros.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const fs::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_path_error); +ATF_TEST_CASE_BODY(invalid_path_error) +{ + const fs::invalid_path_error e("some/invalid/path", "The reason"); + ATF_REQUIRE(std::strcmp("Invalid path 'some/invalid/path': The reason", + e.what()) == 0); + ATF_REQUIRE_EQ("some/invalid/path", e.invalid_path()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join_error); +ATF_TEST_CASE_BODY(join_error) +{ + const fs::join_error e("dir1/file1", "/dir2/file2", "The reason"); + ATF_REQUIRE(std::strcmp("Cannot join paths 'dir1/file1' and '/dir2/file2': " + "The reason", e.what()) == 0); + ATF_REQUIRE_EQ("dir1/file1", e.textual_path1()); + ATF_REQUIRE_EQ("/dir2/file2", e.textual_path2()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(system_error); +ATF_TEST_CASE_BODY(system_error) +{ + const fs::system_error e("Call failed", ENOENT); + const std::string expected = F("Call failed: %s") % std::strerror(ENOENT); + ATF_REQUIRE_EQ(expected, e.what()); + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unsupported_operation_error); +ATF_TEST_CASE_BODY(unsupported_operation_error) +{ + const fs::unsupported_operation_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, invalid_path_error); + ATF_ADD_TEST_CASE(tcs, join_error); + ATF_ADD_TEST_CASE(tcs, system_error); + ATF_ADD_TEST_CASE(tcs, unsupported_operation_error); +} diff --git a/utils/fs/lua_module.cpp b/utils/fs/lua_module.cpp new file mode 100644 index 000000000000..dec410927e1a --- /dev/null +++ b/utils/fs/lua_module.cpp @@ -0,0 +1,340 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/lua_module.hpp" + +extern "C" { +#include +} + +#include +#include +#include +#include + +#include +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Given a path, qualifies it with the module's start directory if necessary. +/// +/// \param state The Lua state. +/// \param path The path to qualify. +/// +/// \return The original path if it was absolute; otherwise the original path +/// appended to the module's start directory. +/// +/// \throw std::runtime_error If the module's state has been corrupted. +static fs::path +qualify_path(lutok::state& state, const fs::path& path) +{ + lutok::stack_cleaner cleaner(state); + + if (path.is_absolute()) { + return path; + } else { + state.get_global("_fs_start_dir"); + if (!state.is_string(-1)) + throw std::runtime_error("Missing _fs_start_dir global variable; " + "state corrupted?"); + return fs::path(state.to_string(-1)) / path; + } +} + + +/// Safely gets a path from the Lua state. +/// +/// \param state The Lua state. +/// \param index The position in the Lua stack that contains the path to query. +/// +/// \return The queried path. +/// +/// \throw fs::error If the value is not a valid path. +/// \throw std::runtime_error If the value on the Lua stack is not convertible +/// to a path. +static fs::path +to_path(lutok::state& state, const int index) +{ + if (!state.is_string(index)) + throw std::runtime_error("Need a string parameter"); + return fs::path(state.to_string(index)); +} + + +/// Lua binding for fs::path::basename. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) The basename of the input path. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_basename(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = to_path(state, -1); + state.push_string(path.leaf_name().c_str()); + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::dirname. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) The directory part of the input path. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_dirname(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = to_path(state, -1); + state.push_string(path.branch_path().c_str()); + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::exists. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) Whether the input path exists or not. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_exists(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = qualify_path(state, to_path(state, -1)); + state.push_boolean(fs::exists(path)); + cleaner.forget(); + return 1; +} + + +/// Lua binding for the files iterator. +/// +/// This function takes an open directory from the closure of the iterator and +/// returns the next entry. See lua_fs_files() for the iterator generator +/// function. +/// +/// \pre upvalue(1) The userdata containing an open DIR* object. +/// +/// \param state The lua state. +/// +/// \return The number of result values, i.e. 0 if there are no more entries or +/// 1 if an entry has been read. +static int +files_iterator(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + DIR** dirp = state.to_userdata< DIR* >(state.upvalue_index(1)); + const struct dirent* entry = ::readdir(*dirp); + if (entry == NULL) + return 0; + else { + state.push_string(entry->d_name); + cleaner.forget(); + return 1; + } +} + + +/// Lua binding for the destruction of the files iterator. +/// +/// This function takes an open directory and closes it. See lua_fs_files() for +/// the iterator generator function. +/// +/// \pre stack(-1) The userdata containing an open DIR* object. +/// \post The DIR* object is closed. +/// +/// \param state The lua state. +/// +/// \return The number of result values, i.e. 0. +static int +files_gc(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + PRE(state.is_userdata(-1)); + + DIR** dirp = state.to_userdata< DIR* >(-1); + // For some reason, this may be called more than once. I don't know why + // this happens, but we must protect against it. + if (*dirp != NULL) { + ::closedir(*dirp); + *dirp = NULL; + } + + return 0; +} + + +/// Lua binding to create an iterator to scan the contents of a directory. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) The iterator function. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_files(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = qualify_path(state, to_path(state, -1)); + + DIR** dirp = state.new_userdata< DIR* >(); + + state.new_table(); + state.push_string("__gc"); + state.push_cxx_function(files_gc); + state.set_table(-3); + + state.set_metatable(-2); + + *dirp = ::opendir(path.c_str()); + if (*dirp == NULL) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to open directory: %s") % + std::strerror(original_errno)); + } + + state.push_cxx_closure(files_iterator, 1); + + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::is_absolute. +/// +/// \pre stack(-1) The input path. +/// \post stack(-1) Whether the input path is absolute or not. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_is_absolute(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path = to_path(state, -1); + + state.push_boolean(path.is_absolute()); + cleaner.forget(); + return 1; +} + + +/// Lua binding for fs::path::operator/. +/// +/// \pre stack(-2) The first input path. +/// \pre stack(-1) The second input path. +/// \post stack(-1) The concatenation of the two paths. +/// +/// \param state The Lua state. +/// +/// \return The number of result values, i.e. 1. +static int +lua_fs_join(lutok::state& state) +{ + lutok::stack_cleaner cleaner(state); + + const fs::path path1 = to_path(state, -2); + const fs::path path2 = to_path(state, -1); + state.push_string((path1 / path2).c_str()); + cleaner.forget(); + return 1; +} + + +} // anonymous namespace + + +/// Creates a Lua 'fs' module with a default start directory of ".". +/// +/// \post The global 'fs' symbol is set to a table that contains functions to a +/// variety of utilites from the fs C++ module. +/// +/// \param s The Lua state. +void +fs::open_fs(lutok::state& s) +{ + open_fs(s, fs::current_path()); +} + + +/// Creates a Lua 'fs' module with an explicit start directory. +/// +/// \post The global 'fs' symbol is set to a table that contains functions to a +/// variety of utilites from the fs C++ module. +/// +/// \param s The Lua state. +/// \param start_dir The start directory to use in all operations that reference +/// the underlying file sytem. +void +fs::open_fs(lutok::state& s, const fs::path& start_dir) +{ + lutok::stack_cleaner cleaner(s); + + s.push_string(start_dir.str()); + s.set_global("_fs_start_dir"); + + std::map< std::string, lutok::cxx_function > members; + members["basename"] = lua_fs_basename; + members["dirname"] = lua_fs_dirname; + members["exists"] = lua_fs_exists; + members["files"] = lua_fs_files; + members["is_absolute"] = lua_fs_is_absolute; + members["join"] = lua_fs_join; + lutok::create_module(s, "fs", members); +} diff --git a/utils/fs/lua_module.hpp b/utils/fs/lua_module.hpp new file mode 100644 index 000000000000..c9c303b15eb7 --- /dev/null +++ b/utils/fs/lua_module.hpp @@ -0,0 +1,54 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/fs/lua_module.hpp +/// Lua bindings for the utils::fs module. +/// +/// When the fs module is bound to Lua, the module has the concept of a "start +/// directory". The start directory is the directory used to qualify all +/// relative paths, and is provided at module binding time. + +#if !defined(UTILS_FS_LUA_MODULE_HPP) +#define UTILS_FS_LUA_MODULE_HPP + +#include + +#include "utils/fs/path.hpp" + +namespace utils { +namespace fs { + + +void open_fs(lutok::state&); +void open_fs(lutok::state&, const fs::path&); + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_LUA_MODULE_HPP) diff --git a/utils/fs/lua_module_test.cpp b/utils/fs/lua_module_test.cpp new file mode 100644 index 000000000000..263632ded13f --- /dev/null +++ b/utils/fs/lua_module_test.cpp @@ -0,0 +1,376 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/lua_module.hpp" + +#include +#include +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(open_fs); +ATF_TEST_CASE_BODY(open_fs) +{ + lutok::state state; + stack_balance_checker checker(state); + fs::open_fs(state); + lutok::do_string(state, "return fs.basename", 0, 1, 0); + ATF_REQUIRE(state.is_function(-1)); + lutok::do_string(state, "return fs.dirname", 0, 1, 0); + ATF_REQUIRE(state.is_function(-1)); + lutok::do_string(state, "return fs.join", 0, 1, 0); + ATF_REQUIRE(state.is_function(-1)); + state.pop(3); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(basename__ok); +ATF_TEST_CASE_BODY(basename__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.basename('/my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE_EQ("file_foobar", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(basename__fail); +ATF_TEST_CASE_BODY(basename__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.basename({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.basename('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dirname__ok); +ATF_TEST_CASE_BODY(dirname__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.dirname('/my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE_EQ("/my/test", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dirname__fail); +ATF_TEST_CASE_BODY(dirname__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.dirname({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.dirname('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists__ok); +ATF_TEST_CASE_BODY(exists__ok) +{ + lutok::state state; + fs::open_fs(state); + + atf::utils::create_file("foo", ""); + + lutok::do_string(state, "return fs.exists('foo')", 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('bar')", 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, + F("return fs.exists('%s')") % fs::current_path(), 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists__fail); +ATF_TEST_CASE_BODY(exists__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.exists({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.exists('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists__custom_start_dir); +ATF_TEST_CASE_BODY(exists__custom_start_dir) +{ + lutok::state state; + fs::open_fs(state, fs::path("subdir")); + + fs::mkdir(fs::path("subdir"), 0755); + atf::utils::create_file("subdir/foo", ""); + atf::utils::create_file("bar", ""); + + lutok::do_string(state, "return fs.exists('foo')", 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('subdir/foo')", 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('bar')", 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, "return fs.exists('../bar')", 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); + + lutok::do_string(state, + F("return fs.exists('%s')") % (fs::current_path() / "bar"), + 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__none); +ATF_TEST_CASE_BODY(files__none) +{ + lutok::state state; + state.open_table(); + fs::open_fs(state); + + fs::mkdir(fs::path("root"), 0755); + + lutok::do_string(state, + "names = {}\n" + "for file in fs.files('root') do\n" + " table.insert(names, file)\n" + "end\n" + "table.sort(names)\n" + "return table.concat(names, ' ')", + 0, 1, 0); + ATF_REQUIRE_EQ(". ..", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__some); +ATF_TEST_CASE_BODY(files__some) +{ + lutok::state state; + state.open_table(); + fs::open_fs(state); + + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file("root/file1", ""); + atf::utils::create_file("root/file2", ""); + + lutok::do_string(state, + "names = {}\n" + "for file in fs.files('root') do\n" + " table.insert(names, file)\n" + "end\n" + "table.sort(names)\n" + "return table.concat(names, ' ')", + 0, 1, 0); + ATF_REQUIRE_EQ(". .. file1 file2", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__some_with_custom_start_dir); +ATF_TEST_CASE_BODY(files__some_with_custom_start_dir) +{ + lutok::state state; + state.open_table(); + fs::open_fs(state, fs::current_path() / "root"); + + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file("root/file1", ""); + atf::utils::create_file("root/file2", ""); + atf::utils::create_file("file3", ""); + + lutok::do_string(state, + "names = {}\n" + "for file in fs.files('.') do\n" + " table.insert(names, file)\n" + "end\n" + "table.sort(names)\n" + "return table.concat(names, ' ')", + 0, 1, 0); + ATF_REQUIRE_EQ(". .. file1 file2", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__fail_arg); +ATF_TEST_CASE_BODY(files__fail_arg) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string parameter", + lutok::do_string(state, "fs.files({})", 0, 0, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "fs.files('')", 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(files__fail_opendir); +ATF_TEST_CASE_BODY(files__fail_opendir) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Failed to open directory", + lutok::do_string(state, "fs.files('root')", 0, 0, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_absolute__ok); +ATF_TEST_CASE_BODY(is_absolute__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.is_absolute('my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE(!state.to_boolean(-1)); + lutok::do_string(state, "return fs.is_absolute('/my/test//file_foobar')", + 0, 1, 0); + ATF_REQUIRE(state.to_boolean(-1)); + state.pop(2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_absolute__fail); +ATF_TEST_CASE_BODY(is_absolute__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.is_absolute({})", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.is_absolute('')", + 0, 1, 0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__ok); +ATF_TEST_CASE_BODY(join__ok) +{ + lutok::state state; + fs::open_fs(state); + + lutok::do_string(state, "return fs.join('/a/b///', 'c/d')", 0, 1, 0); + ATF_REQUIRE_EQ("/a/b/c/d", state.to_string(-1)); + state.pop(1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__fail); +ATF_TEST_CASE_BODY(join__fail) +{ + lutok::state state; + fs::open_fs(state); + + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.join({}, 'a')", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Need a string", + lutok::do_string(state, "return fs.join('a', {})", + 0, 1, 0)); + + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.join('', 'a')", + 0, 1, 0)); + ATF_REQUIRE_THROW_RE(lutok::error, "Invalid path", + lutok::do_string(state, "return fs.join('a', '')", + 0, 1, 0)); + + ATF_REQUIRE_THROW_RE(lutok::error, "Cannot join.*'a/b'.*'/c'", + lutok::do_string(state, "fs.join('a/b', '/c')", + 0, 0, 0)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, open_fs); + + ATF_ADD_TEST_CASE(tcs, basename__ok); + ATF_ADD_TEST_CASE(tcs, basename__fail); + + ATF_ADD_TEST_CASE(tcs, dirname__ok); + ATF_ADD_TEST_CASE(tcs, dirname__fail); + + ATF_ADD_TEST_CASE(tcs, exists__ok); + ATF_ADD_TEST_CASE(tcs, exists__fail); + ATF_ADD_TEST_CASE(tcs, exists__custom_start_dir); + + ATF_ADD_TEST_CASE(tcs, files__none); + ATF_ADD_TEST_CASE(tcs, files__some); + ATF_ADD_TEST_CASE(tcs, files__some_with_custom_start_dir); + ATF_ADD_TEST_CASE(tcs, files__fail_arg); + ATF_ADD_TEST_CASE(tcs, files__fail_opendir); + + ATF_ADD_TEST_CASE(tcs, is_absolute__ok); + ATF_ADD_TEST_CASE(tcs, is_absolute__fail); + + ATF_ADD_TEST_CASE(tcs, join__ok); + ATF_ADD_TEST_CASE(tcs, join__fail); +} diff --git a/utils/fs/operations.cpp b/utils/fs/operations.cpp new file mode 100644 index 000000000000..7a96d0b2058a --- /dev/null +++ b/utils/fs/operations.cpp @@ -0,0 +1,803 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/operations.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include +#if defined(HAVE_SYS_MOUNT_H) +# include +#endif +#include +#if defined(HAVE_SYS_STATVFS_H) && defined(HAVE_STATVFS) +# include +#endif +#if defined(HAVE_SYS_VFS_H) +# include +#endif +#include + +#include +} + +#include +#include +#include +#include +#include +#include +#include + +#include "utils/auto_array.ipp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/directory.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/units.hpp" + +namespace fs = utils::fs; +namespace units = utils::units; + +using utils::optional; + + +namespace { + + +/// Operating systems recognized by the code below. +enum os_type { + os_unsupported = 0, + os_freebsd, + os_linux, + os_netbsd, + os_sunos, +}; + + +/// The current operating system. +static enum os_type current_os = +#if defined(__FreeBSD__) + os_freebsd +#elif defined(__linux__) + os_linux +#elif defined(__NetBSD__) + os_netbsd +#elif defined(__SunOS__) + os_sunos +#else + os_unsupported +#endif + ; + + +/// Specifies if a real unmount(2) is available. +/// +/// We use this as a constant instead of a macro so that we can compile both +/// versions of the unmount code unconditionally. This is a way to prevent +/// compilation bugs going unnoticed for long. +static const bool have_unmount2 = +#if defined(HAVE_UNMOUNT) + true; +#else + false; +#endif + + +#if !defined(UMOUNT) +/// Fake replacement value to the path to umount(8). +# define UMOUNT "do-not-use-this-value" +#else +# if defined(HAVE_UNMOUNT) +# error "umount(8) detected when unmount(2) is also available" +# endif +#endif + + +#if !defined(HAVE_UNMOUNT) +/// Fake unmount(2) function for systems without it. +/// +/// This is only provided to allow our code to compile in all platforms +/// regardless of whether they actually have an unmount(2) or not. +/// +/// \return -1 to indicate error, although this should never happen. +static int +unmount(const char* /* path */, + const int /* flags */) +{ + PRE(false); + return -1; +} +#endif + + +/// Error code returned by subprocess to indicate a controlled failure. +const int exit_known_error = 123; + + +static void run_mount_tmpfs(const fs::path&, const uint64_t) UTILS_NORETURN; + + +/// Executes 'mount -t tmpfs' (or a similar variant). +/// +/// This function must be called from a subprocess as it never returns. +/// +/// \param mount_point Location on which to mount a tmpfs. +/// \param size The size of the tmpfs to mount. If 0, use unlimited. +static void +run_mount_tmpfs(const fs::path& mount_point, const uint64_t size) +{ + const char* mount_args[16]; + std::string size_arg; + + std::size_t last = 0; + switch (current_os) { + case os_freebsd: + mount_args[last++] = "mount"; + mount_args[last++] = "-ttmpfs"; + if (size > 0) { + size_arg = F("-osize=%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + case os_linux: + mount_args[last++] = "mount"; + mount_args[last++] = "-ttmpfs"; + if (size > 0) { + size_arg = F("-osize=%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + case os_netbsd: + mount_args[last++] = "mount"; + mount_args[last++] = "-ttmpfs"; + if (size > 0) { + size_arg = F("-o-s%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + case os_sunos: + mount_args[last++] = "mount"; + mount_args[last++] = "-Ftmpfs"; + if (size > 0) { + size_arg = F("-o-s%s") % size; + mount_args[last++] = size_arg.c_str(); + } + mount_args[last++] = "tmpfs"; + mount_args[last++] = mount_point.c_str(); + break; + + default: + std::cerr << "Don't know how to mount a temporary file system in this " + "host operating system\n"; + std::exit(exit_known_error); + } + mount_args[last] = NULL; + + const char** arg; + std::cout << "Mounting tmpfs onto " << mount_point << " with:"; + for (arg = &mount_args[0]; *arg != NULL; arg++) + std::cout << " " << *arg; + std::cout << "\n"; + + const int ret = ::execvp(mount_args[0], + UTILS_UNCONST(char* const, mount_args)); + INV(ret == -1); + std::cerr << "Failed to exec " << mount_args[0] << "\n"; + std::exit(EXIT_FAILURE); +} + + +/// Unmounts a file system using unmount(2). +/// +/// \pre unmount(2) must be available; i.e. have_unmount2 must be true. +/// +/// \param mount_point The file system to unmount. +/// +/// \throw fs::system_error If the call to unmount(2) fails. +static void +unmount_with_unmount2(const fs::path& mount_point) +{ + PRE(have_unmount2); + + if (::unmount(mount_point.c_str(), 0) == -1) { + const int original_errno = errno; + throw fs::system_error(F("unmount(%s) failed") % mount_point, + original_errno); + } +} + + +/// Unmounts a file system using umount(8). +/// +/// \pre umount(2) must not be available; i.e. have_unmount2 must be false. +/// +/// \param mount_point The file system to unmount. +/// +/// \throw fs::error If the execution of umount(8) fails. +static void +unmount_with_umount8(const fs::path& mount_point) +{ + PRE(!have_unmount2); + + const pid_t pid = ::fork(); + if (pid == -1) { + const int original_errno = errno; + throw fs::system_error("Cannot fork to execute unmount tool", + original_errno); + } else if (pid == 0) { + const int ret = ::execlp(UMOUNT, "umount", mount_point.c_str(), NULL); + INV(ret == -1); + std::cerr << "Failed to exec " UMOUNT "\n"; + std::exit(EXIT_FAILURE); + } + + int status; +retry: + if (::waitpid(pid, &status, 0) == -1) { + const int original_errno = errno; + if (errno == EINTR) + goto retry; + throw fs::system_error("Failed to wait for unmount subprocess", + original_errno); + } + + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) == EXIT_SUCCESS) + return; + else + throw fs::error(F("Failed to unmount %s; returned exit code %s") + % mount_point % WEXITSTATUS(status)); + } else + throw fs::error(F("Failed to unmount %s; unmount tool received signal") + % mount_point); +} + + +/// Stats a file, without following links. +/// +/// \param path The file to stat. +/// +/// \return The stat structure on success. +/// +/// \throw system_error An error on failure. +static struct ::stat +safe_stat(const fs::path& path) +{ + struct ::stat sb; + if (::lstat(path.c_str(), &sb) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Cannot get information about %s") % path, + original_errno); + } + return sb; +} + + +} // anonymous namespace + + +/// Copies a file. +/// +/// \param source The file to copy. +/// \param target The destination of the new copy; must be a file name, not a +/// directory. +/// +/// \throw error If there is a problem copying the file. +void +fs::copy(const fs::path& source, const fs::path& target) +{ + std::ifstream input(source.c_str()); + if (!input) + throw error(F("Cannot open copy source %s") % source); + + std::ofstream output(target.c_str()); + if (!output) + throw error(F("Cannot create copy target %s") % target); + + char buffer[1024]; + while (input.good()) { + input.read(buffer, sizeof(buffer)); + if (input.good() || input.eof()) + output.write(buffer, input.gcount()); + } + if (!input.good() && !input.eof()) + throw error(F("Error while reading input file %s") % source); +} + + +/// Queries the path to the current directory. +/// +/// \return The path to the current directory. +/// +/// \throw fs::error If there is a problem querying the current directory. +fs::path +fs::current_path(void) +{ + char* cwd; +#if defined(HAVE_GETCWD_DYN) + cwd = ::getcwd(NULL, 0); +#else + cwd = ::getcwd(NULL, MAXPATHLEN); +#endif + if (cwd == NULL) { + const int original_errno = errno; + throw fs::system_error(F("Failed to get current working directory"), + original_errno); + } + + try { + const fs::path result(cwd); + std::free(cwd); + return result; + } catch (...) { + std::free(cwd); + throw; + } +} + + +/// Checks if a file exists. +/// +/// Be aware that this is racy in the same way as access(2) is. +/// +/// \param path The file to check the existance of. +/// +/// \return True if the file exists; false otherwise. +bool +fs::exists(const fs::path& path) +{ + return ::access(path.c_str(), F_OK) == 0; +} + + +/// Locates a file in the PATH. +/// +/// \param name The file to locate. +/// +/// \return The path to the located file or none if it was not found. The +/// returned path is always absolute. +optional< fs::path > +fs::find_in_path(const char* name) +{ + const optional< std::string > current_path = utils::getenv("PATH"); + if (!current_path || current_path.get().empty()) + return none; + + std::istringstream path_input(current_path.get() + ":"); + std::string path_component; + while (std::getline(path_input, path_component, ':').good()) { + const fs::path candidate = path_component.empty() ? + fs::path(name) : (fs::path(path_component) / name); + if (exists(candidate)) { + if (candidate.is_absolute()) + return utils::make_optional(candidate); + else + return utils::make_optional(candidate.to_absolute()); + } + } + return none; +} + + +/// Calculates the free space in a given file system. +/// +/// \param path Path to a file in the file system for which to check the free +/// disk space. +/// +/// \return The amount of free space usable by a non-root user. +/// +/// \throw system_error If the call to statfs(2) fails. +utils::units::bytes +fs::free_disk_space(const fs::path& path) +{ +#if defined(HAVE_STATVFS) + struct ::statvfs buf; + if (::statvfs(path.c_str(), &buf) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Failed to stat file system for %s") % path, + original_errno); + } + return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail); +#elif defined(HAVE_STATFS) + struct ::statfs buf; + if (::statfs(path.c_str(), &buf) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Failed to stat file system for %s") % path, + original_errno); + } + return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail); +#else +# error "Don't know how to query free disk space" +#endif +} + + +/// Checks if the given path is a directory or not. +/// +/// \return True if the path is a directory; false otherwise. +bool +fs::is_directory(const fs::path& path) +{ + const struct ::stat sb = safe_stat(path); + return S_ISDIR(sb.st_mode); +} + + +/// Creates a directory. +/// +/// \param dir The path to the directory to create. +/// \param mode The permissions for the new directory. +/// +/// \throw system_error If the call to mkdir(2) fails. +void +fs::mkdir(const fs::path& dir, const int mode) +{ + if (::mkdir(dir.c_str(), static_cast< mode_t >(mode)) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Failed to create directory %s") % dir, + original_errno); + } +} + + +/// Creates a directory and any missing parents. +/// +/// This is separate from the fs::mkdir function to clearly differentiate the +/// libc wrapper from the more complex algorithm implemented here. +/// +/// \param dir The path to the directory to create. +/// \param mode The permissions for the new directories. +/// +/// \throw system_error If any call to mkdir(2) fails. +void +fs::mkdir_p(const fs::path& dir, const int mode) +{ + try { + fs::mkdir(dir, mode); + } catch (const fs::system_error& e) { + if (e.original_errno() == ENOENT) { + fs::mkdir_p(dir.branch_path(), mode); + fs::mkdir(dir, mode); + } else if (e.original_errno() != EEXIST) + throw e; + } +} + + +/// Creates a temporary directory that is world readable/accessible. +/// +/// The temporary directory is created using mkdtemp(3) using the provided +/// template. This should be most likely used in conjunction with +/// fs::auto_directory. +/// +/// The temporary directory is given read and execute permissions to everyone +/// and thus should not be used to protect data that may be subject to snooping. +/// This goes together with the assumption that this function is used to create +/// temporary directories for test cases, and that those test cases may +/// sometimes be executed as an unprivileged user. In those cases, we need to +/// support two different things: +/// +/// - Allow the unprivileged code to write to files in the work directory by +/// name (e.g. to write the results file, whose name is provided by the +/// monitor code running as root). This requires us to grant search +/// permissions. +/// +/// - Allow the test cases themselves to call getcwd(3) at any point. At least +/// on NetBSD 7.x, getcwd(3) requires both read and search permissions on all +/// path components leading to the current directory. This requires us to +/// grant both read and search permissions. +/// +/// TODO(jmmv): A cleaner way to support this would be for the test executor to +/// create two work directory hierarchies directly rooted under TMPDIR: one for +/// root and one for the unprivileged user. However, that requires more +/// bookkeeping for no real gain, because we are not really trying to protect +/// the data within our temporary directories against attacks. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The generated path for the temporary directory. +/// +/// \throw fs::system_error If the call to mkdtemp(3) fails. +fs::path +fs::mkdtemp_public(const std::string& path_template) +{ + PRE(path_template.find("XXXXXX") != std::string::npos); + + const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); + const fs::path full_template = tmpdir / path_template; + + utils::auto_array< char > buf(new char[full_template.str().length() + 1]); + std::strcpy(buf.get(), full_template.c_str()); + if (::mkdtemp(buf.get()) == NULL) { + const int original_errno = errno; + throw fs::system_error(F("Cannot create temporary directory using " + "template %s") % full_template, + original_errno); + } + const fs::path path(buf.get()); + + if (::chmod(path.c_str(), 0755) == -1) { + const int original_errno = errno; + + try { + rmdir(path); + } catch (const fs::system_error& e) { + // This really should not fail. We just created the directory and + // have not written anything to it so there is no reason for this to + // fail. But better handle the failure just in case. + LW(F("Failed to delete just-created temporary directory %s") + % path); + } + + throw fs::system_error(F("Failed to grant search permissions on " + "temporary directory %s") % path, + original_errno); + } + + return path; +} + + +/// Creates a temporary file. +/// +/// The temporary file is created using mkstemp(3) using the provided template. +/// This should be most likely used in conjunction with fs::auto_file. +/// +/// \param path_template The template for the temporary path, which is a +/// basename that is created within the TMPDIR. Must contain the XXXXXX +/// pattern, which is atomically replaced by a random unique string. +/// +/// \return The generated path for the temporary directory. +/// +/// \throw fs::system_error If the call to mkstemp(3) fails. +fs::path +fs::mkstemp(const std::string& path_template) +{ + PRE(path_template.find("XXXXXX") != std::string::npos); + + const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); + const fs::path full_template = tmpdir / path_template; + + utils::auto_array< char > buf(new char[full_template.str().length() + 1]); + std::strcpy(buf.get(), full_template.c_str()); + if (::mkstemp(buf.get()) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Cannot create temporary file using template " + "%s") % full_template, original_errno); + } + return fs::path(buf.get()); +} + + +/// Mounts a temporary file system with unlimited size. +/// +/// \param in_mount_point The path on which the file system will be mounted. +/// +/// \throw fs::system_error If the attempt to mount process fails. +/// \throw fs::unsupported_operation_error If the code does not know how to +/// mount a temporary file system in the current operating system. +void +fs::mount_tmpfs(const fs::path& in_mount_point) +{ + mount_tmpfs(in_mount_point, units::bytes()); +} + + +/// Mounts a temporary file system. +/// +/// \param in_mount_point The path on which the file system will be mounted. +/// \param size The size of the tmpfs to mount. If 0, use unlimited. +/// +/// \throw fs::system_error If the attempt to mount process fails. +/// \throw fs::unsupported_operation_error If the code does not know how to +/// mount a temporary file system in the current operating system. +void +fs::mount_tmpfs(const fs::path& in_mount_point, const units::bytes& size) +{ + // SunOS's mount(8) requires paths to be absolute. To err on the side of + // caution, let's make the mount point absolute in all cases. + const fs::path mount_point = in_mount_point.is_absolute() ? + in_mount_point : in_mount_point.to_absolute(); + + const pid_t pid = ::fork(); + if (pid == -1) { + const int original_errno = errno; + throw fs::system_error("Cannot fork to execute mount tool", + original_errno); + } + if (pid == 0) + run_mount_tmpfs(mount_point, size); + + int status; +retry: + if (::waitpid(pid, &status, 0) == -1) { + const int original_errno = errno; + if (errno == EINTR) + goto retry; + throw fs::system_error("Failed to wait for mount subprocess", + original_errno); + } + + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) == exit_known_error) + throw fs::unsupported_operation_error( + "Don't know how to mount a tmpfs on this operating system"); + else if (WEXITSTATUS(status) == EXIT_SUCCESS) + return; + else + throw fs::error(F("Failed to mount tmpfs on %s; returned exit " + "code %s") % mount_point % WEXITSTATUS(status)); + } else { + throw fs::error(F("Failed to mount tmpfs on %s; mount tool " + "received signal") % mount_point); + } +} + + +/// Recursively removes a directory. +/// +/// This operation simulates a "rm -r". No effort is made to forcibly delete +/// files and no attention is paid to mount points. +/// +/// \param directory The directory to remove. +/// +/// \throw fs::error If there is a problem removing any directory or file. +void +fs::rm_r(const fs::path& directory) +{ + const fs::directory dir(directory); + + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + if (iter->name == "." || iter->name == "..") + continue; + + const fs::path entry = directory / iter->name; + + if (fs::is_directory(entry)) { + LD(F("Descending into %s") % entry); + fs::rm_r(entry); + } else { + LD(F("Removing file %s") % entry); + fs::unlink(entry); + } + } + + LD(F("Removing empty directory %s") % directory); + fs::rmdir(directory); +} + + +/// Removes an empty directory. +/// +/// \param file The directory to remove. +/// +/// \throw fs::system_error If the call to rmdir(2) fails. +void +fs::rmdir(const path& file) +{ + if (::rmdir(file.c_str()) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Removal of %s failed") % file, + original_errno); + } +} + + +/// Obtains all the entries in a directory. +/// +/// \param path The directory to scan. +/// +/// \return The set of all directory entries in the given directory. +/// +/// \throw fs::system_error If reading the directory fails for any reason. +std::set< fs::directory_entry > +fs::scan_directory(const fs::path& path) +{ + std::set< fs::directory_entry > contents; + + fs::directory dir(path); + for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end(); + ++iter) { + contents.insert(*iter); + } + + return contents; +} + + +/// Removes a file. +/// +/// \param file The file to remove. +/// +/// \throw fs::system_error If the call to unlink(2) fails. +void +fs::unlink(const path& file) +{ + if (::unlink(file.c_str()) == -1) { + const int original_errno = errno; + throw fs::system_error(F("Removal of %s failed") % file, + original_errno); + } +} + + +/// Unmounts a file system. +/// +/// \param in_mount_point The file system to unmount. +/// +/// \throw fs::error If the unmount fails. +void +fs::unmount(const fs::path& in_mount_point) +{ + // FreeBSD's unmount(2) requires paths to be absolute. To err on the side + // of caution, let's make it absolute in all cases. + const fs::path mount_point = in_mount_point.is_absolute() ? + in_mount_point : in_mount_point.to_absolute(); + + static const int unmount_retries = 3; + static const int unmount_retry_delay_seconds = 1; + + int retries = unmount_retries; +retry: + try { + if (have_unmount2) { + unmount_with_unmount2(mount_point); + } else { + unmount_with_umount8(mount_point); + } + } catch (const fs::system_error& error) { + if (error.original_errno() == EBUSY && retries > 0) { + LW(F("%s busy; unmount retries left %s") % mount_point % retries); + retries--; + ::sleep(unmount_retry_delay_seconds); + goto retry; + } + throw; + } +} diff --git a/utils/fs/operations.hpp b/utils/fs/operations.hpp new file mode 100644 index 000000000000..bd7560ffc048 --- /dev/null +++ b/utils/fs/operations.hpp @@ -0,0 +1,72 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/fs/operations.hpp +/// File system algorithms and access functions. +/// +/// The functions in this module are exception-based, type-improved wrappers +/// over the functions provided by libc. + +#if !defined(UTILS_FS_OPERATIONS_HPP) +#define UTILS_FS_OPERATIONS_HPP + +#include +#include + +#include "utils/fs/directory_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" +#include "utils/units_fwd.hpp" + +namespace utils { +namespace fs { + + +void copy(const fs::path&, const fs::path&); +path current_path(void); +bool exists(const fs::path&); +utils::optional< path > find_in_path(const char*); +utils::units::bytes free_disk_space(const fs::path&); +bool is_directory(const fs::path&); +void mkdir(const path&, const int); +void mkdir_p(const path&, const int); +fs::path mkdtemp_public(const std::string&); +fs::path mkstemp(const std::string&); +void mount_tmpfs(const path&); +void mount_tmpfs(const path&, const units::bytes&); +void rm_r(const path&); +void rmdir(const path&); +std::set< directory_entry > scan_directory(const path&); +void unlink(const path&); +void unmount(const path&); + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_OPERATIONS_HPP) diff --git a/utils/fs/operations_test.cpp b/utils/fs/operations_test.cpp new file mode 100644 index 000000000000..f1349351166e --- /dev/null +++ b/utils/fs/operations_test.cpp @@ -0,0 +1,826 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/operations.hpp" + +extern "C" { +#include +#include +#include + +#include +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/directory.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/stream.hpp" +#include "utils/units.hpp" + +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace units = utils::units; + +using utils::optional; + + +namespace { + + +/// Checks if a directory entry exists and matches a specific type. +/// +/// \param dir The directory in which to look for the entry. +/// \param name The name of the entry to look up. +/// \param expected_type The expected type of the file as given by dir(5). +/// +/// \return True if the entry exists and matches the given type; false +/// otherwise. +static bool +lookup(const char* dir, const char* name, const unsigned int expected_type) +{ + DIR* dirp = ::opendir(dir); + ATF_REQUIRE(dirp != NULL); + + bool found = false; + struct dirent* dp; + while (!found && (dp = readdir(dirp)) != NULL) { + if (std::strcmp(dp->d_name, name) == 0) { + struct ::stat s; + const fs::path lookup_path = fs::path(dir) / name; + ATF_REQUIRE(::stat(lookup_path.c_str(), &s) != -1); + if ((s.st_mode & S_IFMT) == expected_type) { + found = true; + } + } + } + ::closedir(dirp); + return found; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(copy__ok); +ATF_TEST_CASE_BODY(copy__ok) +{ + const fs::path source("f1.txt"); + const fs::path target("f2.txt"); + + atf::utils::create_file(source.str(), "This is the input"); + fs::copy(source, target); + ATF_REQUIRE(atf::utils::compare_file(target.str(), "This is the input")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(copy__fail_open); +ATF_TEST_CASE_BODY(copy__fail_open) +{ + const fs::path source("f1.txt"); + const fs::path target("f2.txt"); + + ATF_REQUIRE_THROW_RE(fs::error, "Cannot open copy source f1.txt", + fs::copy(source, target)); +} + + +ATF_TEST_CASE(copy__fail_create); +ATF_TEST_CASE_HEAD(copy__fail_create) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(copy__fail_create) +{ + const fs::path source("f1.txt"); + const fs::path target("f2.txt"); + + atf::utils::create_file(target.str(), "Do not override"); + ATF_REQUIRE(::chmod(target.c_str(), 0444) != -1); + + atf::utils::create_file(source.str(), "This is the input"); + ATF_REQUIRE_THROW_RE(fs::error, "Cannot create copy target f2.txt", + fs::copy(source, target)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_path__ok); +ATF_TEST_CASE_BODY(current_path__ok) +{ + const fs::path previous = fs::current_path(); + fs::mkdir(fs::path("root"), 0755); + ATF_REQUIRE(::chdir("root") != -1); + const fs::path cwd = fs::current_path(); + ATF_REQUIRE_EQ(cwd.str().length() - 5, cwd.str().find("/root")); + ATF_REQUIRE_EQ(previous / "root", cwd); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_path__enoent); +ATF_TEST_CASE_BODY(current_path__enoent) +{ + const fs::path previous = fs::current_path(); + fs::mkdir(fs::path("root"), 0755); + ATF_REQUIRE(::chdir("root") != -1); + ATF_REQUIRE(::rmdir("../root") != -1); + try { + (void)fs::current_path(); + fail("system_errpr not raised"); + } catch (const fs::system_error& e) { + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exists); +ATF_TEST_CASE_BODY(exists) +{ + const fs::path dir("dir"); + ATF_REQUIRE(!fs::exists(dir)); + fs::mkdir(dir, 0755); + ATF_REQUIRE(fs::exists(dir)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__no_path); +ATF_TEST_CASE_BODY(find_in_path__no_path) +{ + utils::unsetenv("PATH"); + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file("ls", ""); + ATF_REQUIRE(!fs::find_in_path("ls")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__empty_path); +ATF_TEST_CASE_BODY(find_in_path__empty_path) +{ + utils::setenv("PATH", ""); + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file("ls", ""); + ATF_REQUIRE(!fs::find_in_path("ls")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__one_component); +ATF_TEST_CASE_BODY(find_in_path__one_component) +{ + const fs::path dir = fs::current_path() / "bin"; + fs::mkdir(dir, 0755); + utils::setenv("PATH", dir.str()); + + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file((dir / "ls").str(), ""); + ATF_REQUIRE_EQ(dir / "ls", fs::find_in_path("ls").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__many_components); +ATF_TEST_CASE_BODY(find_in_path__many_components) +{ + const fs::path dir1 = fs::current_path() / "dir1"; + const fs::path dir2 = fs::current_path() / "dir2"; + fs::mkdir(dir1, 0755); + fs::mkdir(dir2, 0755); + utils::setenv("PATH", dir1.str() + ":" + dir2.str()); + + ATF_REQUIRE(!fs::find_in_path("ls")); + atf::utils::create_file((dir2 / "ls").str(), ""); + ATF_REQUIRE_EQ(dir2 / "ls", fs::find_in_path("ls").get()); + atf::utils::create_file((dir1 / "ls").str(), ""); + ATF_REQUIRE_EQ(dir1 / "ls", fs::find_in_path("ls").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__current_directory); +ATF_TEST_CASE_BODY(find_in_path__current_directory) +{ + utils::setenv("PATH", "bin:"); + + ATF_REQUIRE(!fs::find_in_path("foo-bar")); + atf::utils::create_file("foo-bar", ""); + ATF_REQUIRE_EQ(fs::path("foo-bar").to_absolute(), + fs::find_in_path("foo-bar").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_in_path__always_absolute); +ATF_TEST_CASE_BODY(find_in_path__always_absolute) +{ + fs::mkdir(fs::path("my-bin"), 0755); + utils::setenv("PATH", "my-bin"); + + ATF_REQUIRE(!fs::find_in_path("abcd")); + atf::utils::create_file("my-bin/abcd", ""); + ATF_REQUIRE_EQ(fs::path("my-bin/abcd").to_absolute(), + fs::find_in_path("abcd").get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(free_disk_space__ok__smoke); +ATF_TEST_CASE_BODY(free_disk_space__ok__smoke) +{ + const units::bytes space = fs::free_disk_space(fs::path(".")); + ATF_REQUIRE(space > units::MB); // Simple test that should always pass. +} + + +/// Unmounts a directory without raising errors. +/// +/// \param cookie Name of a file that exists while the mount point is still +/// mounted. Used to prevent a double-unmount, which would print a +/// misleading error message. +/// \param mount_point Path to the mount point to unmount. +static void +cleanup_mount_point(const fs::path& cookie, const fs::path& mount_point) +{ + try { + if (fs::exists(cookie)) { + fs::unmount(mount_point); + } + } catch (const std::runtime_error& e) { + std::cerr << "Failed trying to unmount " + mount_point.str() + + " during cleanup: " << e.what() << '\n'; + } +} + + +ATF_TEST_CASE_WITH_CLEANUP(free_disk_space__ok__real); +ATF_TEST_CASE_HEAD(free_disk_space__ok__real) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(free_disk_space__ok__real) +{ + try { + const fs::path mount_point("mount_point"); + fs::mkdir(mount_point, 0755); + fs::mount_tmpfs(mount_point, units::bytes(32 * units::MB)); + atf::utils::create_file("mounted", ""); + const units::bytes space = fs::free_disk_space(fs::path(mount_point)); + fs::unmount(mount_point); + fs::unlink(fs::path("mounted")); + ATF_REQUIRE(space < 35 * units::MB); + ATF_REQUIRE(space > 28 * units::MB); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } +} +ATF_TEST_CASE_CLEANUP(free_disk_space__ok__real) +{ + cleanup_mount_point(fs::path("mounted"), fs::path("mount_point")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(free_disk_space__fail); +ATF_TEST_CASE_BODY(free_disk_space__fail) +{ + ATF_REQUIRE_THROW_RE(fs::error, "Failed to stat file system for missing", + fs::free_disk_space(fs::path("missing"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_directory__ok); +ATF_TEST_CASE_BODY(is_directory__ok) +{ + const fs::path file("file"); + atf::utils::create_file(file.str(), ""); + ATF_REQUIRE(!fs::is_directory(file)); + + const fs::path dir("dir"); + fs::mkdir(dir, 0755); + ATF_REQUIRE(fs::is_directory(dir)); +} + + +ATF_TEST_CASE_WITH_CLEANUP(is_directory__fail); +ATF_TEST_CASE_HEAD(is_directory__fail) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(is_directory__fail) +{ + fs::mkdir(fs::path("dir"), 0000); + ATF_REQUIRE_THROW(fs::error, fs::is_directory(fs::path("dir/foo"))); +} +ATF_TEST_CASE_CLEANUP(is_directory__fail) +{ + if (::chmod("dir", 0755) == -1) { + // If we cannot restore the original permissions, we cannot do much + // more. However, leaving an unwritable directory behind will cause the + // runtime engine to report us as broken. + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir__ok); +ATF_TEST_CASE_BODY(mkdir__ok) +{ + fs::mkdir(fs::path("dir"), 0755); + ATF_REQUIRE(lookup(".", "dir", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir__enoent); +ATF_TEST_CASE_BODY(mkdir__enoent) +{ + try { + fs::mkdir(fs::path("dir1/dir2"), 0755); + fail("system_error not raised"); + } catch (const fs::system_error& e) { + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); + } + ATF_REQUIRE(!lookup(".", "dir1", S_IFDIR)); + ATF_REQUIRE(!lookup(".", "dir2", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir_p__one_component); +ATF_TEST_CASE_BODY(mkdir_p__one_component) +{ + ATF_REQUIRE(!lookup(".", "new-dir", S_IFDIR)); + fs::mkdir_p(fs::path("new-dir"), 0755); + ATF_REQUIRE(lookup(".", "new-dir", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir_p__many_components); +ATF_TEST_CASE_BODY(mkdir_p__many_components) +{ + ATF_REQUIRE(!lookup(".", "a", S_IFDIR)); + fs::mkdir_p(fs::path("a/b/c"), 0755); + ATF_REQUIRE(lookup(".", "a", S_IFDIR)); + ATF_REQUIRE(lookup("a", "b", S_IFDIR)); + ATF_REQUIRE(lookup("a/b", "c", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdir_p__already_exists); +ATF_TEST_CASE_BODY(mkdir_p__already_exists) +{ + fs::mkdir(fs::path("a"), 0755); + fs::mkdir(fs::path("a/b"), 0755); + fs::mkdir_p(fs::path("a/b"), 0755); +} + + +ATF_TEST_CASE(mkdir_p__eacces) +ATF_TEST_CASE_HEAD(mkdir_p__eacces) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(mkdir_p__eacces) +{ + fs::mkdir(fs::path("a"), 0755); + fs::mkdir(fs::path("a/b"), 0755); + ATF_REQUIRE(::chmod("a/b", 0555) != -1); + try { + fs::mkdir_p(fs::path("a/b/c/d"), 0755); + fail("system_error not raised"); + } catch (const fs::system_error& e) { + ATF_REQUIRE_EQ(EACCES, e.original_errno()); + } + ATF_REQUIRE(lookup(".", "a", S_IFDIR)); + ATF_REQUIRE(lookup("a", "b", S_IFDIR)); + ATF_REQUIRE(!lookup(".", "c", S_IFDIR)); + ATF_REQUIRE(!lookup("a", "c", S_IFDIR)); + ATF_REQUIRE(!lookup("a/b", "c", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkdtemp_public) +ATF_TEST_CASE_BODY(mkdtemp_public) +{ + const fs::path tmpdir = fs::current_path() / "tmp"; + utils::setenv("TMPDIR", tmpdir.str()); + fs::mkdir(tmpdir, 0755); + + const std::string dir_template("tempdir.XXXXXX"); + const fs::path tempdir = fs::mkdtemp_public(dir_template); + ATF_REQUIRE(!lookup("tmp", dir_template.c_str(), S_IFDIR)); + ATF_REQUIRE(lookup("tmp", tempdir.leaf_name().c_str(), S_IFDIR)); +} + + +ATF_TEST_CASE(mkdtemp_public__getcwd_as_non_root) +ATF_TEST_CASE_HEAD(mkdtemp_public__getcwd_as_non_root) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mkdtemp_public__getcwd_as_non_root) +{ + const std::string dir_template("dir.XXXXXX"); + const fs::path dir = fs::mkdtemp_public(dir_template); + const fs::path subdir = dir / "subdir"; + fs::mkdir(subdir, 0755); + + const uid_t old_euid = ::geteuid(); + const gid_t old_egid = ::getegid(); + + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + ATF_REQUIRE(::setegid(unprivileged_user.gid) != -1); + ATF_REQUIRE(::seteuid(unprivileged_user.uid) != -1); + + // The next code block runs as non-root. We cannot use any ATF macros nor + // functions in it because a failure would cause the test to attempt to + // write to the ATF result file which may not be writable as non-root. + bool failed = false; + { + try { + if (::chdir(subdir.c_str()) == -1) { + std::cerr << "Cannot enter directory\n"; + failed |= true; + } else { + fs::current_path(); + } + } catch (const fs::error& e) { + failed |= true; + std::cerr << "Failed to query current path in: " << subdir << '\n'; + } + + if (::seteuid(old_euid) == -1) { + std::cerr << "Failed to restore euid; cannot continue\n"; + std::abort(); + } + if (::setegid(old_egid) == -1) { + std::cerr << "Failed to restore egid; cannot continue\n"; + std::abort(); + } + } + + if (failed) + fail("Test failed; see stdout for details"); +} + + +ATF_TEST_CASE(mkdtemp_public__search_permissions_as_non_root) +ATF_TEST_CASE_HEAD(mkdtemp_public__search_permissions_as_non_root) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mkdtemp_public__search_permissions_as_non_root) +{ + const std::string dir_template("dir.XXXXXX"); + const fs::path dir = fs::mkdtemp_public(dir_template); + const fs::path cookie = dir / "not-secret"; + atf::utils::create_file(cookie.str(), "this is readable"); + + // We are running as root so there is no reason to assume that our current + // work directory is accessible by non-root. Weaken the permissions so that + // our code below works. + ATF_REQUIRE(::chmod(".", 0755) != -1); + + const uid_t old_euid = ::geteuid(); + const gid_t old_egid = ::getegid(); + + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + ATF_REQUIRE(::setegid(unprivileged_user.gid) != -1); + ATF_REQUIRE(::seteuid(unprivileged_user.uid) != -1); + + // The next code block runs as non-root. We cannot use any ATF macros nor + // functions in it because a failure would cause the test to attempt to + // write to the ATF result file which may not be writable as non-root. + bool failed = false; + { + try { + const std::string contents = utils::read_file(cookie); + std::cerr << "Read contents: " << contents << '\n'; + failed |= (contents != "this is readable"); + } catch (const std::runtime_error& e) { + failed |= true; + std::cerr << "Failed to read " << cookie << '\n'; + } + + if (::seteuid(old_euid) == -1) { + std::cerr << "Failed to restore euid; cannot continue\n"; + std::abort(); + } + if (::setegid(old_egid) == -1) { + std::cerr << "Failed to restore egid; cannot continue\n"; + std::abort(); + } + } + + if (failed) + fail("Test failed; see stdout for details"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(mkstemp) +ATF_TEST_CASE_BODY(mkstemp) +{ + const fs::path tmpdir = fs::current_path() / "tmp"; + utils::setenv("TMPDIR", tmpdir.str()); + fs::mkdir(tmpdir, 0755); + + const std::string file_template("tempfile.XXXXXX"); + const fs::path tempfile = fs::mkstemp(file_template); + ATF_REQUIRE(!lookup("tmp", file_template.c_str(), S_IFREG)); + ATF_REQUIRE(lookup("tmp", tempfile.leaf_name().c_str(), S_IFREG)); +} + + +static void +test_mount_tmpfs_ok(const units::bytes& size) +{ + const fs::path mount_point("mount_point"); + fs::mkdir(mount_point, 0755); + + try { + atf::utils::create_file("outside", ""); + fs::mount_tmpfs(mount_point, size); + atf::utils::create_file("mounted", ""); + atf::utils::create_file((mount_point / "inside").str(), ""); + + struct ::stat outside, inside; + ATF_REQUIRE(::stat("outside", &outside) != -1); + ATF_REQUIRE(::stat((mount_point / "inside").c_str(), &inside) != -1); + ATF_REQUIRE(outside.st_dev != inside.st_dev); + fs::unmount(mount_point); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } +} + + +ATF_TEST_CASE_WITH_CLEANUP(mount_tmpfs__ok__default_size) +ATF_TEST_CASE_HEAD(mount_tmpfs__ok__default_size) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mount_tmpfs__ok__default_size) +{ + test_mount_tmpfs_ok(units::bytes()); +} +ATF_TEST_CASE_CLEANUP(mount_tmpfs__ok__default_size) +{ + cleanup_mount_point(fs::path("mounted"), fs::path("mount_point")); +} + + +ATF_TEST_CASE_WITH_CLEANUP(mount_tmpfs__ok__explicit_size) +ATF_TEST_CASE_HEAD(mount_tmpfs__ok__explicit_size) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mount_tmpfs__ok__explicit_size) +{ + test_mount_tmpfs_ok(units::bytes(10 * units::MB)); +} +ATF_TEST_CASE_CLEANUP(mount_tmpfs__ok__explicit_size) +{ + cleanup_mount_point(fs::path("mounted"), fs::path("mount_point")); +} + + +ATF_TEST_CASE(mount_tmpfs__fail) +ATF_TEST_CASE_HEAD(mount_tmpfs__fail) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(mount_tmpfs__fail) +{ + try { + fs::mount_tmpfs(fs::path("non-existent")); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } catch (const fs::error& e) { + // Expected. + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rm_r__empty); +ATF_TEST_CASE_BODY(rm_r__empty) +{ + fs::mkdir(fs::path("root"), 0755); + ATF_REQUIRE(lookup(".", "root", S_IFDIR)); + fs::rm_r(fs::path("root")); + ATF_REQUIRE(!lookup(".", "root", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rm_r__files_and_directories); +ATF_TEST_CASE_BODY(rm_r__files_and_directories) +{ + fs::mkdir(fs::path("root"), 0755); + atf::utils::create_file("root/.hidden_file", ""); + fs::mkdir(fs::path("root/.hidden_dir"), 0755); + atf::utils::create_file("root/.hidden_dir/a", ""); + atf::utils::create_file("root/file", ""); + atf::utils::create_file("root/with spaces", ""); + fs::mkdir(fs::path("root/dir1"), 0755); + fs::mkdir(fs::path("root/dir1/dir2"), 0755); + atf::utils::create_file("root/dir1/dir2/file", ""); + fs::mkdir(fs::path("root/dir1/dir3"), 0755); + ATF_REQUIRE(lookup(".", "root", S_IFDIR)); + fs::rm_r(fs::path("root")); + ATF_REQUIRE(!lookup(".", "root", S_IFDIR)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rmdir__ok) +ATF_TEST_CASE_BODY(rmdir__ok) +{ + ATF_REQUIRE(::mkdir("foo", 0755) != -1); + ATF_REQUIRE(::access("foo", X_OK) == 0); + fs::rmdir(fs::path("foo")); + ATF_REQUIRE(::access("foo", X_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(rmdir__fail) +ATF_TEST_CASE_BODY(rmdir__fail) +{ + ATF_REQUIRE_THROW_RE(fs::system_error, "Removal of foo failed", + fs::rmdir(fs::path("foo"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scan_directory__ok) +ATF_TEST_CASE_BODY(scan_directory__ok) +{ + fs::mkdir(fs::path("dir"), 0755); + atf::utils::create_file("dir/foo", ""); + atf::utils::create_file("dir/.hidden", ""); + + const std::set< fs::directory_entry > contents = fs::scan_directory( + fs::path("dir")); + + std::set< fs::directory_entry > exp_contents; + exp_contents.insert(fs::directory_entry(".")); + exp_contents.insert(fs::directory_entry("..")); + exp_contents.insert(fs::directory_entry(".hidden")); + exp_contents.insert(fs::directory_entry("foo")); + + ATF_REQUIRE_EQ(exp_contents, contents); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scan_directory__fail) +ATF_TEST_CASE_BODY(scan_directory__fail) +{ + ATF_REQUIRE_THROW_RE(fs::system_error, "opendir(.*missing.*) failed", + fs::scan_directory(fs::path("missing"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unlink__ok) +ATF_TEST_CASE_BODY(unlink__ok) +{ + atf::utils::create_file("foo", ""); + ATF_REQUIRE(::access("foo", R_OK) == 0); + fs::unlink(fs::path("foo")); + ATF_REQUIRE(::access("foo", R_OK) == -1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unlink__fail) +ATF_TEST_CASE_BODY(unlink__fail) +{ + ATF_REQUIRE_THROW_RE(fs::system_error, "Removal of foo failed", + fs::unlink(fs::path("foo"))); +} + + +ATF_TEST_CASE(unmount__ok) +ATF_TEST_CASE_HEAD(unmount__ok) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(unmount__ok) +{ + const fs::path mount_point("mount_point"); + fs::mkdir(mount_point, 0755); + + atf::utils::create_file((mount_point / "test1").str(), ""); + try { + fs::mount_tmpfs(mount_point); + } catch (const fs::unsupported_operation_error& e) { + ATF_SKIP(e.what()); + } + + atf::utils::create_file((mount_point / "test2").str(), ""); + + ATF_REQUIRE(!fs::exists(mount_point / "test1")); + ATF_REQUIRE( fs::exists(mount_point / "test2")); + fs::unmount(mount_point); + ATF_REQUIRE( fs::exists(mount_point / "test1")); + ATF_REQUIRE(!fs::exists(mount_point / "test2")); +} + + +ATF_TEST_CASE(unmount__fail) +ATF_TEST_CASE_HEAD(unmount__fail) +{ + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(unmount__fail) +{ + ATF_REQUIRE_THROW(fs::error, fs::unmount(fs::path("non-existent"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, copy__ok); + ATF_ADD_TEST_CASE(tcs, copy__fail_open); + ATF_ADD_TEST_CASE(tcs, copy__fail_create); + + ATF_ADD_TEST_CASE(tcs, current_path__ok); + ATF_ADD_TEST_CASE(tcs, current_path__enoent); + + ATF_ADD_TEST_CASE(tcs, exists); + + ATF_ADD_TEST_CASE(tcs, find_in_path__no_path); + ATF_ADD_TEST_CASE(tcs, find_in_path__empty_path); + ATF_ADD_TEST_CASE(tcs, find_in_path__one_component); + ATF_ADD_TEST_CASE(tcs, find_in_path__many_components); + ATF_ADD_TEST_CASE(tcs, find_in_path__current_directory); + ATF_ADD_TEST_CASE(tcs, find_in_path__always_absolute); + + ATF_ADD_TEST_CASE(tcs, free_disk_space__ok__smoke); + ATF_ADD_TEST_CASE(tcs, free_disk_space__ok__real); + ATF_ADD_TEST_CASE(tcs, free_disk_space__fail); + + ATF_ADD_TEST_CASE(tcs, is_directory__ok); + ATF_ADD_TEST_CASE(tcs, is_directory__fail); + + ATF_ADD_TEST_CASE(tcs, mkdir__ok); + ATF_ADD_TEST_CASE(tcs, mkdir__enoent); + + ATF_ADD_TEST_CASE(tcs, mkdir_p__one_component); + ATF_ADD_TEST_CASE(tcs, mkdir_p__many_components); + ATF_ADD_TEST_CASE(tcs, mkdir_p__already_exists); + ATF_ADD_TEST_CASE(tcs, mkdir_p__eacces); + + ATF_ADD_TEST_CASE(tcs, mkdtemp_public); + ATF_ADD_TEST_CASE(tcs, mkdtemp_public__getcwd_as_non_root); + ATF_ADD_TEST_CASE(tcs, mkdtemp_public__search_permissions_as_non_root); + + ATF_ADD_TEST_CASE(tcs, mkstemp); + + ATF_ADD_TEST_CASE(tcs, mount_tmpfs__ok__default_size); + ATF_ADD_TEST_CASE(tcs, mount_tmpfs__ok__explicit_size); + ATF_ADD_TEST_CASE(tcs, mount_tmpfs__fail); + + ATF_ADD_TEST_CASE(tcs, rm_r__empty); + ATF_ADD_TEST_CASE(tcs, rm_r__files_and_directories); + + ATF_ADD_TEST_CASE(tcs, rmdir__ok); + ATF_ADD_TEST_CASE(tcs, rmdir__fail); + + ATF_ADD_TEST_CASE(tcs, scan_directory__ok); + ATF_ADD_TEST_CASE(tcs, scan_directory__fail); + + ATF_ADD_TEST_CASE(tcs, unlink__ok); + ATF_ADD_TEST_CASE(tcs, unlink__fail); + + ATF_ADD_TEST_CASE(tcs, unmount__ok); + ATF_ADD_TEST_CASE(tcs, unmount__fail); +} diff --git a/utils/fs/path.cpp b/utils/fs/path.cpp new file mode 100644 index 000000000000..465ed49c4c2a --- /dev/null +++ b/utils/fs/path.cpp @@ -0,0 +1,303 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/path.hpp" + +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Normalizes an input string to a valid path. +/// +/// A normalized path cannot have empty components; i.e. there can be at most +/// one consecutive separator (/). +/// +/// \param in The string to normalize. +/// +/// \return The normalized string, representing a path. +/// +/// \throw utils::fs::invalid_path_error If the path is empty. +static std::string +normalize(const std::string& in) +{ + if (in.empty()) + throw fs::invalid_path_error(in, "Cannot be empty"); + + std::string out; + + std::string::size_type pos = 0; + do { + const std::string::size_type next_pos = in.find('/', pos); + + const std::string component = in.substr(pos, next_pos - pos); + if (!component.empty()) { + if (pos == 0) + out += component; + else if (component != ".") + out += "/" + component; + } + + if (next_pos == std::string::npos) + pos = next_pos; + else + pos = next_pos + 1; + } while (pos != std::string::npos); + + return out.empty() ? "/" : out; +} + + +} // anonymous namespace + + +/// Creates a new path object from a textual representation of a path. +/// +/// \param text A valid representation of a path in textual form. +/// +/// \throw utils::fs::invalid_path_error If the input text does not represent a +/// valid path. +fs::path::path(const std::string& text) : + _repr(normalize(text)) +{ +} + + +/// Gets a view of the path as an array of characters. +/// +/// \return A \code const char* \endcode representation for the object. +const char* +fs::path::c_str(void) const +{ + return _repr.c_str(); +} + + +/// Gets a view of the path as a std::string. +/// +/// \return A \code std::string& \endcode representation for the object. +const std::string& +fs::path::str(void) const +{ + return _repr; +} + + +/// Gets the branch path (directory name) of the path. +/// +/// The branch path of a path with just one component (no separators) is ".". +/// +/// \return A new path representing the branch path. +fs::path +fs::path::branch_path(void) const +{ + const std::string::size_type end_pos = _repr.rfind('/'); + if (end_pos == std::string::npos) + return fs::path("."); + else if (end_pos == 0) + return fs::path("/"); + else + return fs::path(_repr.substr(0, end_pos)); +} + + +/// Gets the leaf name (base name) of the path. +/// +/// \return A new string representing the leaf name. +std::string +fs::path::leaf_name(void) const +{ + const std::string::size_type beg_pos = _repr.rfind('/'); + + if (beg_pos == std::string::npos) + return _repr; + else + return _repr.substr(beg_pos + 1); +} + + +/// Converts a relative path in the current directory to an absolute path. +/// +/// \pre The path is relative. +/// +/// \return The absolute representation of the relative path. +fs::path +fs::path::to_absolute(void) const +{ + PRE(!is_absolute()); + return fs::current_path() / *this; +} + + +/// \return True if the representation of the path is absolute. +bool +fs::path::is_absolute(void) const +{ + return _repr[0] == '/'; +} + + +/// Checks whether the path is a parent of another path. +/// +/// A path is considered to be a parent of itself. +/// +/// \return True if this path is a parent of p. +bool +fs::path::is_parent_of(path p) const +{ + do { + if ((*this) == p) + return true; + p = p.branch_path(); + } while (p != fs::path(".") && p != fs::path("/")); + return false; +} + + +/// Counts the number of components in the path. +/// +/// \return The number of components. +int +fs::path::ncomponents(void) const +{ + int count = 0; + if (_repr == "/") + return 1; + else { + for (std::string::const_iterator iter = _repr.begin(); + iter != _repr.end(); ++iter) { + if (*iter == '/') + count++; + } + return count + 1; + } +} + + +/// Less-than comparator for paths. +/// +/// This is provided to make identifiers useful as map keys. +/// +/// \param p The path to compare to. +/// +/// \return True if this identifier sorts before the other identifier; false +/// otherwise. +bool +fs::path::operator<(const fs::path& p) const +{ + return _repr < p._repr; +} + + +/// Compares two paths for equality. +/// +/// Given that the paths are internally normalized, input paths such as +/// ///foo/bar and /foo///bar are exactly the same. However, this does NOT +/// check for true equality: i.e. this does not access the file system to check +/// if the paths actually point to the same object my means of links. +/// +/// \param p The path to compare to. +/// +/// \returns A boolean indicating whether the paths are equal. +bool +fs::path::operator==(const fs::path& p) const +{ + return _repr == p._repr; +} + + +/// Compares two paths for inequality. +/// +/// See the description of operator==() for more details on the comparison +/// performed. +/// +/// \param p The path to compare to. +/// +/// \returns A boolean indicating whether the paths are different. +bool +fs::path::operator!=(const fs::path& p) const +{ + return _repr != p._repr; +} + + +/// Concatenates this path with one or more components. +/// +/// \param components The new components to concatenate to the path. These are +/// normalized because, in general, they may come from user input. These +/// components cannot represent an absolute path. +/// +/// \return A new path containing the concatenation of this path and the +/// provided components. +/// +/// \throw utils::fs::invalid_path_error If components does not represent a +/// valid path. +/// \throw utils::fs::join_error If the join operation is invalid because the +/// two paths are incompatible. +fs::path +fs::path::operator/(const std::string& components) const +{ + return (*this) / fs::path(components); +} + + +/// Concatenates this path with another path. +/// +/// \param rest The path to concatenate to this one. Cannot be absolute. +/// +/// \return A new path containing the concatenation of this path and the other +/// path. +/// +/// \throw utils::fs::join_error If the join operation is invalid because the +/// two paths are incompatible. +fs::path +fs::path::operator/(const fs::path& rest) const +{ + if (rest.is_absolute()) + throw fs::join_error(_repr, rest._repr, + "Cannot concatenate a path to an absolute path"); + return fs::path(_repr + '/' + rest._repr); +} + + +/// Formats a path for insertion on a stream. +/// +/// \param os The output stream. +/// \param p The path to inject to the stream. +/// +/// \return The output stream os. +std::ostream& +fs::operator<<(std::ostream& os, const fs::path& p) +{ + return (os << p.str()); +} diff --git a/utils/fs/path.hpp b/utils/fs/path.hpp new file mode 100644 index 000000000000..fe55fd55f234 --- /dev/null +++ b/utils/fs/path.hpp @@ -0,0 +1,87 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/fs/path.hpp +/// Provides the utils::fs::path class. +/// +/// This is a poor man's reimplementation of the path class provided by +/// Boost.Filesystem, in the sense that it tries to follow the same API but is +/// much simplified. + +#if !defined(UTILS_FS_PATH_HPP) +#define UTILS_FS_PATH_HPP + +#include "utils/fs/path_fwd.hpp" + +#include +#include + +namespace utils { +namespace fs { + + +/// Representation and manipulation of a file system path. +/// +/// Application code should always use this class to represent a path instead of +/// std::string, because this class is more semantically representative, ensures +/// that the values are valid and provides some useful manipulation functions. +/// +/// Conversions to and from strings are always explicit. +class path { + /// Internal representation of the path. + std::string _repr; + +public: + explicit path(const std::string&); + + const char* c_str(void) const; + const std::string& str(void) const; + + path branch_path(void) const; + std::string leaf_name(void) const; + path to_absolute(void) const; + + bool is_absolute(void) const; + bool is_parent_of(path) const; + int ncomponents(void) const; + + bool operator<(const path&) const; + bool operator==(const path&) const; + bool operator!=(const path&) const; + path operator/(const std::string&) const; + path operator/(const path&) const; +}; + + +std::ostream& operator<<(std::ostream&, const path&); + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_PATH_HPP) diff --git a/utils/fs/path_fwd.hpp b/utils/fs/path_fwd.hpp new file mode 100644 index 000000000000..4e6856073553 --- /dev/null +++ b/utils/fs/path_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/fs/path_fwd.hpp +/// Forward declarations for utils/fs/path.hpp + +#if !defined(UTILS_FS_PATH_FWD_HPP) +#define UTILS_FS_PATH_FWD_HPP + +namespace utils { +namespace fs { + + +class path; + + +} // namespace fs +} // namespace utils + +#endif // !defined(UTILS_FS_PATH_FWD_HPP) diff --git a/utils/fs/path_test.cpp b/utils/fs/path_test.cpp new file mode 100644 index 000000000000..30ad3110de31 --- /dev/null +++ b/utils/fs/path_test.cpp @@ -0,0 +1,277 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/fs/path.hpp" + +extern "C" { +#include +} + +#include + +#include + +#include "utils/fs/exceptions.hpp" + +using utils::fs::invalid_path_error; +using utils::fs::join_error; +using utils::fs::path; + + +#define REQUIRE_JOIN_ERROR(path1, path2, expr) \ + try { \ + expr; \ + ATF_FAIL("Expecting join_error but no error raised"); \ + } catch (const join_error& e) { \ + ATF_REQUIRE_EQ(path1, e.textual_path1()); \ + ATF_REQUIRE_EQ(path2, e.textual_path2()); \ + } + + +ATF_TEST_CASE_WITHOUT_HEAD(normalize__ok); +ATF_TEST_CASE_BODY(normalize__ok) +{ + ATF_REQUIRE_EQ(".", path(".").str()); + ATF_REQUIRE_EQ("..", path("..").str()); + ATF_REQUIRE_EQ("/", path("/").str()); + ATF_REQUIRE_EQ("/", path("///").str()); + + ATF_REQUIRE_EQ("foo", path("foo").str()); + ATF_REQUIRE_EQ("foo/bar", path("foo/bar").str()); + ATF_REQUIRE_EQ("foo/bar", path("foo/bar/").str()); + + ATF_REQUIRE_EQ("/foo", path("/foo").str()); + ATF_REQUIRE_EQ("/foo/bar", path("/foo/bar").str()); + ATF_REQUIRE_EQ("/foo/bar", path("/foo/bar/").str()); + + ATF_REQUIRE_EQ("/foo", path("///foo").str()); + ATF_REQUIRE_EQ("/foo/bar", path("///foo///bar").str()); + ATF_REQUIRE_EQ("/foo/bar", path("///foo///bar///").str()); + + ATF_REQUIRE_EQ("./foo/bar", path("./foo/bar").str()); + ATF_REQUIRE_EQ("./foo/bar", path("./foo/./bar").str()); + ATF_REQUIRE_EQ("./foo/bar", path("././foo/./bar").str()); + ATF_REQUIRE_EQ("foo/bar", path("foo/././bar").str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(normalize__invalid); +ATF_TEST_CASE_BODY(normalize__invalid) +{ + try { + path(""); + fail("invalid_path_error not raised"); + } catch (const invalid_path_error& e) { + ATF_REQUIRE(e.invalid_path().empty()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_absolute); +ATF_TEST_CASE_BODY(is_absolute) +{ + ATF_REQUIRE( path("/").is_absolute()); + ATF_REQUIRE( path("////").is_absolute()); + ATF_REQUIRE( path("////a").is_absolute()); + ATF_REQUIRE( path("//a//").is_absolute()); + ATF_REQUIRE(!path("a////").is_absolute()); + ATF_REQUIRE(!path("../foo").is_absolute()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(is_parent_of); +ATF_TEST_CASE_BODY(is_parent_of) +{ + ATF_REQUIRE( path("/").is_parent_of(path("/"))); + ATF_REQUIRE( path(".").is_parent_of(path("."))); + ATF_REQUIRE( path("/a").is_parent_of(path("/a"))); + ATF_REQUIRE( path("/a/b/c").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE( path("a").is_parent_of(path("a"))); + ATF_REQUIRE( path("a/b/c").is_parent_of(path("a/b/c"))); + + ATF_REQUIRE( path("/a/b/c").is_parent_of(path("/a/b/c/d"))); + ATF_REQUIRE( path("/a/b/c").is_parent_of(path("/a/b/c/d/e"))); + ATF_REQUIRE(!path("/a/b/c").is_parent_of(path("a/b/c"))); + ATF_REQUIRE(!path("/a/b/c").is_parent_of(path("a/b/c/d/e"))); + + ATF_REQUIRE( path("a/b/c").is_parent_of(path("a/b/c/d"))); + ATF_REQUIRE( path("a/b/c").is_parent_of(path("a/b/c/d/e"))); + ATF_REQUIRE(!path("a/b/c").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE(!path("a/b/c").is_parent_of(path("/a/b/c/d/e"))); + + ATF_REQUIRE(!path("/a/b/c/d/e").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE(!path("/a/b/c/d/e").is_parent_of(path("a/b/c"))); + ATF_REQUIRE(!path("a/b/c/d/e").is_parent_of(path("/a/b/c"))); + ATF_REQUIRE(!path("a/b/c/d/e").is_parent_of(path("a/b/c"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ncomponents); +ATF_TEST_CASE_BODY(ncomponents) +{ + ATF_REQUIRE_EQ(1, path(".").ncomponents()); + ATF_REQUIRE_EQ(1, path("/").ncomponents()); + + ATF_REQUIRE_EQ(1, path("abc").ncomponents()); + ATF_REQUIRE_EQ(1, path("abc/").ncomponents()); + + ATF_REQUIRE_EQ(2, path("/abc").ncomponents()); + ATF_REQUIRE_EQ(3, path("/abc/def").ncomponents()); + + ATF_REQUIRE_EQ(2, path("abc/def").ncomponents()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(branch_path); +ATF_TEST_CASE_BODY(branch_path) +{ + ATF_REQUIRE_EQ(".", path(".").branch_path().str()); + ATF_REQUIRE_EQ(".", path("foo").branch_path().str()); + ATF_REQUIRE_EQ("foo", path("foo/bar").branch_path().str()); + ATF_REQUIRE_EQ("/", path("/foo").branch_path().str()); + ATF_REQUIRE_EQ("/foo", path("/foo/bar").branch_path().str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(leaf_name); +ATF_TEST_CASE_BODY(leaf_name) +{ + ATF_REQUIRE_EQ(".", path(".").leaf_name()); + ATF_REQUIRE_EQ("foo", path("foo").leaf_name()); + ATF_REQUIRE_EQ("bar", path("foo/bar").leaf_name()); + ATF_REQUIRE_EQ("foo", path("/foo").leaf_name()); + ATF_REQUIRE_EQ("bar", path("/foo/bar").leaf_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_absolute); +ATF_TEST_CASE_BODY(to_absolute) +{ + ATF_REQUIRE(::chdir("/bin") != -1); + const std::string absolute = path("ls").to_absolute().str(); + // In some systems (e.g. in Fedora 17), /bin is really a symlink to + // /usr/bin. Doing an explicit match of 'absolute' to /bin/ls fails in such + // case. Instead, attempt doing a search in the generated path just for a + // substring containing '/bin/ls'. Note that this can still fail if /bin is + // linked to something arbitrary like /a/b... but let's just assume this + // does not happen. + ATF_REQUIRE(absolute.find("/bin/ls") != std::string::npos); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(compare_less_than); +ATF_TEST_CASE_BODY(compare_less_than) +{ + ATF_REQUIRE(!(path("/") < path("/"))); + ATF_REQUIRE(!(path("/") < path("///"))); + + ATF_REQUIRE(!(path("/a/b/c") < path("/a/b/c"))); + + ATF_REQUIRE( path("/a") < path("/b")); + ATF_REQUIRE(!(path("/b") < path("/a"))); + + ATF_REQUIRE( path("/a") < path("/aa")); + ATF_REQUIRE(!(path("/aa") < path("/a"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(compare_equal); +ATF_TEST_CASE_BODY(compare_equal) +{ + ATF_REQUIRE(path("/") == path("///")); + ATF_REQUIRE(path("/a") == path("///a")); + ATF_REQUIRE(path("/a") == path("///a///")); + + ATF_REQUIRE(path("a/b/c") == path("a//b//c")); + ATF_REQUIRE(path("a/b/c") == path("a//b//c///")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(compare_different); +ATF_TEST_CASE_BODY(compare_different) +{ + ATF_REQUIRE(path("/") != path("//a/")); + ATF_REQUIRE(path("/a") != path("a///")); + + ATF_REQUIRE(path("a/b/c") != path("a/b")); + ATF_REQUIRE(path("a/b/c") != path("a//b")); + ATF_REQUIRE(path("a/b/c") != path("/a/b/c")); + ATF_REQUIRE(path("a/b/c") != path("/a//b//c")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(concat__to_string); +ATF_TEST_CASE_BODY(concat__to_string) +{ + ATF_REQUIRE_EQ("foo/bar", (path("foo") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar", (path("foo/") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar/baz", (path("foo/") / "bar//baz///").str()); + + ATF_REQUIRE_THROW(invalid_path_error, path("foo") / ""); + REQUIRE_JOIN_ERROR("foo", "/a/b", path("foo") / "/a/b"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(concat__to_path); +ATF_TEST_CASE_BODY(concat__to_path) +{ + ATF_REQUIRE_EQ("foo/bar", (path("foo") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar", (path("foo/") / "bar").str()); + ATF_REQUIRE_EQ("foo/bar/baz", (path("foo/") / "bar//baz///").str()); + + REQUIRE_JOIN_ERROR("foo", "/a/b", path("foo") / path("/a/b")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(use_as_key); +ATF_TEST_CASE_BODY(use_as_key) +{ + std::set< path > paths; + paths.insert(path("/a")); + ATF_REQUIRE(paths.find(path("//a")) != paths.end()); + ATF_REQUIRE(paths.find(path("a")) == paths.end()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, normalize__ok); + ATF_ADD_TEST_CASE(tcs, normalize__invalid); + ATF_ADD_TEST_CASE(tcs, is_absolute); + ATF_ADD_TEST_CASE(tcs, is_parent_of); + ATF_ADD_TEST_CASE(tcs, ncomponents); + ATF_ADD_TEST_CASE(tcs, branch_path); + ATF_ADD_TEST_CASE(tcs, leaf_name); + ATF_ADD_TEST_CASE(tcs, to_absolute); + ATF_ADD_TEST_CASE(tcs, compare_less_than); + ATF_ADD_TEST_CASE(tcs, compare_equal); + ATF_ADD_TEST_CASE(tcs, compare_different); + ATF_ADD_TEST_CASE(tcs, concat__to_string); + ATF_ADD_TEST_CASE(tcs, concat__to_path); + ATF_ADD_TEST_CASE(tcs, use_as_key); +} diff --git a/utils/logging/Kyuafile b/utils/logging/Kyuafile new file mode 100644 index 000000000000..0853a335c6ae --- /dev/null +++ b/utils/logging/Kyuafile @@ -0,0 +1,6 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="macros_test"} +atf_test_program{name="operations_test"} diff --git a/utils/logging/Makefile.am.inc b/utils/logging/Makefile.am.inc new file mode 100644 index 000000000000..7d88f16859d7 --- /dev/null +++ b/utils/logging/Makefile.am.inc @@ -0,0 +1,53 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +UTILS_CFLAGS += $(LUTOK_CFLAGS) +UTILS_LIBS += $(LUTOK_LIBS) + +libutils_a_CPPFLAGS += $(LUTOK_CFLAGS) +libutils_a_SOURCES += utils/logging/macros.hpp +libutils_a_SOURCES += utils/logging/operations.cpp +libutils_a_SOURCES += utils/logging/operations.hpp +libutils_a_SOURCES += utils/logging/operations_fwd.hpp + +if WITH_ATF +tests_utils_loggingdir = $(pkgtestsdir)/utils/logging + +tests_utils_logging_DATA = utils/logging/Kyuafile +EXTRA_DIST += $(tests_utils_logging_DATA) + +tests_utils_logging_PROGRAMS = utils/logging/macros_test +utils_logging_macros_test_SOURCES = utils/logging/macros_test.cpp +utils_logging_macros_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_logging_macros_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_logging_PROGRAMS += utils/logging/operations_test +utils_logging_operations_test_SOURCES = utils/logging/operations_test.cpp +utils_logging_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_logging_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/logging/macros.hpp b/utils/logging/macros.hpp new file mode 100644 index 000000000000..73dd0a60ef87 --- /dev/null +++ b/utils/logging/macros.hpp @@ -0,0 +1,68 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/logging/macros.hpp +/// Convenience macros to simplify usage of the logging library. +/// +/// This file must not be included from other header files. + +#if !defined(UTILS_LOGGING_MACROS_HPP) +#define UTILS_LOGGING_MACROS_HPP + +#include "utils/logging/operations.hpp" + + +/// Logs a debug message. +/// +/// \param message The message to log. +#define LD(message) utils::logging::log(utils::logging::level_debug, \ + __FILE__, __LINE__, message) + + +/// Logs an error message. +/// +/// \param message The message to log. +#define LE(message) utils::logging::log(utils::logging::level_error, \ + __FILE__, __LINE__, message) + + +/// Logs an informational message. +/// +/// \param message The message to log. +#define LI(message) utils::logging::log(utils::logging::level_info, \ + __FILE__, __LINE__, message) + + +/// Logs a warning message. +/// +/// \param message The message to log. +#define LW(message) utils::logging::log(utils::logging::level_warning, \ + __FILE__, __LINE__, message) + + +#endif // !defined(UTILS_LOGGING_MACROS_HPP) diff --git a/utils/logging/macros_test.cpp b/utils/logging/macros_test.cpp new file mode 100644 index 000000000000..fe3ee63cd533 --- /dev/null +++ b/utils/logging/macros_test.cpp @@ -0,0 +1,115 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/logging/macros.hpp" + +#include +#include + +#include + +#include "utils/datetime.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/operations.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; + + +ATF_TEST_CASE_WITHOUT_HEAD(ld); +ATF_TEST_CASE_BODY(ld) +{ + logging::set_persistency("debug", fs::path("test.log")); + datetime::set_mock_now(2011, 2, 21, 18, 30, 0, 0); + LD("Debug message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_MATCH("20110221-183000 D .*: Debug message", line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(le); +ATF_TEST_CASE_BODY(le) +{ + logging::set_persistency("debug", fs::path("test.log")); + datetime::set_mock_now(2011, 2, 21, 18, 30, 0, 0); + LE("Error message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_MATCH("20110221-183000 E .*: Error message", line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(li); +ATF_TEST_CASE_BODY(li) +{ + logging::set_persistency("debug", fs::path("test.log")); + datetime::set_mock_now(2011, 2, 21, 18, 30, 0, 0); + LI("Info message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_MATCH("20110221-183000 I .*: Info message", line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(lw); +ATF_TEST_CASE_BODY(lw) +{ + logging::set_persistency("debug", fs::path("test.log")); + datetime::set_mock_now(2011, 2, 21, 18, 30, 0, 0); + LW("Warning message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_MATCH("20110221-183000 W .*: Warning message", line); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ld); + ATF_ADD_TEST_CASE(tcs, le); + ATF_ADD_TEST_CASE(tcs, li); + ATF_ADD_TEST_CASE(tcs, lw); +} diff --git a/utils/logging/operations.cpp b/utils/logging/operations.cpp new file mode 100644 index 000000000000..88f25361fa18 --- /dev/null +++ b/utils/logging/operations.cpp @@ -0,0 +1,303 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/logging/operations.hpp" + +extern "C" { +#include +} + +#include +#include +#include +#include + +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; + +using utils::none; +using utils::optional; + + +/// The general idea for the application-wide logging goes like this: +/// +/// 1. The application starts. Logging is initialized to capture _all_ log +/// messages into memory regardless of their level by issuing a call to the +/// set_inmemory() function. +/// +/// 2. The application offers the user a way to select the logging level and a +/// file into which to store the log. +/// +/// 3. The application calls set_persistency providing a new log level and a log +/// file. This must be done as early as possible, to minimize the chances of an +/// early crash not capturing any logs. +/// +/// 4. At this point, any log messages stored into memory are flushed to disk +/// respecting the provided log level. +/// +/// 5. The internal state of the logging module is updated to only capture +/// messages that are of the provided log level (or below) and is configured to +/// directly send messages to disk. +/// +/// 6. The user may choose to call set_inmemory() again at a later stage, which +/// will cause the log to be flushed and messages to be recorded in memory +/// again. This is useful in case the logs are being sent to either stdout or +/// stderr and the process forks and wants to keep those child channels +/// unpolluted. +/// +/// The call to set_inmemory() should only be performed by the user-facing +/// application. Tests should skip this call so that the logging messages go to +/// stderr by default, thus generating a useful log to debug the tests. + + +namespace { + + +/// Constant string to strftime to format timestamps. +static const char* timestamp_format = "%Y%m%d-%H%M%S"; + + +/// Mutable global state. +struct global_state { + /// Current log level. + logging::level log_level; + + /// Indicates whether set_persistency() will be called automatically or not. + bool auto_set_persistency; + + /// First time recorded by the logging module. + optional< datetime::timestamp > first_timestamp; + + /// In-memory record of log entries before persistency is enabled. + std::vector< std::pair< logging::level, std::string > > backlog; + + /// Stream to the currently open log file. + std::auto_ptr< std::ostream > logfile; + + global_state() : + log_level(logging::level_debug), + auto_set_persistency(true) + { + } +}; + + +/// Single instance of the mutable global state. +/// +/// Note that this is a raw pointer that we intentionally leak. We must do +/// this, instead of making all of the singleton's members static values, +/// because we want other destructors in the program to be able to log critical +/// conditions. If we use complex types in this translation unit, they may be +/// destroyed before the logging methods in the destructors get a chance to run +/// thus resulting in a premature crash. By using a plain pointer, we ensure +/// this state never gets cleaned up. +static struct global_state* globals_singleton = NULL; + + +/// Gets the singleton instance of global_state. +/// +/// \return A pointer to the unique global_state instance. +static struct global_state* +get_globals(void) +{ + if (globals_singleton == NULL) { + globals_singleton = new global_state(); + } + return globals_singleton; +} + + +/// Converts a level to a printable character. +/// +/// \param level The level to convert. +/// +/// \return The printable character, to be used in log messages. +static char +level_to_char(const logging::level level) +{ + switch (level) { + case logging::level_error: return 'E'; + case logging::level_warning: return 'W'; + case logging::level_info: return 'I'; + case logging::level_debug: return 'D'; + default: UNREACHABLE; + } +} + + +} // anonymous namespace + + +/// Generates a standard log name. +/// +/// This always adds the same timestamp to the log name for a particular run. +/// Also, the timestamp added to the file name corresponds to the first +/// timestamp recorded by the module; it does not necessarily contain the +/// current value of "now". +/// +/// \param logdir The path to the directory in which to place the log. +/// \param progname The name of the program that is generating the log. +/// +/// \return A string representation of the log name based on \p logdir and +/// \p progname. +fs::path +logging::generate_log_name(const fs::path& logdir, const std::string& progname) +{ + struct global_state* globals = get_globals(); + + if (!globals->first_timestamp) + globals->first_timestamp = datetime::timestamp::now(); + // Update kyua(1) if you change the name format. + return logdir / (F("%s.%s.log") % progname % + globals->first_timestamp.get().strftime(timestamp_format)); +} + + +/// Logs an entry to the log file. +/// +/// If the log is not yet set to persistent mode, the entry is recorded in the +/// in-memory backlog. Otherwise, it is just written to disk. +/// +/// \param message_level The level of the entry. +/// \param file The file from which the log message is generated. +/// \param line The line from which the log message is generated. +/// \param user_message The raw message to store. +void +logging::log(const level message_level, const char* file, const int line, + const std::string& user_message) +{ + struct global_state* globals = get_globals(); + + const datetime::timestamp now = datetime::timestamp::now(); + if (!globals->first_timestamp) + globals->first_timestamp = now; + + if (globals->auto_set_persistency) { + // These values are hardcoded here for testing purposes. The + // application should call set_inmemory() by itself during + // initialization to avoid this, so that it has explicit control on how + // the call to set_persistency() happens. + set_persistency("debug", fs::path("/dev/stderr")); + globals->auto_set_persistency = false; + } + + if (message_level > globals->log_level) + return; + + // Update doc/troubleshooting.texi if you change the log format. + const std::string message = F("%s %s %s %s:%s: %s") % + now.strftime(timestamp_format) % level_to_char(message_level) % + ::getpid() % file % line % user_message; + if (globals->logfile.get() == NULL) + globals->backlog.push_back(std::make_pair(message_level, message)); + else { + INV(globals->backlog.empty()); + (*globals->logfile) << message << '\n'; + globals->logfile->flush(); + } +} + + +/// Sets the logging to record messages in memory for later flushing. +/// +/// Can be called after set_persistency to flush logs and set recording to be +/// in-memory again. +void +logging::set_inmemory(void) +{ + struct global_state* globals = get_globals(); + + globals->auto_set_persistency = false; + + if (globals->logfile.get() != NULL) { + INV(globals->backlog.empty()); + globals->logfile->flush(); + globals->logfile.reset(NULL); + } +} + + +/// Makes the log persistent. +/// +/// Calling this function flushes the in-memory log, if any, to disk and sets +/// the logging module to send log entries to disk from this point onwards. +/// There is no way back, and the caller program should execute this function as +/// early as possible to ensure that a crash at startup does not discard too +/// many useful log entries. +/// +/// Any log entries above the provided new_level are discarded. +/// +/// \param new_level The new log level. +/// \param path The file to write the logs to. +/// +/// \throw std::range_error If the given log level is invalid. +/// \throw std::runtime_error If the given file cannot be created. +void +logging::set_persistency(const std::string& new_level, const fs::path& path) +{ + struct global_state* globals = get_globals(); + + globals->auto_set_persistency = false; + + PRE(globals->logfile.get() == NULL); + + // Update doc/troubleshooting.info if you change the log levels. + if (new_level == "debug") + globals->log_level = level_debug; + else if (new_level == "error") + globals->log_level = level_error; + else if (new_level == "info") + globals->log_level = level_info; + else if (new_level == "warning") + globals->log_level = level_warning; + else + throw std::range_error(F("Unrecognized log level '%s'") % new_level); + + try { + globals->logfile = utils::open_ostream(path); + } catch (const std::runtime_error& unused_error) { + throw std::runtime_error(F("Failed to create log file %s") % path); + } + + for (std::vector< std::pair< logging::level, std::string > >::const_iterator + iter = globals->backlog.begin(); iter != globals->backlog.end(); + ++iter) { + if ((*iter).first <= globals->log_level) + (*globals->logfile) << (*iter).second << '\n'; + } + globals->logfile->flush(); + globals->backlog.clear(); +} diff --git a/utils/logging/operations.hpp b/utils/logging/operations.hpp new file mode 100644 index 000000000000..1bb72219dcae --- /dev/null +++ b/utils/logging/operations.hpp @@ -0,0 +1,54 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/logging/operations.hpp +/// Stateless logging facilities. + +#if !defined(UTILS_LOGGING_OPERATIONS_HPP) +#define UTILS_LOGGING_OPERATIONS_HPP + +#include "utils/logging/operations_fwd.hpp" + +#include + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace logging { + + +fs::path generate_log_name(const fs::path&, const std::string&); +void log(const level, const char*, const int, const std::string&); +void set_inmemory(void); +void set_persistency(const std::string&, const fs::path&); + + +} // namespace logging +} // namespace utils + +#endif // !defined(UTILS_LOGGING_OPERATIONS_HPP) diff --git a/utils/logging/operations_fwd.hpp b/utils/logging/operations_fwd.hpp new file mode 100644 index 000000000000..0e3edd7993ec --- /dev/null +++ b/utils/logging/operations_fwd.hpp @@ -0,0 +1,54 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/logging/operations_fwd.hpp +/// Forward declarations for utils/logging/operations.hpp + +#if !defined(UTILS_LOGGING_OPERATIONS_FWD_HPP) +#define UTILS_LOGGING_OPERATIONS_FWD_HPP + +namespace utils { +namespace logging { + + +/// Severity levels for log messages. +/// +/// This enumeration must be sorted from the most severe message to the least +/// severe. +enum level { + level_error = 0, + level_warning, + level_info, + level_debug, +}; + + +} // namespace logging +} // namespace utils + +#endif // !defined(UTILS_LOGGING_OPERATIONS_FWD_HPP) diff --git a/utils/logging/operations_test.cpp b/utils/logging/operations_test.cpp new file mode 100644 index 000000000000..402f36e62904 --- /dev/null +++ b/utils/logging/operations_test.cpp @@ -0,0 +1,354 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/logging/operations.hpp" + +extern "C" { +#include +} + +#include +#include + +#include + +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_log_name__before_log); +ATF_TEST_CASE_BODY(generate_log_name__before_log) +{ + datetime::set_mock_now(2011, 2, 21, 18, 10, 0, 0); + ATF_REQUIRE_EQ(fs::path("/some/dir/foobar.20110221-181000.log"), + logging::generate_log_name(fs::path("/some/dir"), "foobar")); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 1, 987654); + logging::log(logging::level_info, "file", 123, "A message"); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 2, 123); + ATF_REQUIRE_EQ(fs::path("/some/dir/foobar.20110221-181000.log"), + logging::generate_log_name(fs::path("/some/dir"), "foobar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(generate_log_name__after_log); +ATF_TEST_CASE_BODY(generate_log_name__after_log) +{ + datetime::set_mock_now(2011, 2, 21, 18, 15, 0, 0); + logging::log(logging::level_info, "file", 123, "A message"); + datetime::set_mock_now(2011, 2, 21, 18, 15, 1, 987654); + logging::log(logging::level_info, "file", 123, "A message"); + + datetime::set_mock_now(2011, 2, 21, 18, 15, 2, 123); + ATF_REQUIRE_EQ(fs::path("/some/dir/foobar.20110221-181500.log"), + logging::generate_log_name(fs::path("/some/dir"), "foobar")); + + datetime::set_mock_now(2011, 2, 21, 18, 15, 3, 1); + logging::log(logging::level_info, "file", 123, "A message"); + + datetime::set_mock_now(2011, 2, 21, 18, 15, 4, 91); + ATF_REQUIRE_EQ(fs::path("/some/dir/foobar.20110221-181500.log"), + logging::generate_log_name(fs::path("/some/dir"), "foobar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(log); +ATF_TEST_CASE_BODY(log) +{ + logging::set_inmemory(); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 0, 0); + logging::log(logging::level_debug, "f1", 1, "Debug message"); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 1, 987654); + logging::log(logging::level_error, "f2", 2, "Error message"); + + logging::set_persistency("debug", fs::path("test.log")); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 2, 123); + logging::log(logging::level_info, "f3", 3, "Info message"); + + datetime::set_mock_now(2011, 2, 21, 18, 10, 3, 456); + logging::log(logging::level_warning, "f4", 4, "Warning message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-181000 D %s f1:1: Debug message") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-181001 E %s f2:2: Error message") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-181002 I %s f3:3: Info message") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-181003 W %s f4:4: Warning message") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_inmemory__reset); +ATF_TEST_CASE_BODY(set_inmemory__reset) +{ + logging::set_persistency("debug", fs::path("test.log")); + + datetime::set_mock_now(2011, 2, 21, 18, 20, 0, 654321); + logging::log(logging::level_debug, "file", 123, "Debug message"); + logging::set_inmemory(); + logging::log(logging::level_debug, "file", 123, "Debug message 2"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-182000 D %s file:123: Debug message") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__no_backlog); +ATF_TEST_CASE_BODY(set_persistency__no_backlog) +{ + logging::set_persistency("debug", fs::path("test.log")); + + datetime::set_mock_now(2011, 2, 21, 18, 20, 0, 654321); + logging::log(logging::level_debug, "file", 123, "Debug message"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110221-182000 D %s file:123: Debug message") % pid).str(), line); +} + + +/// Creates a log for testing purposes, buffering messages on start. +/// +/// \param level The level of the desired log. +/// \param path The output file. +static void +create_log(const std::string& level, const std::string& path) +{ + logging::set_inmemory(); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 0, 100); + logging::log(logging::level_debug, "file1", 11, "Debug 1"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 1, 200); + logging::log(logging::level_error, "file2", 22, "Error 1"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 2, 300); + logging::log(logging::level_info, "file3", 33, "Info 1"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 3, 400); + logging::log(logging::level_warning, "file4", 44, "Warning 1"); + + logging::set_persistency(level, fs::path(path)); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 4, 500); + logging::log(logging::level_debug, "file1", 11, "Debug 2"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 5, 600); + logging::log(logging::level_error, "file2", 22, "Error 2"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 6, 700); + logging::log(logging::level_info, "file3", 33, "Info 2"); + + datetime::set_mock_now(2011, 3, 19, 11, 40, 7, 800); + logging::log(logging::level_warning, "file4", 44, "Warning 2"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__some_backlog__debug); +ATF_TEST_CASE_BODY(set_persistency__some_backlog__debug) +{ + create_log("debug", "test.log"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114000 D %s file1:11: Debug 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114001 E %s file2:22: Error 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114002 I %s file3:33: Info 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114003 W %s file4:44: Warning 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114004 D %s file1:11: Debug 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114005 E %s file2:22: Error 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114006 I %s file3:33: Info 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114007 W %s file4:44: Warning 2") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__some_backlog__error); +ATF_TEST_CASE_BODY(set_persistency__some_backlog__error) +{ + create_log("error", "test.log"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114001 E %s file2:22: Error 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114005 E %s file2:22: Error 2") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__some_backlog__info); +ATF_TEST_CASE_BODY(set_persistency__some_backlog__info) +{ + create_log("info", "test.log"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114001 E %s file2:22: Error 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114002 I %s file3:33: Info 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114003 W %s file4:44: Warning 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114005 E %s file2:22: Error 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114006 I %s file3:33: Info 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114007 W %s file4:44: Warning 2") % pid).str(), line); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(set_persistency__some_backlog__warning); +ATF_TEST_CASE_BODY(set_persistency__some_backlog__warning) +{ + create_log("warning", "test.log"); + + std::ifstream input("test.log"); + ATF_REQUIRE(input); + + const pid_t pid = ::getpid(); + + std::string line; + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114001 E %s file2:22: Error 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114003 W %s file4:44: Warning 1") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114005 E %s file2:22: Error 2") % pid).str(), line); + ATF_REQUIRE(std::getline(input, line).good()); + ATF_REQUIRE_EQ( + (F("20110319-114007 W %s file4:44: Warning 2") % pid).str(), line); +} + + +ATF_TEST_CASE(set_persistency__fail); +ATF_TEST_CASE_HEAD(set_persistency__fail) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(set_persistency__fail) +{ + ATF_REQUIRE_THROW_RE(std::range_error, "'foobar'", + logging::set_persistency("foobar", fs::path("log"))); + + fs::mkdir(fs::path("dir"), 0644); + ATF_REQUIRE_THROW_RE(std::runtime_error, "dir/fail.log", + logging::set_persistency("debug", + fs::path("dir/fail.log"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, generate_log_name__before_log); + ATF_ADD_TEST_CASE(tcs, generate_log_name__after_log); + + ATF_ADD_TEST_CASE(tcs, log); + + ATF_ADD_TEST_CASE(tcs, set_inmemory__reset); + + ATF_ADD_TEST_CASE(tcs, set_persistency__no_backlog); + ATF_ADD_TEST_CASE(tcs, set_persistency__some_backlog__debug); + ATF_ADD_TEST_CASE(tcs, set_persistency__some_backlog__error); + ATF_ADD_TEST_CASE(tcs, set_persistency__some_backlog__info); + ATF_ADD_TEST_CASE(tcs, set_persistency__some_backlog__warning); + ATF_ADD_TEST_CASE(tcs, set_persistency__fail); +} diff --git a/utils/memory.cpp b/utils/memory.cpp new file mode 100644 index 000000000000..ca121f6f4dec --- /dev/null +++ b/utils/memory.cpp @@ -0,0 +1,158 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/memory.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#if defined(HAVE_SYS_TYPES_H) +# include +#endif +#if defined(HAVE_SYS_PARAM_H) +# include +#endif +#if defined(HAVE_SYS_SYSCTL_H) +# include +#endif +} + +#include +#include +#include +#include + +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/units.hpp" +#include "utils/sanity.hpp" + +namespace units = utils::units; + + +namespace { + + +/// Name of the method to query the available memory as detected by configure. +static const char* query_type = MEMORY_QUERY_TYPE; + + +/// Value of query_type when we do not know how to query the memory. +static const char* query_type_unknown = "unknown"; + + +/// Value of query_type when we have to use sysctlbyname(3). +static const char* query_type_sysctlbyname = "sysctlbyname"; + + +/// Name of the sysctl MIB with the physical memory as detected by configure. +/// +/// This should only be used if memory_query_type is 'sysctl'. +static const char* query_sysctl_mib = MEMORY_QUERY_SYSCTL_MIB; + + +#if !defined(HAVE_SYSCTLBYNAME) +/// Stub for sysctlbyname(3) for systems that don't have it. +/// +/// The whole purpose of this fake function is to allow the caller code to be +/// compiled on any machine regardless of the presence of sysctlbyname(3). This +/// will prevent the code from breaking when it is compiled on a machine without +/// this function. It also prevents "unused variable" warnings in the caller +/// code. +/// +/// \return Nothing; this always crashes. +static int +sysctlbyname(const char* /* name */, + void* /* oldp */, + std::size_t* /* oldlenp */, + const void* /* newp */, + std::size_t /* newlen */) +{ + UNREACHABLE; +} +#endif + + +} // anonymous namespace + + +/// Gets the value of an integral sysctl MIB. +/// +/// \pre The system supports the sysctlbyname(3) function. +/// +/// \param mib The name of the sysctl MIB to query. +/// +/// \return The value of the MIB, if found. +/// +/// \throw std::runtime_error If the sysctlbyname(3) call fails. This might be +/// a bit drastic. If it turns out that this causes problems, we could just +/// change the code to log the error instead of raising an exception. +static int64_t +query_sysctl(const char* mib) +{ + // This must be explicitly initialized to 0. If the sysctl query returned a + // value smaller in size than value_length, we would get garbage otherwise. + int64_t value = 0; + std::size_t value_length = sizeof(value); + if (::sysctlbyname(mib, &value, &value_length, NULL, 0) == -1) { + const int original_errno = errno; + throw std::runtime_error(F("Failed to get sysctl(%s) value: %s") % + mib % std::strerror(original_errno)); + } + return value; +} + + +/// Queries the total amount of physical memory. +/// +/// The real query is run only once and the result is cached. Further calls to +/// this function will always return the same value. +/// +/// \return The amount of physical memory, in bytes. If the code does not know +/// how to query the memory, this logs a warning and returns 0. +units::bytes +utils::physical_memory(void) +{ + static int64_t amount = -1; + if (amount == -1) { + if (std::strcmp(query_type, query_type_unknown) == 0) { + LW("Don't know how to query the physical memory"); + amount = 0; + } else if (std::strcmp(query_type, query_type_sysctlbyname) == 0) { + amount = query_sysctl(query_sysctl_mib); + } else + UNREACHABLE_MSG("Unimplemented memory query type"); + LI(F("Physical memory as returned by query type '%s': %s") % + query_type % amount); + } + POST(amount > -1); + return units::bytes(amount); +} diff --git a/utils/memory.hpp b/utils/memory.hpp new file mode 100644 index 000000000000..5a956a82005a --- /dev/null +++ b/utils/memory.hpp @@ -0,0 +1,45 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/memory.hpp +/// Utilities to query details of the system memory. + +#if !defined(UTILS_MEMORY_HPP) +#define UTILS_MEMORY_HPP + +#include "utils/units_fwd.hpp" + +namespace utils { + + +units::bytes physical_memory(void); + + +} // namespace utils + +#endif // !defined(UTILS_MEMORY_HPP) diff --git a/utils/memory_test.cpp b/utils/memory_test.cpp new file mode 100644 index 000000000000..66750fbe9c6c --- /dev/null +++ b/utils/memory_test.cpp @@ -0,0 +1,63 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include "utils/memory.hpp" + +#include + +#include + +#include "utils/units.hpp" + +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(physical_memory); +ATF_TEST_CASE_BODY(physical_memory) +{ + const units::bytes memory = utils::physical_memory(); + + if (std::strcmp(MEMORY_QUERY_TYPE, "unknown") == 0) { + ATF_REQUIRE(memory == 0); + } else if (std::strcmp(MEMORY_QUERY_TYPE, "sysctlbyname") == 0) { + ATF_REQUIRE(memory > 0); + ATF_REQUIRE(memory < 100 * units::TB); // Large enough for now... + } else { + fail("Unimplemented memory query type"); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, physical_memory); +} diff --git a/utils/noncopyable.hpp b/utils/noncopyable.hpp new file mode 100644 index 000000000000..6a0ad6bf713a --- /dev/null +++ b/utils/noncopyable.hpp @@ -0,0 +1,75 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/noncopyable.hpp +/// Provides the utils::noncopyable class. +/// +/// The class is provided as a separate module on its own to minimize +/// header-inclusion side-effects. + +#if !defined(UTILS_NONCOPYABLE_HPP) +#define UTILS_NONCOPYABLE_HPP + + +namespace utils { + + +/// Forbids copying a class at compile-time. +/// +/// Inheriting from this class delivers a private copy constructor and an +/// assignment operator that effectively forbid copying the class during +/// compilation. +/// +/// Always use private inheritance. +class noncopyable { + /// Data placeholder. + /// + /// The class cannot be empty; otherwise we get ABI-stability warnings + /// during the build, which will break it due to strict checking. + int _noncopyable_dummy; + + /// Private copy constructor to deny copying of subclasses. + noncopyable(const noncopyable&); + + /// Private assignment constructor to deny copying of subclasses. + /// + /// \return A reference to the object. + noncopyable& operator=(const noncopyable&); + +protected: + // Explicitly needed to provide some non-private functions. Otherwise + // we also get some warnings during the build. + noncopyable(void) {} + ~noncopyable(void) {} +}; + + +} // namespace utils + + +#endif // !defined(UTILS_NONCOPYABLE_HPP) diff --git a/utils/optional.hpp b/utils/optional.hpp new file mode 100644 index 000000000000..a4557bff5dc8 --- /dev/null +++ b/utils/optional.hpp @@ -0,0 +1,90 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/optional.hpp +/// Provides the utils::optional class. +/// +/// The class is provided as a separate module on its own to minimize +/// header-inclusion side-effects. + +#if !defined(UTILS_OPTIONAL_HPP) +#define UTILS_OPTIONAL_HPP + +#include "utils/optional_fwd.hpp" + +#include + +namespace utils { + + +/// Holds a data value or none. +/// +/// This class allows users to represent values that may be uninitialized. +/// Instead of having to keep separate variables to track whether a variable is +/// supposed to have a value or not, this class allows multiplexing the +/// behaviors. +/// +/// This class is a simplified version of Boost.Optional. +template< class T > +class optional { + /// Internal representation of the optional data value. + T* _data; + +public: + optional(void); + optional(utils::detail::none_t); + optional(const optional< T >&); + explicit optional(const T&); + ~optional(void); + + optional& operator=(utils::detail::none_t); + optional& operator=(const T&); + optional& operator=(const optional< T >&); + + bool operator==(const optional< T >&) const; + bool operator!=(const optional< T >&) const; + + operator bool(void) const; + + const T& get(void) const; + const T& get_default(const T&) const; + T& get(void); +}; + + +template< class T > +std::ostream& operator<<(std::ostream&, const optional< T >&); + + +template< class T > +optional< T > make_optional(const T&); + + +} // namespace utils + +#endif // !defined(UTILS_OPTIONAL_HPP) diff --git a/utils/optional.ipp b/utils/optional.ipp new file mode 100644 index 000000000000..3e2f3f878f2a --- /dev/null +++ b/utils/optional.ipp @@ -0,0 +1,252 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_OPTIONAL_IPP) +#define UTILS_OPTIONAL_IPP + +#include + +#include "utils/defs.hpp" +#include "utils/optional.hpp" +#include "utils/sanity.hpp" + + +/// Initializes an optional object to the none value. +template< class T > +utils::optional< T >::optional(void) : + _data(NULL) +{ +} + + +/// Explicitly initializes an optional object to the none value. +template< class T > +utils::optional< T >::optional(utils::detail::none_t /* none */) : + _data(NULL) +{ +} + + +/// Initializes an optional object to a non-none value. +/// +/// \param data The initial value for the object. +template< class T > +utils::optional< T >::optional(const T& data) : + _data(new T(data)) +{ +} + + +/// Copy constructor. +/// +/// \param other The optional object to copy from. +template< class T > +utils::optional< T >::optional(const optional< T >& other) : + _data(other._data == NULL ? NULL : new T(*(other._data))) +{ +} + + +/// Destructor. +template< class T > +utils::optional< T >::~optional(void) +{ + if (_data != NULL) + delete _data; + _data = NULL; // Prevent accidental reuse. +} + + +/// Explicitly assigns an optional object to the none value. +/// +/// \return A reference to this. +template< class T > +utils::optional< T >& +utils::optional< T >::operator=(utils::detail::none_t /* none */) +{ + if (_data != NULL) + delete _data; + _data = NULL; + return *this; +} + + +/// Assigns a new value to the optional object. +/// +/// \param data The initial value for the object. +/// +/// \return A reference to this. +template< class T > +utils::optional< T >& +utils::optional< T >::operator=(const T& data) +{ + T* new_data = new T(data); + if (_data != NULL) + delete _data; + _data = new_data; + return *this; +} + + +/// Copies an optional value. +/// +/// \param other The optional object to copy from. +/// +/// \return A reference to this. +template< class T > +utils::optional< T >& +utils::optional< T >::operator=(const optional< T >& other) +{ + T* new_data = other._data == NULL ? NULL : new T(*(other._data)); + if (_data != NULL) + delete _data; + _data = new_data; + return *this; +} + + +/// Equality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are equal; false otherwise. +template< class T > +bool +utils::optional< T >::operator==(const optional< T >& other) const +{ + if (_data == NULL && other._data == NULL) { + return true; + } else if (_data == NULL || other._data == NULL) { + return false; + } else { + INV(_data != NULL && other._data != NULL); + return *_data == *other._data; + } +} + + +/// Inequality comparator. +/// +/// \param other The other object to compare this one to. +/// +/// \return True if this object and other are different; false otherwise. +template< class T > +bool +utils::optional< T >::operator!=(const optional< T >& other) const +{ + return !(*this == other); +} + + +/// Gets the value hold by the optional object. +/// +/// \pre The optional object must not be none. +/// +/// \return A reference to the data. +template< class T > +const T& +utils::optional< T >::get(void) const +{ + PRE(_data != NULL); + return *_data; +} + + +/// Gets the value of this object with a default fallback. +/// +/// \param default_value The value to return if this object holds no value. +/// +/// \return A reference to the data in the optional object, or the reference +/// passed in as a parameter. +template< class T > +const T& +utils::optional< T >::get_default(const T& default_value) const +{ + if (_data != NULL) + return *_data; + else + return default_value; +} + + +/// Tests whether the optional object contains data or not. +/// +/// \return True if the object is not none; false otherwise. +template< class T > +utils::optional< T >::operator bool(void) const +{ + return _data != NULL; +} + + +/// Tests whether the optional object contains data or not. +/// +/// \return True if the object is not none; false otherwise. +template< class T > +T& +utils::optional< T >::get(void) +{ + PRE(_data != NULL); + return *_data; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param object The object to format. +/// +/// \return The output stream. +template< class T > +std::ostream& utils::operator<<(std::ostream& output, + const optional< T >& object) +{ + if (!object) { + output << "none"; + } else { + output << object.get(); + } + return output; +} + + +/// Helper function to instantiate optional objects. +/// +/// \param value The value for the optional object. Shouldn't be none, as +/// optional objects can be constructed from none right away. +/// +/// \return A new optional object. +template< class T > +utils::optional< T > +utils::make_optional(const T& value) +{ + return optional< T >(value); +} + + +#endif // !defined(UTILS_OPTIONAL_IPP) diff --git a/utils/optional_fwd.hpp b/utils/optional_fwd.hpp new file mode 100644 index 000000000000..931dbbfe88da --- /dev/null +++ b/utils/optional_fwd.hpp @@ -0,0 +1,61 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/optional_fwd.hpp +/// Forward declarations for utils/optional.hpp + +#if !defined(UTILS_OPTIONAL_FWD_HPP) +#define UTILS_OPTIONAL_FWD_HPP + +namespace utils { + + +namespace detail { + + +/// Internal type-safe representation for the none type. +struct none_t {}; + + +} // namespace detail + + +/// The none value. +/// +/// This has internal linkage so it is OK to define it in the header file. +/// However, pointers to none from different translation units will be +/// different. Just don't do that. +const detail::none_t none = {}; + + +template< class > class optional; + + +} // namespace utils + +#endif // !defined(UTILS_OPTIONAL_FWD_HPP) diff --git a/utils/optional_test.cpp b/utils/optional_test.cpp new file mode 100644 index 000000000000..debd8949852e --- /dev/null +++ b/utils/optional_test.cpp @@ -0,0 +1,285 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/optional.ipp" + +#include +#include + +#include + +using utils::none; +using utils::optional; + + +namespace { + + +/// Fake class to capture calls to the new and delete operators. +class test_alloc { +public: + /// Value to disambiguate objects after construction. + int value; + + /// Balance of alive instances of this class in dynamic memory. + static size_t instances; + + /// Constructs a new optional object. + /// + /// \param value_ The value to store in this object for disambiguation. + test_alloc(int value_) : value(value_) + { + } + + /// Allocates a new object and records its existence. + /// + /// \param size The amount of memory to allocate. + /// + /// \return A pointer to the allocated memory. + /// + /// \throw std::bad_alloc If the memory allocation fails. + void* + operator new(size_t size) + { + instances++; + std::cout << "test_alloc::operator new called\n"; + return ::operator new(size); + } + + /// Deallocates an existing object and unrecords its existence. + /// + /// \param mem The pointer to the memory to deallocate. + void + operator delete(void* mem) + { + instances--; + std::cout << "test_alloc::operator delete called\n"; + ::operator delete(mem); + } +}; + + +size_t test_alloc::instances = 0; + + +/// Constructs and returns an optional object. +/// +/// This is used by tests to validate that returning an object from within a +/// function works (i.e. the necessary constructors are available). +/// +/// \tparam Type The type of the object included in the optional wrapper. +/// \param value The value to put inside the optional wrapper. +/// +/// \return The constructed optional object. +template< typename Type > +optional< Type > +return_optional(const Type& value) +{ + return optional< Type >(value); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(ctors__native_type); +ATF_TEST_CASE_BODY(ctors__native_type) +{ + const optional< int > no_args; + ATF_REQUIRE(!no_args); + + const optional< int > with_none(none); + ATF_REQUIRE(!with_none); + + const optional< int > with_arg(3); + ATF_REQUIRE(with_arg); + ATF_REQUIRE_EQ(3, with_arg.get()); + + const optional< int > copy_none(with_none); + ATF_REQUIRE(!copy_none); + + const optional< int > copy_arg(with_arg); + ATF_REQUIRE(copy_arg); + ATF_REQUIRE_EQ(3, copy_arg.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(ctors__complex_type); +ATF_TEST_CASE_BODY(ctors__complex_type) +{ + const optional< std::string > no_args; + ATF_REQUIRE(!no_args); + + const optional< std::string > with_none(none); + ATF_REQUIRE(!with_none); + + const optional< std::string > with_arg("foo"); + ATF_REQUIRE(with_arg); + ATF_REQUIRE_EQ("foo", with_arg.get()); + + const optional< std::string > copy_none(with_none); + ATF_REQUIRE(!copy_none); + + const optional< std::string > copy_arg(with_arg); + ATF_REQUIRE(copy_arg); + ATF_REQUIRE_EQ("foo", copy_arg.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(assign); +ATF_TEST_CASE_BODY(assign) +{ + optional< int > from_default; + from_default = optional< int >(); + ATF_REQUIRE(!from_default); + + optional< int > from_none(3); + from_none = none; + ATF_REQUIRE(!from_none); + + optional< int > from_int; + from_int = 6; + ATF_REQUIRE_EQ(6, from_int.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(return); +ATF_TEST_CASE_BODY(return) +{ + optional< long > from_return(return_optional< long >(123)); + ATF_REQUIRE(from_return); + ATF_REQUIRE_EQ(123, from_return.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(memory); +ATF_TEST_CASE_BODY(memory) +{ + ATF_REQUIRE_EQ(0, test_alloc::instances); + { + optional< test_alloc > optional1(test_alloc(3)); + ATF_REQUIRE_EQ(1, test_alloc::instances); + ATF_REQUIRE_EQ(3, optional1.get().value); + + { + optional< test_alloc > optional2(optional1); + ATF_REQUIRE_EQ(2, test_alloc::instances); + ATF_REQUIRE_EQ(3, optional2.get().value); + + optional2 = 5; + ATF_REQUIRE_EQ(2, test_alloc::instances); + ATF_REQUIRE_EQ(5, optional2.get().value); + ATF_REQUIRE_EQ(3, optional1.get().value); + } + ATF_REQUIRE_EQ(1, test_alloc::instances); + ATF_REQUIRE_EQ(3, optional1.get().value); + } + ATF_REQUIRE_EQ(0, test_alloc::instances); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(get_default); +ATF_TEST_CASE_BODY(get_default) +{ + const std::string def_value = "hello"; + optional< std::string > optional; + ATF_REQUIRE(&def_value == &optional.get_default(def_value)); + optional = "bye"; + ATF_REQUIRE_EQ("bye", optional.get_default(def_value)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(make_optional); +ATF_TEST_CASE_BODY(make_optional) +{ + optional< int > opt = utils::make_optional(576); + ATF_REQUIRE(opt); + ATF_REQUIRE_EQ(576, opt.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne); +ATF_TEST_CASE_BODY(operators_eq_and_ne) +{ + optional< int > opt1, opt2; + + opt1 = none; opt2 = none; + ATF_REQUIRE( opt1 == opt2); + ATF_REQUIRE(!(opt1 != opt2)); + + opt1 = utils::make_optional(5); opt2 = none; + ATF_REQUIRE(!(opt1 == opt2)); + ATF_REQUIRE( opt1 != opt2); + + opt1 = none; opt2 = utils::make_optional(5); + ATF_REQUIRE(!(opt1 == opt2)); + ATF_REQUIRE( opt1 != opt2); + + opt1 = utils::make_optional(5); opt2 = utils::make_optional(5); + ATF_REQUIRE( opt1 == opt2); + ATF_REQUIRE(!(opt1 != opt2)); + + opt1 = utils::make_optional(6); opt2 = utils::make_optional(5); + ATF_REQUIRE(!(opt1 == opt2)); + ATF_REQUIRE( opt1 != opt2); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output); +ATF_TEST_CASE_BODY(output) +{ + { + std::ostringstream str; + str << optional< int >(none); + ATF_REQUIRE_EQ("none", str.str()); + } + { + std::ostringstream str; + str << optional< int >(5); + ATF_REQUIRE_EQ("5", str.str()); + } + { + std::ostringstream str; + str << optional< std::string >("this is a text"); + ATF_REQUIRE_EQ("this is a text", str.str()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ctors__native_type); + ATF_ADD_TEST_CASE(tcs, ctors__complex_type); + ATF_ADD_TEST_CASE(tcs, assign); + ATF_ADD_TEST_CASE(tcs, return); + ATF_ADD_TEST_CASE(tcs, memory); + ATF_ADD_TEST_CASE(tcs, get_default); + ATF_ADD_TEST_CASE(tcs, make_optional); + ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne); + ATF_ADD_TEST_CASE(tcs, output); +} diff --git a/utils/passwd.cpp b/utils/passwd.cpp new file mode 100644 index 000000000000..32a16bb4d462 --- /dev/null +++ b/utils/passwd.cpp @@ -0,0 +1,194 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/passwd.hpp" + +extern "C" { +#include + +#include +#include +} + +#include + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace passwd_ns = utils::passwd; + + +namespace { + + +/// If defined, replaces the value returned by current_user(). +static utils::optional< passwd_ns::user > fake_current_user; + + +/// If not empty, defines the current set of mock users. +static std::vector< passwd_ns::user > mock_users; + + +/// Formats a user for logging purposes. +/// +/// \param user The user to format. +/// +/// \return The user as a string. +static std::string +format_user(const passwd_ns::user& user) +{ + return F("name=%s, uid=%s, gid=%s") % user.name % user.uid % user.gid; +} + + +} // anonymous namespace + + +/// Constructs a new user. +/// +/// \param name_ The name of the user. +/// \param uid_ The user identifier. +/// \param gid_ The login group identifier. +passwd_ns::user::user(const std::string& name_, const unsigned int uid_, + const unsigned int gid_) : + name(name_), + uid(uid_), + gid(gid_) +{ +} + + +/// Checks if the user has superpowers or not. +/// +/// \return True if the user is root, false otherwise. +bool +passwd_ns::user::is_root(void) const +{ + return uid == 0; +} + + +/// Gets the current user. +/// +/// \return The current user. +passwd_ns::user +passwd_ns::current_user(void) +{ + if (fake_current_user) { + const user u = fake_current_user.get(); + LD(F("Current user is fake: %s") % format_user(u)); + return u; + } else { + const user u = find_user_by_uid(::getuid()); + LD(F("Current user is: %s") % format_user(u)); + return u; + } +} + + +/// Gets information about a user by its name. +/// +/// \param name The name of the user to query. +/// +/// \return The information about the user. +/// +/// \throw std::runtime_error If the user does not exist. +passwd_ns::user +passwd_ns::find_user_by_name(const std::string& name) +{ + if (mock_users.empty()) { + const struct ::passwd* pw = ::getpwnam(name.c_str()); + if (pw == NULL) + throw std::runtime_error(F("Failed to get information about the " + "user '%s'") % name); + INV(pw->pw_name == name); + return user(pw->pw_name, pw->pw_uid, pw->pw_gid); + } else { + for (std::vector< user >::const_iterator iter = mock_users.begin(); + iter != mock_users.end(); iter++) { + if ((*iter).name == name) + return *iter; + } + throw std::runtime_error(F("Failed to get information about the " + "user '%s'") % name); + } +} + + +/// Gets information about a user by its identifier. +/// +/// \param uid The identifier of the user to query. +/// +/// \return The information about the user. +/// +/// \throw std::runtime_error If the user does not exist. +passwd_ns::user +passwd_ns::find_user_by_uid(const unsigned int uid) +{ + if (mock_users.empty()) { + const struct ::passwd* pw = ::getpwuid(uid); + if (pw == NULL) + throw std::runtime_error(F("Failed to get information about the " + "user with UID %s") % uid); + INV(pw->pw_uid == uid); + return user(pw->pw_name, pw->pw_uid, pw->pw_gid); + } else { + for (std::vector< user >::const_iterator iter = mock_users.begin(); + iter != mock_users.end(); iter++) { + if ((*iter).uid == uid) + return *iter; + } + throw std::runtime_error(F("Failed to get information about the " + "user with UID %s") % uid); + } +} + + +/// Overrides the current user for testing purposes. +/// +/// This DOES NOT change the current privileges! +/// +/// \param new_current_user The new current user. +void +passwd_ns::set_current_user_for_testing(const user& new_current_user) +{ + fake_current_user = new_current_user; +} + + +/// Overrides the current set of users for testing purposes. +/// +/// \param users The new users set. Cannot be empty. +void +passwd_ns::set_mock_users_for_testing(const std::vector< user >& users) +{ + PRE(!users.empty()); + mock_users = users; +} diff --git a/utils/passwd.hpp b/utils/passwd.hpp new file mode 100644 index 000000000000..e0b17c547080 --- /dev/null +++ b/utils/passwd.hpp @@ -0,0 +1,72 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/passwd.hpp +/// Querying and manipulation of users and groups. + +#if !defined(UTILS_PASSWD_HPP) +#define UTILS_PASSWD_HPP + +#include "utils/passwd_fwd.hpp" + +#include +#include + +namespace utils { +namespace passwd { + + +/// Represents a system user. +class user { +public: + /// The name of the user. + std::string name; + + /// The system-wide identifier of the user. + unsigned int uid; + + /// The login group identifier for the user. + unsigned int gid; + + user(const std::string&, const unsigned int, const unsigned int); + + bool is_root(void) const; +}; + + +user current_user(void); +user find_user_by_name(const std::string&); +user find_user_by_uid(const unsigned int); +void set_current_user_for_testing(const user&); +void set_mock_users_for_testing(const std::vector< user >&); + + +} // namespace passwd +} // namespace utils + +#endif // !defined(UTILS_PASSWD_HPP) diff --git a/utils/passwd_fwd.hpp b/utils/passwd_fwd.hpp new file mode 100644 index 000000000000..bedbd34c8af8 --- /dev/null +++ b/utils/passwd_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/passwd_fwd.hpp +/// Forward declarations for utils/passwd.hpp + +#if !defined(UTILS_PASSWD_FWD_HPP) +#define UTILS_PASSWD_FWD_HPP + +namespace utils { +namespace passwd { + + +class user; + + +} // namespace passwd +} // namespace utils + +#endif // !defined(UTILS_PASSWD_FWD_HPP) diff --git a/utils/passwd_test.cpp b/utils/passwd_test.cpp new file mode 100644 index 000000000000..720ecb32e5fe --- /dev/null +++ b/utils/passwd_test.cpp @@ -0,0 +1,179 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/passwd.hpp" + +extern "C" { +#include + +#include +#include +} + +#include +#include + +#include + +namespace passwd_ns = utils::passwd; + + +ATF_TEST_CASE_WITHOUT_HEAD(user__public_fields); +ATF_TEST_CASE_BODY(user__public_fields) +{ + const passwd_ns::user user("the-name", 1, 2); + ATF_REQUIRE_EQ("the-name", user.name); + ATF_REQUIRE_EQ(1, user.uid); + ATF_REQUIRE_EQ(2, user.gid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(user__is_root__true); +ATF_TEST_CASE_BODY(user__is_root__true) +{ + const passwd_ns::user user("i-am-root", 0, 10); + ATF_REQUIRE(user.is_root()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(user__is_root__false); +ATF_TEST_CASE_BODY(user__is_root__false) +{ + const passwd_ns::user user("i-am-not-root", 123, 10); + ATF_REQUIRE(!user.is_root()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_user); +ATF_TEST_CASE_BODY(current_user) +{ + const passwd_ns::user user = passwd_ns::current_user(); + ATF_REQUIRE_EQ(::getuid(), user.uid); + ATF_REQUIRE_EQ(::getgid(), user.gid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(current_user__fake); +ATF_TEST_CASE_BODY(current_user__fake) +{ + const passwd_ns::user new_user("someone-else", ::getuid() + 1, 0); + passwd_ns::set_current_user_for_testing(new_user); + + const passwd_ns::user user = passwd_ns::current_user(); + ATF_REQUIRE(::getuid() != user.uid); + ATF_REQUIRE_EQ(new_user.uid, user.uid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_name__ok); +ATF_TEST_CASE_BODY(find_user_by_name__ok) +{ + const struct ::passwd* pw = ::getpwuid(::getuid()); + ATF_REQUIRE(pw != NULL); + + const passwd_ns::user user = passwd_ns::find_user_by_name(pw->pw_name); + ATF_REQUIRE_EQ(::getuid(), user.uid); + ATF_REQUIRE_EQ(::getgid(), user.gid); + ATF_REQUIRE_EQ(pw->pw_name, user.name); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_name__fail); +ATF_TEST_CASE_BODY(find_user_by_name__fail) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "Failed.*user 'i-do-not-exist'", + passwd_ns::find_user_by_name("i-do-not-exist")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_name__fake); +ATF_TEST_CASE_BODY(find_user_by_name__fake) +{ + std::vector< passwd_ns::user > users; + users.push_back(passwd_ns::user("myself2", 20, 40)); + users.push_back(passwd_ns::user("myself1", 10, 15)); + users.push_back(passwd_ns::user("myself3", 30, 60)); + passwd_ns::set_mock_users_for_testing(users); + + const passwd_ns::user user = passwd_ns::find_user_by_name("myself1"); + ATF_REQUIRE_EQ(10, user.uid); + ATF_REQUIRE_EQ(15, user.gid); + ATF_REQUIRE_EQ("myself1", user.name); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Failed.*user 'root'", + passwd_ns::find_user_by_name("root")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_uid__ok); +ATF_TEST_CASE_BODY(find_user_by_uid__ok) +{ + const passwd_ns::user user = passwd_ns::find_user_by_uid(::getuid()); + ATF_REQUIRE_EQ(::getuid(), user.uid); + ATF_REQUIRE_EQ(::getgid(), user.gid); + + const struct ::passwd* pw = ::getpwuid(::getuid()); + ATF_REQUIRE(pw != NULL); + ATF_REQUIRE_EQ(pw->pw_name, user.name); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_user_by_uid__fake); +ATF_TEST_CASE_BODY(find_user_by_uid__fake) +{ + std::vector< passwd_ns::user > users; + users.push_back(passwd_ns::user("myself2", 20, 40)); + users.push_back(passwd_ns::user("myself1", 10, 15)); + users.push_back(passwd_ns::user("myself3", 30, 60)); + passwd_ns::set_mock_users_for_testing(users); + + const passwd_ns::user user = passwd_ns::find_user_by_uid(10); + ATF_REQUIRE_EQ(10, user.uid); + ATF_REQUIRE_EQ(15, user.gid); + ATF_REQUIRE_EQ("myself1", user.name); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Failed.*user.*UID 0", + passwd_ns::find_user_by_uid(0)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, user__public_fields); + ATF_ADD_TEST_CASE(tcs, user__is_root__true); + ATF_ADD_TEST_CASE(tcs, user__is_root__false); + + ATF_ADD_TEST_CASE(tcs, current_user); + ATF_ADD_TEST_CASE(tcs, current_user__fake); + + ATF_ADD_TEST_CASE(tcs, find_user_by_name__ok); + ATF_ADD_TEST_CASE(tcs, find_user_by_name__fail); + ATF_ADD_TEST_CASE(tcs, find_user_by_name__fake); + ATF_ADD_TEST_CASE(tcs, find_user_by_uid__ok); + ATF_ADD_TEST_CASE(tcs, find_user_by_uid__fake); +} diff --git a/utils/process/.gitignore b/utils/process/.gitignore new file mode 100644 index 000000000000..fb3291b39e0c --- /dev/null +++ b/utils/process/.gitignore @@ -0,0 +1 @@ +helpers diff --git a/utils/process/Kyuafile b/utils/process/Kyuafile new file mode 100644 index 000000000000..92e62cfac6fc --- /dev/null +++ b/utils/process/Kyuafile @@ -0,0 +1,13 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="child_test"} +atf_test_program{name="deadline_killer_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="executor_test"} +atf_test_program{name="fdstream_test"} +atf_test_program{name="isolation_test"} +atf_test_program{name="operations_test"} +atf_test_program{name="status_test"} +atf_test_program{name="systembuf_test"} diff --git a/utils/process/Makefile.am.inc b/utils/process/Makefile.am.inc new file mode 100644 index 000000000000..3cff02e7e455 --- /dev/null +++ b/utils/process/Makefile.am.inc @@ -0,0 +1,113 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +libutils_a_SOURCES += utils/process/child.cpp +libutils_a_SOURCES += utils/process/child.hpp +libutils_a_SOURCES += utils/process/child.ipp +libutils_a_SOURCES += utils/process/child_fwd.hpp +libutils_a_SOURCES += utils/process/deadline_killer.cpp +libutils_a_SOURCES += utils/process/deadline_killer.hpp +libutils_a_SOURCES += utils/process/deadline_killer_fwd.hpp +libutils_a_SOURCES += utils/process/exceptions.cpp +libutils_a_SOURCES += utils/process/exceptions.hpp +libutils_a_SOURCES += utils/process/executor.cpp +libutils_a_SOURCES += utils/process/executor.hpp +libutils_a_SOURCES += utils/process/executor.ipp +libutils_a_SOURCES += utils/process/executor_fwd.hpp +libutils_a_SOURCES += utils/process/fdstream.cpp +libutils_a_SOURCES += utils/process/fdstream.hpp +libutils_a_SOURCES += utils/process/fdstream_fwd.hpp +libutils_a_SOURCES += utils/process/isolation.cpp +libutils_a_SOURCES += utils/process/isolation.hpp +libutils_a_SOURCES += utils/process/operations.cpp +libutils_a_SOURCES += utils/process/operations.hpp +libutils_a_SOURCES += utils/process/operations_fwd.hpp +libutils_a_SOURCES += utils/process/status.cpp +libutils_a_SOURCES += utils/process/status.hpp +libutils_a_SOURCES += utils/process/status_fwd.hpp +libutils_a_SOURCES += utils/process/system.cpp +libutils_a_SOURCES += utils/process/system.hpp +libutils_a_SOURCES += utils/process/systembuf.cpp +libutils_a_SOURCES += utils/process/systembuf.hpp +libutils_a_SOURCES += utils/process/systembuf_fwd.hpp + +if WITH_ATF +tests_utils_processdir = $(pkgtestsdir)/utils/process + +tests_utils_process_DATA = utils/process/Kyuafile +EXTRA_DIST += $(tests_utils_process_DATA) + +tests_utils_process_PROGRAMS = utils/process/child_test +utils_process_child_test_SOURCES = utils/process/child_test.cpp +utils_process_child_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_child_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/deadline_killer_test +utils_process_deadline_killer_test_SOURCES = \ + utils/process/deadline_killer_test.cpp +utils_process_deadline_killer_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_deadline_killer_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/exceptions_test +utils_process_exceptions_test_SOURCES = utils/process/exceptions_test.cpp +utils_process_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/executor_test +utils_process_executor_test_SOURCES = utils/process/executor_test.cpp +utils_process_executor_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_executor_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/fdstream_test +utils_process_fdstream_test_SOURCES = utils/process/fdstream_test.cpp +utils_process_fdstream_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_fdstream_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/isolation_test +utils_process_isolation_test_SOURCES = utils/process/isolation_test.cpp +utils_process_isolation_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_isolation_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/helpers +utils_process_helpers_SOURCES = utils/process/helpers.cpp + +tests_utils_process_PROGRAMS += utils/process/operations_test +utils_process_operations_test_SOURCES = utils/process/operations_test.cpp +utils_process_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/status_test +utils_process_status_test_SOURCES = utils/process/status_test.cpp +utils_process_status_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_status_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_process_PROGRAMS += utils/process/systembuf_test +utils_process_systembuf_test_SOURCES = utils/process/systembuf_test.cpp +utils_process_systembuf_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_process_systembuf_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/process/child.cpp b/utils/process/child.cpp new file mode 100644 index 000000000000..fef09ccaad3b --- /dev/null +++ b/utils/process/child.cpp @@ -0,0 +1,385 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/child.ipp" + +extern "C" { +#include +#include + +#include +#include +#include +} + +#include +#include +#include + +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/process/exceptions.hpp" +#include "utils/process/fdstream.hpp" +#include "utils/process/operations.hpp" +#include "utils/process/system.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/interrupts.hpp" + + +namespace utils { +namespace process { + + +/// Private implementation fields for child objects. +struct child::impl : utils::noncopyable { + /// The process identifier. + pid_t _pid; + + /// The input stream for the process' stdout and stderr. May be NULL. + std::auto_ptr< process::ifdstream > _output; + + /// Initializes private implementation data. + /// + /// \param pid The process identifier. + /// \param output The input stream. Grabs ownership of the pointer. + impl(const pid_t pid, process::ifdstream* output) : + _pid(pid), _output(output) {} +}; + + +} // namespace process +} // namespace utils + + +namespace fs = utils::fs; +namespace process = utils::process; +namespace signals = utils::signals; + + +namespace { + + +/// Exception-based version of dup(2). +/// +/// \param old_fd The file descriptor to duplicate. +/// \param new_fd The file descriptor to use as the duplicate. This is +/// closed if it was open before the copy happens. +/// +/// \throw process::system_error If the call to dup2(2) fails. +static void +safe_dup(const int old_fd, const int new_fd) +{ + if (process::detail::syscall_dup2(old_fd, new_fd) == -1) { + const int original_errno = errno; + throw process::system_error(F("dup2(%s, %s) failed") % old_fd % new_fd, + original_errno); + } +} + + +/// Exception-based version of open(2) to open (or create) a file for append. +/// +/// \param filename The file to open in append mode. +/// +/// \return The file descriptor for the opened or created file. +/// +/// \throw process::system_error If the call to open(2) fails. +static int +open_for_append(const fs::path& filename) +{ + const int fd = process::detail::syscall_open( + filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd == -1) { + const int original_errno = errno; + throw process::system_error(F("Failed to create %s because open(2) " + "failed") % filename, original_errno); + } + return fd; +} + + +/// Logs the execution of another program. +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +static void +log_exec(const fs::path& program, const process::args_vector& args) +{ + std::string plain_command = program.str(); + for (process::args_vector::const_iterator iter = args.begin(); + iter != args.end(); ++iter) + plain_command += F(" %s") % *iter; + LD(F("Executing %s") % plain_command); +} + + +} // anonymous namespace + + +/// Prints out a fatal error and aborts. +void +utils::process::detail::report_error_and_abort(void) +{ + std::cerr << "Caught unknown exception\n"; + std::abort(); +} + + +/// Prints out a fatal error and aborts. +/// +/// \param error The error to display. +void +utils::process::detail::report_error_and_abort(const std::runtime_error& error) +{ + std::cerr << "Caught runtime_error: " << error.what() << '\n'; + std::abort(); +} + + +/// Creates a new child. +/// +/// \param implptr A dynamically-allocated impl object with the contents of the +/// new child. +process::child::child(impl *implptr) : + _pimpl(implptr) +{ +} + + +/// Destructor for child. +process::child::~child(void) +{ +} + + +/// Helper function for fork(). +/// +/// Please note: if you update this function to change the return type or to +/// raise different errors, do not forget to update fork() accordingly. +/// +/// \return In the case of the parent, a new child object returned as a +/// dynamically-allocated object because children classes are unique and thus +/// noncopyable. In the case of the child, a NULL pointer. +/// +/// \throw process::system_error If the calls to pipe(2) or fork(2) fail. +std::auto_ptr< process::child > +process::child::fork_capture_aux(void) +{ + std::cout.flush(); + std::cerr.flush(); + + int fds[2]; + if (detail::syscall_pipe(fds) == -1) + throw process::system_error("pipe(2) failed", errno); + + std::auto_ptr< signals::interrupts_inhibiter > inhibiter( + new signals::interrupts_inhibiter); + pid_t pid = detail::syscall_fork(); + if (pid == -1) { + inhibiter.reset(NULL); // Unblock signals. + ::close(fds[0]); + ::close(fds[1]); + throw process::system_error("fork(2) failed", errno); + } else if (pid == 0) { + inhibiter.reset(NULL); // Unblock signals. + ::setsid(); + + try { + ::close(fds[0]); + safe_dup(fds[1], STDOUT_FILENO); + safe_dup(fds[1], STDERR_FILENO); + ::close(fds[1]); + } catch (const system_error& e) { + std::cerr << F("Failed to set up subprocess: %s\n") % e.what(); + std::abort(); + } + return std::auto_ptr< process::child >(NULL); + } else { + ::close(fds[1]); + LD(F("Spawned process %s: stdout and stderr inherited") % pid); + signals::add_pid_to_kill(pid); + inhibiter.reset(NULL); // Unblock signals. + return std::auto_ptr< process::child >( + new process::child(new impl(pid, new process::ifdstream(fds[0])))); + } +} + + +/// Helper function for fork(). +/// +/// Please note: if you update this function to change the return type or to +/// raise different errors, do not forget to update fork() accordingly. +/// +/// \param stdout_file The name of the file in which to store the stdout. +/// If this has the magic value /dev/stdout, then the parent's stdout is +/// reused without applying any redirection. +/// \param stderr_file The name of the file in which to store the stderr. +/// If this has the magic value /dev/stderr, then the parent's stderr is +/// reused without applying any redirection. +/// +/// \return In the case of the parent, a new child object returned as a +/// dynamically-allocated object because children classes are unique and thus +/// noncopyable. In the case of the child, a NULL pointer. +/// +/// \throw process::system_error If the call to fork(2) fails. +std::auto_ptr< process::child > +process::child::fork_files_aux(const fs::path& stdout_file, + const fs::path& stderr_file) +{ + std::cout.flush(); + std::cerr.flush(); + + std::auto_ptr< signals::interrupts_inhibiter > inhibiter( + new signals::interrupts_inhibiter); + pid_t pid = detail::syscall_fork(); + if (pid == -1) { + inhibiter.reset(NULL); // Unblock signals. + throw process::system_error("fork(2) failed", errno); + } else if (pid == 0) { + inhibiter.reset(NULL); // Unblock signals. + ::setsid(); + + try { + if (stdout_file != fs::path("/dev/stdout")) { + const int stdout_fd = open_for_append(stdout_file); + safe_dup(stdout_fd, STDOUT_FILENO); + ::close(stdout_fd); + } + if (stderr_file != fs::path("/dev/stderr")) { + const int stderr_fd = open_for_append(stderr_file); + safe_dup(stderr_fd, STDERR_FILENO); + ::close(stderr_fd); + } + } catch (const system_error& e) { + std::cerr << F("Failed to set up subprocess: %s\n") % e.what(); + std::abort(); + } + return std::auto_ptr< process::child >(NULL); + } else { + LD(F("Spawned process %s: stdout=%s, stderr=%s") % pid % stdout_file % + stderr_file); + signals::add_pid_to_kill(pid); + inhibiter.reset(NULL); // Unblock signals. + return std::auto_ptr< process::child >( + new process::child(new impl(pid, NULL))); + } +} + + +/// Spawns a new binary and multiplexes and captures its stdout and stderr. +/// +/// If the subprocess cannot be completely set up for any reason, it attempts to +/// dump an error message to its stderr channel and it then calls std::abort(). +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +/// +/// \return A new child object, returned as a dynamically-allocated object +/// because children classes are unique and thus noncopyable. +/// +/// \throw process::system_error If the process cannot be spawned due to a +/// system call error. +std::auto_ptr< process::child > +process::child::spawn_capture(const fs::path& program, const args_vector& args) +{ + std::auto_ptr< child > child = fork_capture_aux(); + if (child.get() == NULL) + exec(program, args); + log_exec(program, args); + return child; +} + + +/// Spawns a new binary and redirects its stdout and stderr to files. +/// +/// If the subprocess cannot be completely set up for any reason, it attempts to +/// dump an error message to its stderr channel and it then calls std::abort(). +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +/// \param stdout_file The name of the file in which to store the stdout. +/// \param stderr_file The name of the file in which to store the stderr. +/// +/// \return A new child object, returned as a dynamically-allocated object +/// because children classes are unique and thus noncopyable. +/// +/// \throw process::system_error If the process cannot be spawned due to a +/// system call error. +std::auto_ptr< process::child > +process::child::spawn_files(const fs::path& program, + const args_vector& args, + const fs::path& stdout_file, + const fs::path& stderr_file) +{ + std::auto_ptr< child > child = fork_files_aux(stdout_file, stderr_file); + if (child.get() == NULL) + exec(program, args); + log_exec(program, args); + return child; +} + + +/// Returns the process identifier of this child. +/// +/// \return A process identifier. +int +process::child::pid(void) const +{ + return _pimpl->_pid; +} + + +/// Gets the input stream corresponding to the stdout and stderr of the child. +/// +/// \pre The child must have been started by fork_capture(). +/// +/// \return A reference to the input stream connected to the output of the test +/// case. +std::istream& +process::child::output(void) +{ + PRE(_pimpl->_output.get() != NULL); + return *_pimpl->_output; +} + + +/// Blocks to wait for completion. +/// +/// \return The termination status of the child process. +/// +/// \throw process::system_error If the call to waitpid(2) fails. +process::status +process::child::wait(void) +{ + return process::wait(_pimpl->_pid); +} diff --git a/utils/process/child.hpp b/utils/process/child.hpp new file mode 100644 index 000000000000..2c9450f6500a --- /dev/null +++ b/utils/process/child.hpp @@ -0,0 +1,113 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/child.hpp +/// Spawning and manipulation of children processes. +/// +/// The child module provides a set of functions to spawn subprocesses with +/// different settings, and the corresponding set of classes to interact with +/// said subprocesses. The interfaces to fork subprocesses are very simplified +/// and only provide the minimum functionality required by the rest of the +/// project. +/// +/// Be aware that the semantics of the fork and wait methods exposed by this +/// module are slightly different from that of the native calls. Any process +/// spawned by fork here will be isolated in its own session; once any of +/// such children processes is awaited for, its whole process group will be +/// terminated. This is the semantics we want in the above layers to ensure +/// that test programs (and, for that matter, external utilities) do not leak +/// subprocesses on the system. + +#if !defined(UTILS_PROCESS_CHILD_HPP) +#define UTILS_PROCESS_CHILD_HPP + +#include "utils/process/child_fwd.hpp" + +#include +#include +#include + +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/noncopyable.hpp" +#include "utils/process/operations_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace utils { +namespace process { + + +namespace detail { + +void report_error_and_abort(void) UTILS_NORETURN; +void report_error_and_abort(const std::runtime_error&) UTILS_NORETURN; + + +} // namespace detail + + +/// Child process spawner and controller. +class child : noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + + static std::auto_ptr< child > fork_capture_aux(void); + + static std::auto_ptr< child > fork_files_aux(const fs::path&, + const fs::path&); + + explicit child(impl *); + +public: + ~child(void); + + template< typename Hook > + static std::auto_ptr< child > fork_capture(Hook); + std::istream& output(void); + + template< typename Hook > + static std::auto_ptr< child > fork_files(Hook, const fs::path&, + const fs::path&); + + static std::auto_ptr< child > spawn_capture( + const fs::path&, const args_vector&); + static std::auto_ptr< child > spawn_files( + const fs::path&, const args_vector&, const fs::path&, const fs::path&); + + int pid(void) const; + + status wait(void); +}; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_CHILD_HPP) diff --git a/utils/process/child.ipp b/utils/process/child.ipp new file mode 100644 index 000000000000..aa90373652fd --- /dev/null +++ b/utils/process/child.ipp @@ -0,0 +1,110 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_PROCESS_CHILD_IPP) +#define UTILS_PROCESS_CHILD_IPP + +#include + +#include "utils/process/child.hpp" + +namespace utils { +namespace process { + + +/// Spawns a new subprocess and redirects its stdout and stderr to files. +/// +/// If the subprocess cannot be completely set up for any reason, it attempts to +/// dump an error message to its stderr channel and it then calls std::abort(). +/// +/// \param hook The function to execute in the subprocess. Must not return. +/// \param stdout_file The name of the file in which to store the stdout. +/// \param stderr_file The name of the file in which to store the stderr. +/// +/// \return A new child object, returned as a dynamically-allocated object +/// because children classes are unique and thus noncopyable. +/// +/// \throw process::system_error If the process cannot be spawned due to a +/// system call error. +template< typename Hook > +std::auto_ptr< child > +child::fork_files(Hook hook, const fs::path& stdout_file, + const fs::path& stderr_file) +{ + std::auto_ptr< child > child = fork_files_aux(stdout_file, stderr_file); + if (child.get() == NULL) { + try { + hook(); + std::abort(); + } catch (const std::runtime_error& e) { + detail::report_error_and_abort(e); + } catch (...) { + detail::report_error_and_abort(); + } + } + + return child; +} + + +/// Spawns a new subprocess and multiplexes and captures its stdout and stderr. +/// +/// If the subprocess cannot be completely set up for any reason, it attempts to +/// dump an error message to its stderr channel and it then calls std::abort(). +/// +/// \param hook The function to execute in the subprocess. Must not return. +/// +/// \return A new child object, returned as a dynamically-allocated object +/// because children classes are unique and thus noncopyable. +/// +/// \throw process::system_error If the process cannot be spawned due to a +/// system call error. +template< typename Hook > +std::auto_ptr< child > +child::fork_capture(Hook hook) +{ + std::auto_ptr< child > child = fork_capture_aux(); + if (child.get() == NULL) { + try { + hook(); + std::abort(); + } catch (const std::runtime_error& e) { + detail::report_error_and_abort(e); + } catch (...) { + detail::report_error_and_abort(); + } + } + + return child; +} + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_CHILD_IPP) diff --git a/utils/process/child_fwd.hpp b/utils/process/child_fwd.hpp new file mode 100644 index 000000000000..4d4caa17d58c --- /dev/null +++ b/utils/process/child_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/child_fwd.hpp +/// Forward declarations for utils/process/child.hpp + +#if !defined(UTILS_PROCESS_CHILD_FWD_HPP) +#define UTILS_PROCESS_CHILD_FWD_HPP + +namespace utils { +namespace process { + + +class child; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_CHILD_FWD_HPP) diff --git a/utils/process/child_test.cpp b/utils/process/child_test.cpp new file mode 100644 index 000000000000..69de9991ae13 --- /dev/null +++ b/utils/process/child_test.cpp @@ -0,0 +1,846 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/child.ipp" + +extern "C" { +#include +#include + +#include +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/process/exceptions.hpp" +#include "utils/process/status.hpp" +#include "utils/process/system.hpp" +#include "utils/sanity.hpp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace process = utils::process; + + +namespace { + + +/// Checks if the current subprocess is in its own session. +static void +child_check_own_session(void) +{ + std::exit((::getsid(::getpid()) == ::getpid()) ? + EXIT_SUCCESS : EXIT_FAILURE); +} + + +/// Body for a process that prints a simple message and exits. +/// +/// \tparam ExitStatus The exit status for the subprocess. +/// \tparam Message A single character that will be prepended to the printed +/// messages. This would ideally be a string, but we cannot templatize a +/// function with an object nor a pointer. +template< int ExitStatus, char Message > +static void +child_simple_function(void) +{ + std::cout << "To stdout: " << Message << "\n"; + std::cerr << "To stderr: " << Message << "\n"; + std::exit(ExitStatus); +} + + +/// Functor for the body of a process that prints a simple message and exits. +class child_simple_functor { + /// The exit status that the subprocess will yield. + int _exitstatus; + + /// The message to print on stdout and stderr. + std::string _message; + +public: + /// Constructs a new functor. + /// + /// \param exitstatus The exit status that the subprocess will yield. + /// \param message The message to print on stdout and stderr. + child_simple_functor(const int exitstatus, const std::string& message) : + _exitstatus(exitstatus), + _message(message) + { + } + + /// Body for the subprocess. + void + operator()(void) + { + std::cout << "To stdout: " << _message << "\n"; + std::cerr << "To stderr: " << _message << "\n"; + std::exit(_exitstatus); + } +}; + + +/// Body for a process that prints many messages to stdout and exits. +/// +/// The goal of this body is to validate that any buffering performed on the +/// parent process to read the output of the subprocess works correctly. +static void +child_printer_function(void) +{ + for (std::size_t i = 0; i < 100; i++) + std::cout << "This is a message to stdout, sequence " << i << "\n"; + std::cout.flush(); + std::cerr << "Exiting\n"; + std::exit(EXIT_SUCCESS); +} + + +/// Functor for the body of a process that runs child_printer_function. +class child_printer_functor { +public: + /// Body for the subprocess. + void + operator()(void) + { + child_printer_function(); + } +}; + + +/// Body for a child process that throws an exception. +static void +child_throw_exception(void) +{ + throw std::runtime_error("A loose exception"); +} + + +/// Body for a child process that creates a pidfile. +static void +child_write_pid(void) +{ + std::ofstream output("pidfile"); + output << ::getpid() << "\n"; + output.close(); + std::exit(EXIT_SUCCESS); +} + + +/// A child process that returns. +/// +/// The fork() wrappers are supposed to capture this condition and terminate the +/// child before the code returns to the fork() call point. +static void +child_return(void) +{ +} + + +/// A child process that raises an exception. +/// +/// The fork() wrappers are supposed to capture this condition and terminate the +/// child before the code returns to the fork() call point. +/// +/// \tparam Type The type of the exception to raise. +/// \tparam Value The value passed to the constructor of the exception type. In +/// general, this only makes sense if Type is a primitive type so that, in +/// the end, the code becomes "throw int(123)". +/// +/// \throw Type An exception of the provided type. +template< class Type, Type Value > +void +child_raise_exception(void) +{ + throw Type(Value); +} + + +/// Calculates the path to the test helpers binary. +/// +/// \param tc A pointer to the caller test case, needed to extract the value of +/// the "srcdir" property. +/// +/// \return The path to the helpers binary. +static fs::path +get_helpers(const atf::tests::tc* tc) +{ + return fs::path(tc->get_config_var("srcdir")) / "helpers"; +} + + +/// Mock fork(2) that just returns an error. +/// +/// \tparam Errno The value to set as the errno of the failed call. +/// +/// \return Always -1. +template< int Errno > +static pid_t +fork_fail(void) throw() +{ + errno = Errno; + return -1; +} + + +/// Mock open(2) that fails if the 'raise-error' file is opened. +/// +/// \tparam Errno The value to set as the errno if the known failure triggers. +/// \param path The path to the file to be opened. +/// \param flags The open flags. +/// \param ... The file mode creation, if flags contains O_CREAT. +/// +/// \return The opened file handle or -1 on error. +template< int Errno > +static int +open_fail(const char* path, const int flags, ...) throw() +{ + if (std::strcmp(path, "raise-error") == 0) { + errno = Errno; + return -1; + } else { + va_list ap; + va_start(ap, flags); + const int mode = va_arg(ap, int); + va_end(ap); + return ::open(path, flags, mode); + } +} + + +/// Mock pipe(2) that just returns an error. +/// +/// \tparam Errno The value to set as the errno of the failed call. +/// +/// \return Always -1. +template< int Errno > +static pid_t +pipe_fail(int* /* fildes */) throw() +{ + errno = Errno; + return -1; +} + + +/// Helper for child tests to validate inheritance of stdout/stderr. +/// +/// This function ensures that passing one of /dev/stdout or /dev/stderr to +/// the child__fork_files fork method does the right thing. The idea is that we +/// call fork with the given parameters and then make our child redirect one of +/// its file descriptors to a specific file without going through the process +/// library. We then validate if this redirection worked and got the expected +/// output. +/// +/// \param fork_stdout The path to pass to the fork call as the stdout file. +/// \param fork_stderr The path to pass to the fork call as the stderr file. +/// \param child_file The file to explicitly in the subchild. +/// \param child_fd The file descriptor to which to attach child_file. +static void +do_inherit_test(const char* fork_stdout, const char* fork_stderr, + const char* child_file, const int child_fd) +{ + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + logging::set_inmemory(); + + const int fd = ::open(child_file, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (fd != child_fd) { + if (::dup2(fd, child_fd) == -1) + std::abort(); + ::close(fd); + } + + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_function< 123, 'Z' >, + fs::path(fork_stdout), fs::path(fork_stderr)); + const process::status status = child->wait(); + if (!status.exited() || status.exitstatus() != 123) + std::abort(); + std::exit(EXIT_SUCCESS); + } else { + int status; + ATF_REQUIRE(::waitpid(pid, &status, 0) != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + ATF_REQUIRE(atf::utils::grep_file("stdout: Z", "stdout.txt")); + ATF_REQUIRE(atf::utils::grep_file("stderr: Z", "stderr.txt")); + } +} + + +/// Performs a "child__fork_capture__ok_*" test. +/// +/// This test basically ensures that the child__fork_capture class spawns a +/// process whose output is captured in an input stream. +/// +/// \tparam Hook The type of the fork hook to use. +/// \param hook The hook to the fork call. +template< class Hook > +static void +child__fork_capture__ok(Hook hook) +{ + std::cout << "This unflushed message should not propagate to the child"; + std::cerr << "This unflushed message should not propagate to the child"; + std::auto_ptr< process::child > child = process::child::fork_capture(hook); + std::cout.flush(); + std::cerr.flush(); + + std::istream& output = child->output(); + for (std::size_t i = 0; i < 100; i++) { + std::string line; + ATF_REQUIRE(std::getline(output, line).good()); + ATF_REQUIRE_EQ((F("This is a message to stdout, " + "sequence %s") % i).str(), line); + } + + std::string line; + ATF_REQUIRE(std::getline(output, line).good()); + ATF_REQUIRE_EQ("Exiting", line); + + process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__ok_function); +ATF_TEST_CASE_BODY(child__fork_capture__ok_function) +{ + child__fork_capture__ok(child_printer_function); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__ok_functor); +ATF_TEST_CASE_BODY(child__fork_capture__ok_functor) +{ + child__fork_capture__ok(child_printer_functor()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__catch_exceptions); +ATF_TEST_CASE_BODY(child__fork_capture__catch_exceptions) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_throw_exception); + + std::string message; + std::istream& output = child->output(); + ATF_REQUIRE(std::getline(output, message).good()); + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + + ATF_REQUIRE_MATCH("Caught.*A loose exception", message); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__new_session); +ATF_TEST_CASE_BODY(child__fork_capture__new_session) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_check_own_session); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__pipe_fail); +ATF_TEST_CASE_BODY(child__fork_capture__pipe_fail) +{ + process::detail::syscall_pipe = pipe_fail< 23 >; + try { + process::child::fork_capture(child_simple_function< 1, 'A' >); + fail("Expected exception but none raised"); + } catch (const process::system_error& e) { + ATF_REQUIRE(atf::utils::grep_string("pipe.*failed", e.what())); + ATF_REQUIRE_EQ(23, e.original_errno()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__fork_cannot_exit); +ATF_TEST_CASE_BODY(child__fork_capture__fork_cannot_exit) +{ + const pid_t parent_pid = ::getpid(); + atf::utils::create_file("to-not-be-deleted", ""); + + std::auto_ptr< process::child > child = process::child::fork_capture( + child_return); + if (::getpid() != parent_pid) { + // If we enter this clause, it is because the hook returned. + ::unlink("to-not-be-deleted"); + std::exit(EXIT_SUCCESS); + } + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__fork_cannot_unwind); +ATF_TEST_CASE_BODY(child__fork_capture__fork_cannot_unwind) +{ + const pid_t parent_pid = ::getpid(); + atf::utils::create_file("to-not-be-deleted", ""); + try { + std::auto_ptr< process::child > child = process::child::fork_capture( + child_raise_exception< int, 123 >); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted"))); + } catch (const int i) { + // If we enter this clause, it is because an exception leaked from the + // hook. + INV(parent_pid != ::getpid()); + INV(i == 123); + ::unlink("to-not-be-deleted"); + std::exit(EXIT_SUCCESS); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_capture__fork_fail); +ATF_TEST_CASE_BODY(child__fork_capture__fork_fail) +{ + process::detail::syscall_fork = fork_fail< 89 >; + try { + process::child::fork_capture(child_simple_function< 1, 'A' >); + fail("Expected exception but none raised"); + } catch (const process::system_error& e) { + ATF_REQUIRE(atf::utils::grep_string("fork.*failed", e.what())); + ATF_REQUIRE_EQ(89, e.original_errno()); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__ok_function); +ATF_TEST_CASE_BODY(child__fork_files__ok_function) +{ + const fs::path file1("file1.txt"); + const fs::path file2("file2.txt"); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_function< 15, 'Z' >, file1, file2); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(15, status.exitstatus()); + + ATF_REQUIRE( atf::utils::grep_file("^To stdout: Z$", file1.str())); + ATF_REQUIRE(!atf::utils::grep_file("^To stdout: Z$", file2.str())); + + ATF_REQUIRE( atf::utils::grep_file("^To stderr: Z$", file2.str())); + ATF_REQUIRE(!atf::utils::grep_file("^To stderr: Z$", file1.str())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__ok_functor); +ATF_TEST_CASE_BODY(child__fork_files__ok_functor) +{ + const fs::path filea("fileA.txt"); + const fs::path fileb("fileB.txt"); + + atf::utils::create_file(filea.str(), "Initial stdout\n"); + atf::utils::create_file(fileb.str(), "Initial stderr\n"); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_functor(16, "a functor"), filea, fileb); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(16, status.exitstatus()); + + ATF_REQUIRE( atf::utils::grep_file("^Initial stdout$", filea.str())); + ATF_REQUIRE(!atf::utils::grep_file("^Initial stdout$", fileb.str())); + + ATF_REQUIRE( atf::utils::grep_file("^To stdout: a functor$", filea.str())); + ATF_REQUIRE(!atf::utils::grep_file("^To stdout: a functor$", fileb.str())); + + ATF_REQUIRE( atf::utils::grep_file("^Initial stderr$", fileb.str())); + ATF_REQUIRE(!atf::utils::grep_file("^Initial stderr$", filea.str())); + + ATF_REQUIRE( atf::utils::grep_file("^To stderr: a functor$", fileb.str())); + ATF_REQUIRE(!atf::utils::grep_file("^To stderr: a functor$", filea.str())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__catch_exceptions); +ATF_TEST_CASE_BODY(child__fork_files__catch_exceptions) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + child_throw_exception, + fs::path("unused.out"), fs::path("stderr")); + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + + ATF_REQUIRE(atf::utils::grep_file("Caught.*A loose exception", "stderr")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__new_session); +ATF_TEST_CASE_BODY(child__fork_files__new_session) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + child_check_own_session, + fs::path("unused.out"), fs::path("unused.err")); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__inherit_stdout); +ATF_TEST_CASE_BODY(child__fork_files__inherit_stdout) +{ + do_inherit_test("/dev/stdout", "stderr.txt", "stdout.txt", STDOUT_FILENO); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__inherit_stderr); +ATF_TEST_CASE_BODY(child__fork_files__inherit_stderr) +{ + do_inherit_test("stdout.txt", "/dev/stderr", "stderr.txt", STDERR_FILENO); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__fork_cannot_exit); +ATF_TEST_CASE_BODY(child__fork_files__fork_cannot_exit) +{ + const pid_t parent_pid = ::getpid(); + atf::utils::create_file("to-not-be-deleted", ""); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_return, fs::path("out"), fs::path("err")); + if (::getpid() != parent_pid) { + // If we enter this clause, it is because the hook returned. + ::unlink("to-not-be-deleted"); + std::exit(EXIT_SUCCESS); + } + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__fork_cannot_unwind); +ATF_TEST_CASE_BODY(child__fork_files__fork_cannot_unwind) +{ + const pid_t parent_pid = ::getpid(); + atf::utils::create_file("to-not-be-deleted", ""); + try { + std::auto_ptr< process::child > child = process::child::fork_files( + child_raise_exception< int, 123 >, fs::path("out"), + fs::path("err")); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(fs::exists(fs::path("to-not-be-deleted"))); + } catch (const int i) { + // If we enter this clause, it is because an exception leaked from the + // hook. + INV(parent_pid != ::getpid()); + INV(i == 123); + ::unlink("to-not-be-deleted"); + std::exit(EXIT_SUCCESS); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__fork_fail); +ATF_TEST_CASE_BODY(child__fork_files__fork_fail) +{ + process::detail::syscall_fork = fork_fail< 1234 >; + try { + process::child::fork_files(child_simple_function< 1, 'A' >, + fs::path("a.txt"), fs::path("b.txt")); + fail("Expected exception but none raised"); + } catch (const process::system_error& e) { + ATF_REQUIRE(atf::utils::grep_string("fork.*failed", e.what())); + ATF_REQUIRE_EQ(1234, e.original_errno()); + } + ATF_REQUIRE(!fs::exists(fs::path("a.txt"))); + ATF_REQUIRE(!fs::exists(fs::path("b.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__create_stdout_fail); +ATF_TEST_CASE_BODY(child__fork_files__create_stdout_fail) +{ + process::detail::syscall_open = open_fail< ENOENT >; + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_function< 1, 'A' >, fs::path("raise-error"), + fs::path("created")); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(!fs::exists(fs::path("raise-error"))); + ATF_REQUIRE(!fs::exists(fs::path("created"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__fork_files__create_stderr_fail); +ATF_TEST_CASE_BODY(child__fork_files__create_stderr_fail) +{ + process::detail::syscall_open = open_fail< ENOENT >; + std::auto_ptr< process::child > child = process::child::fork_files( + child_simple_function< 1, 'A' >, fs::path("created"), + fs::path("raise-error")); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(fs::exists(fs::path("created"))); + ATF_REQUIRE(!fs::exists(fs::path("raise-error"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__absolute_path); +ATF_TEST_CASE_BODY(child__spawn__absolute_path) +{ + std::vector< std::string > args; + args.push_back("return-code"); + args.push_back("12"); + + const fs::path program = get_helpers(this); + INV(program.is_absolute()); + std::auto_ptr< process::child > child = process::child::spawn_files( + program, args, fs::path("out"), fs::path("err")); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(12, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__relative_path); +ATF_TEST_CASE_BODY(child__spawn__relative_path) +{ + std::vector< std::string > args; + args.push_back("return-code"); + args.push_back("13"); + + ATF_REQUIRE(::mkdir("root", 0755) != -1); + ATF_REQUIRE(::symlink(get_helpers(this).c_str(), "root/helpers") != -1); + + std::auto_ptr< process::child > child = process::child::spawn_files( + fs::path("root/helpers"), args, fs::path("out"), fs::path("err")); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(13, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__basename_only); +ATF_TEST_CASE_BODY(child__spawn__basename_only) +{ + std::vector< std::string > args; + args.push_back("return-code"); + args.push_back("14"); + + ATF_REQUIRE(::symlink(get_helpers(this).c_str(), "helpers") != -1); + + std::auto_ptr< process::child > child = process::child::spawn_files( + fs::path("helpers"), args, fs::path("out"), fs::path("err")); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(14, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__no_path); +ATF_TEST_CASE_BODY(child__spawn__no_path) +{ + logging::set_inmemory(); + + std::vector< std::string > args; + args.push_back("return-code"); + args.push_back("14"); + + const fs::path helpers = get_helpers(this); + utils::setenv("PATH", helpers.branch_path().c_str()); + std::auto_ptr< process::child > child = process::child::spawn_capture( + fs::path(helpers.leaf_name()), args); + + std::string line; + ATF_REQUIRE(std::getline(child->output(), line).good()); + ATF_REQUIRE_MATCH("Failed to execute", line); + ATF_REQUIRE(!std::getline(child->output(), line)); + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__no_args); +ATF_TEST_CASE_BODY(child__spawn__no_args) +{ + std::vector< std::string > args; + std::auto_ptr< process::child > child = process::child::spawn_capture( + get_helpers(this), args); + + std::string line; + ATF_REQUIRE(std::getline(child->output(), line).good()); + ATF_REQUIRE_EQ("Must provide a helper name", line); + ATF_REQUIRE(!std::getline(child->output(), line)); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_FAILURE, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__some_args); +ATF_TEST_CASE_BODY(child__spawn__some_args) +{ + std::vector< std::string > args; + args.push_back("print-args"); + args.push_back("foo"); + args.push_back(" bar baz "); + std::auto_ptr< process::child > child = process::child::spawn_capture( + get_helpers(this), args); + + std::string line; + ATF_REQUIRE(std::getline(child->output(), line).good()); + ATF_REQUIRE_EQ("argv[0] = " + get_helpers(this).str(), line); + ATF_REQUIRE(std::getline(child->output(), line).good()); + ATF_REQUIRE_EQ("argv[1] = print-args", line); + ATF_REQUIRE(std::getline(child->output(), line)); + ATF_REQUIRE_EQ("argv[2] = foo", line); + ATF_REQUIRE(std::getline(child->output(), line)); + ATF_REQUIRE_EQ("argv[3] = bar baz ", line); + ATF_REQUIRE(std::getline(child->output(), line)); + ATF_REQUIRE_EQ("argv[4] = NULL", line); + ATF_REQUIRE(!std::getline(child->output(), line)); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__spawn__missing_program); +ATF_TEST_CASE_BODY(child__spawn__missing_program) +{ + std::vector< std::string > args; + std::auto_ptr< process::child > child = process::child::spawn_capture( + fs::path("a/b/c"), args); + + std::string line; + ATF_REQUIRE(std::getline(child->output(), line).good()); + const std::string exp = "Failed to execute a/b/c: "; + ATF_REQUIRE_EQ(exp, line.substr(0, exp.length())); + ATF_REQUIRE(!std::getline(child->output(), line)); + + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(child__pid); +ATF_TEST_CASE_BODY(child__pid) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_write_pid); + + const int pid = child->pid(); + + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); + + std::ifstream input("pidfile"); + ATF_REQUIRE(input); + int read_pid; + input >> read_pid; + input.close(); + + ATF_REQUIRE_EQ(read_pid, pid); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + utils::avoid_coredump_on_crash(); + + ATF_ADD_TEST_CASE(tcs, child__fork_capture__ok_function); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__ok_functor); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__catch_exceptions); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__new_session); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__pipe_fail); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__fork_cannot_exit); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__fork_cannot_unwind); + ATF_ADD_TEST_CASE(tcs, child__fork_capture__fork_fail); + + ATF_ADD_TEST_CASE(tcs, child__fork_files__ok_function); + ATF_ADD_TEST_CASE(tcs, child__fork_files__ok_functor); + ATF_ADD_TEST_CASE(tcs, child__fork_files__catch_exceptions); + ATF_ADD_TEST_CASE(tcs, child__fork_files__new_session); + ATF_ADD_TEST_CASE(tcs, child__fork_files__inherit_stdout); + ATF_ADD_TEST_CASE(tcs, child__fork_files__inherit_stderr); + ATF_ADD_TEST_CASE(tcs, child__fork_files__fork_cannot_exit); + ATF_ADD_TEST_CASE(tcs, child__fork_files__fork_cannot_unwind); + ATF_ADD_TEST_CASE(tcs, child__fork_files__fork_fail); + ATF_ADD_TEST_CASE(tcs, child__fork_files__create_stdout_fail); + ATF_ADD_TEST_CASE(tcs, child__fork_files__create_stderr_fail); + + ATF_ADD_TEST_CASE(tcs, child__spawn__absolute_path); + ATF_ADD_TEST_CASE(tcs, child__spawn__relative_path); + ATF_ADD_TEST_CASE(tcs, child__spawn__basename_only); + ATF_ADD_TEST_CASE(tcs, child__spawn__no_path); + ATF_ADD_TEST_CASE(tcs, child__spawn__no_args); + ATF_ADD_TEST_CASE(tcs, child__spawn__some_args); + ATF_ADD_TEST_CASE(tcs, child__spawn__missing_program); + + ATF_ADD_TEST_CASE(tcs, child__pid); +} diff --git a/utils/process/deadline_killer.cpp b/utils/process/deadline_killer.cpp new file mode 100644 index 000000000000..ed733e402f76 --- /dev/null +++ b/utils/process/deadline_killer.cpp @@ -0,0 +1,54 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/deadline_killer.hpp" + +#include "utils/datetime.hpp" +#include "utils/process/operations.hpp" + +namespace datetime = utils::datetime; +namespace process = utils::process; + + +/// Constructor. +/// +/// \param delta Time to the timer activation. +/// \param pid PID of the process (and process group) to kill. +process::deadline_killer::deadline_killer(const datetime::delta& delta, + const int pid) : + signals::timer(delta), _pid(pid) +{ +} + + +/// Timer activation callback. +void +process::deadline_killer::callback(void) +{ + process::terminate_group(_pid); +} diff --git a/utils/process/deadline_killer.hpp b/utils/process/deadline_killer.hpp new file mode 100644 index 000000000000..8b337a0f9d8c --- /dev/null +++ b/utils/process/deadline_killer.hpp @@ -0,0 +1,58 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/deadline_killer.hpp +/// Timer to kill a process on activation. + +#if !defined(UTILS_PROCESS_DEADLINE_KILLER_HPP) +#define UTILS_PROCESS_DEADLINE_KILLER_HPP + +#include "utils/process/deadline_killer_fwd.hpp" + +#include "utils/signals/timer.hpp" + +namespace utils { +namespace process { + + +/// Timer that forcibly kills a process group on activation. +class deadline_killer : public utils::signals::timer { + /// PID of the process (and process group) to kill. + const int _pid; + + void callback(void); + +public: + deadline_killer(const datetime::delta&, const int); +}; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_DEADLINE_KILLER_HPP) diff --git a/utils/process/deadline_killer_fwd.hpp b/utils/process/deadline_killer_fwd.hpp new file mode 100644 index 000000000000..fca3c5dc57c7 --- /dev/null +++ b/utils/process/deadline_killer_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/deadline_killer_fwd.hpp +/// Forward declarations for utils/process/deadline_killer.hpp + +#if !defined(UTILS_PROCESS_DEADLINE_KILLER_FWD_HPP) +#define UTILS_PROCESS_DEADLINE_KILLER_FWD_HPP + +namespace utils { +namespace process { + + +class deadline_killer; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_DEADLINE_KILLER_FWD_HPP) diff --git a/utils/process/deadline_killer_test.cpp b/utils/process/deadline_killer_test.cpp new file mode 100644 index 000000000000..06c89660ac31 --- /dev/null +++ b/utils/process/deadline_killer_test.cpp @@ -0,0 +1,108 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/deadline_killer.hpp" + +extern "C" { +#include +#include +} + +#include + +#include + +#include "utils/datetime.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" + +namespace datetime = utils::datetime; +namespace process = utils::process; + + +namespace { + + +/// Body of a child process that sleeps and then exits. +/// +/// \tparam Seconds The delay the subprocess has to sleep for. +template< int Seconds > +static void +child_sleep(void) +{ + ::sleep(Seconds); + std::exit(EXIT_SUCCESS); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(activation); +ATF_TEST_CASE_BODY(activation) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_sleep< 60 >); + + datetime::timestamp start = datetime::timestamp::now(); + process::deadline_killer killer(datetime::delta(1, 0), child->pid()); + const process::status status = child->wait(); + killer.unprogram(); + datetime::timestamp end = datetime::timestamp::now(); + + ATF_REQUIRE(killer.fired()); + ATF_REQUIRE(end - start <= datetime::delta(10, 0)); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGKILL, status.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(no_activation); +ATF_TEST_CASE_BODY(no_activation) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_sleep< 1 >); + + datetime::timestamp start = datetime::timestamp::now(); + process::deadline_killer killer(datetime::delta(60, 0), child->pid()); + const process::status status = child->wait(); + killer.unprogram(); + datetime::timestamp end = datetime::timestamp::now(); + + ATF_REQUIRE(!killer.fired()); + ATF_REQUIRE(end - start <= datetime::delta(10, 0)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, activation); + ATF_ADD_TEST_CASE(tcs, no_activation); +} diff --git a/utils/process/exceptions.cpp b/utils/process/exceptions.cpp new file mode 100644 index 000000000000..d7590c330499 --- /dev/null +++ b/utils/process/exceptions.cpp @@ -0,0 +1,91 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/exceptions.hpp" + +#include + +#include "utils/format/macros.hpp" + +namespace process = utils::process; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +process::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +process::error::~error(void) throw() +{ +} + + +/// Constructs a new error based on an errno code. +/// +/// \param message_ The message describing what caused the error. +/// \param errno_ The error code. +process::system_error::system_error(const std::string& message_, + const int errno_) : + error(F("%s: %s") % message_ % strerror(errno_)), + _original_errno(errno_) +{ +} + + +/// Destructor for the error. +process::system_error::~system_error(void) throw() +{ +} + + +/// \return The original errno value. +int +process::system_error::original_errno(void) const throw() +{ + return _original_errno; +} + + +/// Constructs a new timeout_error. +/// +/// \param message_ The message describing what caused the error. +process::timeout_error::timeout_error(const std::string& message_) : + error(message_) +{ +} + + +/// Destructor for the error. +process::timeout_error::~timeout_error(void) throw() +{ +} diff --git a/utils/process/exceptions.hpp b/utils/process/exceptions.hpp new file mode 100644 index 000000000000..3bf740459864 --- /dev/null +++ b/utils/process/exceptions.hpp @@ -0,0 +1,78 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/exceptions.hpp +/// Exception types raised by the process module. + +#if !defined(UTILS_PROCESS_EXCEPTIONS_HPP) +#define UTILS_PROCESS_EXCEPTIONS_HPP + +#include + +namespace utils { +namespace process { + + +/// Base exceptions for process errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Exceptions for errno-based errors. +/// +/// TODO(jmmv): This code is duplicated in, at least, utils::fs. Figure +/// out a way to reuse this exception while maintaining the correct inheritance +/// (i.e. be able to keep it as a child of process::error). +class system_error : public error { + /// Error number describing this libc error condition. + int _original_errno; + +public: + explicit system_error(const std::string&, const int); + ~system_error(void) throw(); + + int original_errno(void) const throw(); +}; + + +/// Denotes that a deadline was exceeded. +class timeout_error : public error { +public: + explicit timeout_error(const std::string&); + ~timeout_error(void) throw(); +}; + + +} // namespace process +} // namespace utils + + +#endif // !defined(UTILS_PROCESS_EXCEPTIONS_HPP) diff --git a/utils/process/exceptions_test.cpp b/utils/process/exceptions_test.cpp new file mode 100644 index 000000000000..375b635fc173 --- /dev/null +++ b/utils/process/exceptions_test.cpp @@ -0,0 +1,63 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/exceptions.hpp" + +#include +#include + +#include + +#include "utils/format/macros.hpp" + +namespace process = utils::process; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const process::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(system_error); +ATF_TEST_CASE_BODY(system_error) +{ + const process::system_error e("Call failed", ENOENT); + const std::string expected = F("Call failed: %s") % std::strerror(ENOENT); + ATF_REQUIRE_EQ(expected, e.what()); + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, system_error); +} diff --git a/utils/process/executor.cpp b/utils/process/executor.cpp new file mode 100644 index 000000000000..dbdf31268f86 --- /dev/null +++ b/utils/process/executor.cpp @@ -0,0 +1,869 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/executor.ipp" + +#if defined(HAVE_CONFIG_H) +#include "config.h" +#endif + +extern "C" { +#include +#include + +#include +} + +#include +#include +#include +#include + +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/auto_cleaners.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/logging/operations.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/child.ipp" +#include "utils/process/deadline_killer.hpp" +#include "utils/process/isolation.hpp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/interrupts.hpp" +#include "utils/signals/timer.hpp" + +namespace datetime = utils::datetime; +namespace executor = utils::process::executor; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace passwd = utils::passwd; +namespace process = utils::process; +namespace signals = utils::signals; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Template for temporary directories created by the executor. +static const char* work_directory_template = PACKAGE_TARNAME ".XXXXXX"; + + +/// Mapping of active subprocess PIDs to their execution data. +typedef std::map< int, executor::exec_handle > exec_handles_map; + + +} // anonymous namespace + + +/// Basename of the file containing the stdout of the subprocess. +const char* utils::process::executor::detail::stdout_name = "stdout.txt"; + + +/// Basename of the file containing the stderr of the subprocess. +const char* utils::process::executor::detail::stderr_name = "stderr.txt"; + + +/// Basename of the subdirectory in which the subprocess is actually executed. +/// +/// This is a subdirectory of the "unique work directory" generated for the +/// subprocess so that our code can create control files on disk and not +/// get them clobbered by the subprocess's activity. +const char* utils::process::executor::detail::work_subdir = "work"; + + +/// Prepares a subprocess to run a user-provided hook in a controlled manner. +/// +/// \param unprivileged_user User to switch to if not none. +/// \param control_directory Path to the subprocess-specific control directory. +/// \param work_directory Path to the subprocess-specific work directory. +void +utils::process::executor::detail::setup_child( + const optional< passwd::user > unprivileged_user, + const fs::path& control_directory, + const fs::path& work_directory) +{ + logging::set_inmemory(); + process::isolate_path(unprivileged_user, control_directory); + process::isolate_child(unprivileged_user, work_directory); +} + + +/// Internal implementation for the exit_handle class. +struct utils::process::executor::exec_handle::impl : utils::noncopyable { + /// PID of the process being run. + int pid; + + /// Path to the subprocess-specific work directory. + fs::path control_directory; + + /// Path to the subprocess's stdout file. + const fs::path stdout_file; + + /// Path to the subprocess's stderr file. + const fs::path stderr_file; + + /// Start time. + datetime::timestamp start_time; + + /// User the subprocess is running as if different than the current one. + const optional< passwd::user > unprivileged_user; + + /// Timer to kill the subprocess on activation. + process::deadline_killer timer; + + /// Number of owners of the on-disk state. + executor::detail::refcnt_t state_owners; + + /// Constructor. + /// + /// \param pid_ PID of the forked process. + /// \param control_directory_ Path to the subprocess-specific work + /// directory. + /// \param stdout_file_ Path to the subprocess's stdout file. + /// \param stderr_file_ Path to the subprocess's stderr file. + /// \param start_time_ Timestamp of when this object was constructed. + /// \param timeout Maximum amount of time the subprocess can run for. + /// \param unprivileged_user_ User the subprocess is running as if + /// different than the current one. + /// \param [in,out] state_owners_ Number of owners of the on-disk state. + /// For first-time processes, this should be a new counter set to 0; + /// for followup processes, this should point to the same counter used + /// by the preceding process. + impl(const int pid_, + const fs::path& control_directory_, + const fs::path& stdout_file_, + const fs::path& stderr_file_, + const datetime::timestamp& start_time_, + const datetime::delta& timeout, + const optional< passwd::user > unprivileged_user_, + executor::detail::refcnt_t state_owners_) : + pid(pid_), + control_directory(control_directory_), + stdout_file(stdout_file_), + stderr_file(stderr_file_), + start_time(start_time_), + unprivileged_user(unprivileged_user_), + timer(timeout, pid_), + state_owners(state_owners_) + { + (*state_owners)++; + POST(*state_owners > 0); + } +}; + + +/// Constructor. +/// +/// \param pimpl Constructed internal implementation. +executor::exec_handle::exec_handle(std::shared_ptr< impl > pimpl) : + _pimpl(pimpl) +{ +} + + +/// Destructor. +executor::exec_handle::~exec_handle(void) +{ +} + + +/// Returns the PID of the process being run. +/// +/// \return A PID. +int +executor::exec_handle::pid(void) const +{ + return _pimpl->pid; +} + + +/// Returns the path to the subprocess-specific control directory. +/// +/// This is where the executor may store control files. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +executor::exec_handle::control_directory(void) const +{ + return _pimpl->control_directory; +} + + +/// Returns the path to the subprocess-specific work directory. +/// +/// This is guaranteed to be clear of files created by the executor. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +executor::exec_handle::work_directory(void) const +{ + return _pimpl->control_directory / detail::work_subdir; +} + + +/// Returns the path to the subprocess's stdout file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +executor::exec_handle::stdout_file(void) const +{ + return _pimpl->stdout_file; +} + + +/// Returns the path to the subprocess's stderr file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +executor::exec_handle::stderr_file(void) const +{ + return _pimpl->stderr_file; +} + + +/// Internal implementation for the exit_handle class. +struct utils::process::executor::exit_handle::impl : utils::noncopyable { + /// Original PID of the terminated subprocess. + /// + /// Note that this PID is no longer valid and cannot be used on system + /// tables! + const int original_pid; + + /// Termination status of the subprocess, or none if it timed out. + const optional< process::status > status; + + /// The user the process ran as, if different than the current one. + const optional< passwd::user > unprivileged_user; + + /// Timestamp of when the subprocess was spawned. + const datetime::timestamp start_time; + + /// Timestamp of when wait() or wait_any() returned this object. + const datetime::timestamp end_time; + + /// Path to the subprocess-specific work directory. + const fs::path control_directory; + + /// Path to the subprocess's stdout file. + const fs::path stdout_file; + + /// Path to the subprocess's stderr file. + const fs::path stderr_file; + + /// Number of owners of the on-disk state. + /// + /// This will be 1 if this exit_handle is the last holder of the on-disk + /// state, in which case cleanup() invocations will wipe the disk state. + /// For all other cases, this will hold a higher value. + detail::refcnt_t state_owners; + + /// Mutable pointer to the corresponding executor state. + /// + /// This object references a member of the executor_handle that yielded this + /// exit_handle instance. We need this direct access to clean up after + /// ourselves when the handle is destroyed. + exec_handles_map& all_exec_handles; + + /// Whether the subprocess state has been cleaned yet or not. + /// + /// Used to keep track of explicit calls to the public cleanup(). + bool cleaned; + + /// Constructor. + /// + /// \param original_pid_ Original PID of the terminated subprocess. + /// \param status_ Termination status of the subprocess, or none if + /// timed out. + /// \param unprivileged_user_ The user the process ran as, if different than + /// the current one. + /// \param start_time_ Timestamp of when the subprocess was spawned. + /// \param end_time_ Timestamp of when wait() or wait_any() returned this + /// object. + /// \param control_directory_ Path to the subprocess-specific work + /// directory. + /// \param stdout_file_ Path to the subprocess's stdout file. + /// \param stderr_file_ Path to the subprocess's stderr file. + /// \param [in,out] state_owners_ Number of owners of the on-disk state. + /// \param [in,out] all_exec_handles_ Global object keeping track of all + /// active executions for an executor. This is a pointer to a member of + /// the executor_handle object. + impl(const int original_pid_, + const optional< process::status > status_, + const optional< passwd::user > unprivileged_user_, + const datetime::timestamp& start_time_, + const datetime::timestamp& end_time_, + const fs::path& control_directory_, + const fs::path& stdout_file_, + const fs::path& stderr_file_, + detail::refcnt_t state_owners_, + exec_handles_map& all_exec_handles_) : + original_pid(original_pid_), status(status_), + unprivileged_user(unprivileged_user_), + start_time(start_time_), end_time(end_time_), + control_directory(control_directory_), + stdout_file(stdout_file_), stderr_file(stderr_file_), + state_owners(state_owners_), + all_exec_handles(all_exec_handles_), cleaned(false) + { + } + + /// Destructor. + ~impl(void) + { + if (!cleaned) { + LW(F("Implicitly cleaning up exit_handle for exec_handle %s; " + "ignoring errors!") % original_pid); + try { + cleanup(); + } catch (const std::runtime_error& error) { + LE(F("Subprocess cleanup failed: %s") % error.what()); + } + } + } + + /// Cleans up the subprocess on-disk state. + /// + /// \throw engine::error If the cleanup fails, especially due to the + /// inability to remove the work directory. + void + cleanup(void) + { + PRE(*state_owners > 0); + if (*state_owners == 1) { + LI(F("Cleaning up exit_handle for exec_handle %s") % original_pid); + fs::rm_r(control_directory); + } else { + LI(F("Not cleaning up exit_handle for exec_handle %s; " + "%s owners left") % original_pid % (*state_owners - 1)); + } + // We must decrease our reference only after we have successfully + // cleaned up the control directory. Otherwise, the rm_r call would + // throw an exception, which would in turn invoke the implicit cleanup + // from the destructor, which would make us crash due to an invalid + // reference count. + (*state_owners)--; + // Marking this object as clean here, even if we did not do actually the + // cleaning above, is fine (albeit a bit confusing). Note that "another + // owner" refers to a handle for a different PID, so that handle will be + // the one issuing the cleanup. + all_exec_handles.erase(original_pid); + cleaned = true; + } +}; + + +/// Constructor. +/// +/// \param pimpl Constructed internal implementation. +executor::exit_handle::exit_handle(std::shared_ptr< impl > pimpl) : + _pimpl(pimpl) +{ +} + + +/// Destructor. +executor::exit_handle::~exit_handle(void) +{ +} + + +/// Cleans up the subprocess status. +/// +/// This function should be called explicitly as it provides the means to +/// control any exceptions raised during cleanup. Do not rely on the destructor +/// to clean things up. +/// +/// \throw engine::error If the cleanup fails, especially due to the inability +/// to remove the work directory. +void +executor::exit_handle::cleanup(void) +{ + PRE(!_pimpl->cleaned); + _pimpl->cleanup(); + POST(_pimpl->cleaned); +} + + +/// Gets the current number of owners of the on-disk data. +/// +/// \return A shared reference counter. Even though this function is marked as +/// const, the return value is intentionally mutable because we need to update +/// reference counts from different but related processes. This is why this +/// method is not public. +std::shared_ptr< std::size_t > +executor::exit_handle::state_owners(void) const +{ + return _pimpl->state_owners; +} + + +/// Returns the original PID corresponding to the terminated subprocess. +/// +/// \return An exec_handle. +int +executor::exit_handle::original_pid(void) const +{ + return _pimpl->original_pid; +} + + +/// Returns the process termination status of the subprocess. +/// +/// \return A process termination status, or none if the subprocess timed out. +const optional< process::status >& +executor::exit_handle::status(void) const +{ + return _pimpl->status; +} + + +/// Returns the user the process ran as if different than the current one. +/// +/// \return None if the credentials of the process were the same as the current +/// one, or else a user. +const optional< passwd::user >& +executor::exit_handle::unprivileged_user(void) const +{ + return _pimpl->unprivileged_user; +} + + +/// Returns the timestamp of when the subprocess was spawned. +/// +/// \return A timestamp. +const datetime::timestamp& +executor::exit_handle::start_time(void) const +{ + return _pimpl->start_time; +} + + +/// Returns the timestamp of when wait() or wait_any() returned this object. +/// +/// \return A timestamp. +const datetime::timestamp& +executor::exit_handle::end_time(void) const +{ + return _pimpl->end_time; +} + + +/// Returns the path to the subprocess-specific control directory. +/// +/// This is where the executor may store control files. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +executor::exit_handle::control_directory(void) const +{ + return _pimpl->control_directory; +} + + +/// Returns the path to the subprocess-specific work directory. +/// +/// This is guaranteed to be clear of files created by the executor. +/// +/// \return The path to a directory that exists until cleanup() is called. +fs::path +executor::exit_handle::work_directory(void) const +{ + return _pimpl->control_directory / detail::work_subdir; +} + + +/// Returns the path to the subprocess's stdout file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +executor::exit_handle::stdout_file(void) const +{ + return _pimpl->stdout_file; +} + + +/// Returns the path to the subprocess's stderr file. +/// +/// \return The path to a file that exists until cleanup() is called. +const fs::path& +executor::exit_handle::stderr_file(void) const +{ + return _pimpl->stderr_file; +} + + +/// Internal implementation for the executor_handle. +/// +/// Because the executor is a singleton, these essentially is a container for +/// global variables. +struct utils::process::executor::executor_handle::impl : utils::noncopyable { + /// Numeric counter of executed subprocesses. + /// + /// This is used to generate a unique identifier for each subprocess as an + /// easy mechanism to discern their unique work directories. + size_t last_subprocess; + + /// Interrupts handler. + std::auto_ptr< signals::interrupts_handler > interrupts_handler; + + /// Root work directory for all executed subprocesses. + std::auto_ptr< fs::auto_directory > root_work_directory; + + /// Mapping of PIDs to the data required at run time. + exec_handles_map all_exec_handles; + + /// Whether the executor state has been cleaned yet or not. + /// + /// Used to keep track of explicit calls to the public cleanup(). + bool cleaned; + + /// Constructor. + impl(void) : + last_subprocess(0), + interrupts_handler(new signals::interrupts_handler()), + root_work_directory(new fs::auto_directory( + fs::auto_directory::mkdtemp_public(work_directory_template))), + cleaned(false) + { + } + + /// Destructor. + ~impl(void) + { + if (!cleaned) { + LW("Implicitly cleaning up executor; ignoring errors!"); + try { + cleanup(); + cleaned = true; + } catch (const std::runtime_error& error) { + LE(F("Executor global cleanup failed: %s") % error.what()); + } + } + } + + /// Cleans up the executor state. + void + cleanup(void) + { + PRE(!cleaned); + + for (exec_handles_map::const_iterator iter = all_exec_handles.begin(); + iter != all_exec_handles.end(); ++iter) { + const int& pid = (*iter).first; + const exec_handle& data = (*iter).second; + + process::terminate_group(pid); + int status; + if (::waitpid(pid, &status, 0) == -1) { + // Should not happen. + LW(F("Failed to wait for PID %s") % pid); + } + + try { + fs::rm_r(data.control_directory()); + } catch (const fs::error& e) { + LE(F("Failed to clean up subprocess work directory %s: %s") % + data.control_directory() % e.what()); + } + } + all_exec_handles.clear(); + + try { + // The following only causes the work directory to be deleted, not + // any of its contents, so we expect this to always succeed. This + // *should* be sufficient because, in the loop above, we have + // individually wiped the subdirectories of any still-unclean + // subprocesses. + root_work_directory->cleanup(); + } catch (const fs::error& e) { + LE(F("Failed to clean up executor work directory %s: %s; this is " + "an internal error") % root_work_directory->directory() + % e.what()); + } + root_work_directory.reset(NULL); + + interrupts_handler->unprogram(); + interrupts_handler.reset(NULL); + } + + /// Common code to run after any of the wait calls. + /// + /// \param original_pid The PID of the terminated subprocess. + /// \param status The exit status of the terminated subprocess. + /// + /// \return A pointer to an object describing the waited-for subprocess. + executor::exit_handle + post_wait(const int original_pid, const process::status& status) + { + PRE(original_pid == status.dead_pid()); + LI(F("Waited for subprocess with exec_handle %s") % original_pid); + + process::terminate_group(status.dead_pid()); + + const exec_handles_map::iterator iter = all_exec_handles.find( + original_pid); + exec_handle& data = (*iter).second; + data._pimpl->timer.unprogram(); + + // It is tempting to assert here (and old code did) that, if the timer + // has fired, the process has been forcibly killed by us. This is not + // always the case though: for short-lived processes and with very short + // timeouts (think 1ms), it is possible for scheduling decisions to + // allow the subprocess to finish while at the same time cause the timer + // to fire. So we do not assert this any longer and just rely on the + // timer expiration to check if the process timed out or not. If the + // process did finish but the timer expired... oh well, we do not detect + // this correctly but we don't care because this should not really + // happen. + + if (!fs::exists(data.stdout_file())) { + std::ofstream new_stdout(data.stdout_file().c_str()); + } + if (!fs::exists(data.stderr_file())) { + std::ofstream new_stderr(data.stderr_file().c_str()); + } + + return exit_handle(std::shared_ptr< exit_handle::impl >( + new exit_handle::impl( + data.pid(), + data._pimpl->timer.fired() ? + none : utils::make_optional(status), + data._pimpl->unprivileged_user, + data._pimpl->start_time, datetime::timestamp::now(), + data.control_directory(), + data.stdout_file(), + data.stderr_file(), + data._pimpl->state_owners, + all_exec_handles))); + } +}; + + +/// Constructor. +executor::executor_handle::executor_handle(void) throw() : _pimpl(new impl()) +{ +} + + +/// Destructor. +executor::executor_handle::~executor_handle(void) +{ +} + + +/// Queries the path to the root of the work directory for all subprocesses. +/// +/// \return A path. +const fs::path& +executor::executor_handle::root_work_directory(void) const +{ + return _pimpl->root_work_directory->directory(); +} + + +/// Cleans up the executor state. +/// +/// This function should be called explicitly as it provides the means to +/// control any exceptions raised during cleanup. Do not rely on the destructor +/// to clean things up. +/// +/// \throw engine::error If there are problems cleaning up the executor. +void +executor::executor_handle::cleanup(void) +{ + PRE(!_pimpl->cleaned); + _pimpl->cleanup(); + _pimpl->cleaned = true; +} + + +/// Initializes the executor. +/// +/// \pre This function can only be called if there is no other executor_handle +/// object alive. +/// +/// \return A handle to the operations of the executor. +executor::executor_handle +executor::setup(void) +{ + return executor_handle(); +} + + +/// Pre-helper for the spawn() method. +/// +/// \return The created control directory for the subprocess. +fs::path +executor::executor_handle::spawn_pre(void) +{ + signals::check_interrupt(); + + ++_pimpl->last_subprocess; + + const fs::path control_directory = + _pimpl->root_work_directory->directory() / + (F("%s") % _pimpl->last_subprocess); + fs::mkdir_p(control_directory / detail::work_subdir, 0755); + + return control_directory; +} + + +/// Post-helper for the spawn() method. +/// +/// \param control_directory Control directory as returned by spawn_pre(). +/// \param stdout_file Path to the subprocess' stdout. +/// \param stderr_file Path to the subprocess' stderr. +/// \param timeout Maximum amount of time the subprocess can run for. +/// \param unprivileged_user If not none, user to switch to before execution. +/// \param child The process created by spawn(). +/// +/// \return The execution handle of the started subprocess. +executor::exec_handle +executor::executor_handle::spawn_post( + const fs::path& control_directory, + const fs::path& stdout_file, + const fs::path& stderr_file, + const datetime::delta& timeout, + const optional< passwd::user > unprivileged_user, + std::auto_ptr< process::child > child) +{ + const exec_handle handle(std::shared_ptr< exec_handle::impl >( + new exec_handle::impl( + child->pid(), + control_directory, + stdout_file, + stderr_file, + datetime::timestamp::now(), + timeout, + unprivileged_user, + detail::refcnt_t(new detail::refcnt_t::element_type(0))))); + INV_MSG(_pimpl->all_exec_handles.find(handle.pid()) == + _pimpl->all_exec_handles.end(), + F("PID %s already in all_exec_handles; not properly cleaned " + "up or reused too fast") % handle.pid());; + _pimpl->all_exec_handles.insert(exec_handles_map::value_type( + handle.pid(), handle)); + LI(F("Spawned subprocess with exec_handle %s") % handle.pid()); + return handle; +} + + +/// Pre-helper for the spawn_followup() method. +void +executor::executor_handle::spawn_followup_pre(void) +{ + signals::check_interrupt(); +} + + +/// Post-helper for the spawn_followup() method. +/// +/// \param base Exit handle of the subprocess to use as context. +/// \param timeout Maximum amount of time the subprocess can run for. +/// \param child The process created by spawn_followup(). +/// +/// \return The execution handle of the started subprocess. +executor::exec_handle +executor::executor_handle::spawn_followup_post( + const exit_handle& base, + const datetime::delta& timeout, + std::auto_ptr< process::child > child) +{ + INV(*base.state_owners() > 0); + const exec_handle handle(std::shared_ptr< exec_handle::impl >( + new exec_handle::impl( + child->pid(), + base.control_directory(), + base.stdout_file(), + base.stderr_file(), + datetime::timestamp::now(), + timeout, + base.unprivileged_user(), + base.state_owners()))); + INV_MSG(_pimpl->all_exec_handles.find(handle.pid()) == + _pimpl->all_exec_handles.end(), + F("PID %s already in all_exec_handles; not properly cleaned " + "up or reused too fast") % handle.pid());; + _pimpl->all_exec_handles.insert(exec_handles_map::value_type( + handle.pid(), handle)); + LI(F("Spawned subprocess with exec_handle %s") % handle.pid()); + return handle; +} + + +/// Waits for completion of any forked process. +/// +/// \param exec_handle The handle of the process to wait for. +/// +/// \return A pointer to an object describing the waited-for subprocess. +executor::exit_handle +executor::executor_handle::wait(const exec_handle exec_handle) +{ + signals::check_interrupt(); + const process::status status = process::wait(exec_handle.pid()); + return _pimpl->post_wait(exec_handle.pid(), status); +} + + +/// Waits for completion of any forked process. +/// +/// \return A pointer to an object describing the waited-for subprocess. +executor::exit_handle +executor::executor_handle::wait_any(void) +{ + signals::check_interrupt(); + const process::status status = process::wait_any(); + return _pimpl->post_wait(status.dead_pid(), status); +} + + +/// Checks if an interrupt has fired. +/// +/// Calls to this function should be sprinkled in strategic places through the +/// code protected by an interrupts_handler object. +/// +/// This is just a wrapper over signals::check_interrupt() to avoid leaking this +/// dependency to the caller. +/// +/// \throw signals::interrupted_error If there has been an interrupt. +void +executor::executor_handle::check_interrupt(void) const +{ + signals::check_interrupt(); +} diff --git a/utils/process/executor.hpp b/utils/process/executor.hpp new file mode 100644 index 000000000000..858ad9c815aa --- /dev/null +++ b/utils/process/executor.hpp @@ -0,0 +1,231 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/executor.hpp +/// Multiprogrammed process executor with isolation guarantees. +/// +/// This module provides a mechanism to invoke more than one process +/// concurrently while at the same time ensuring that each process is run +/// in a clean container and in a "safe" work directory that gets cleaned +/// up automatically on termination. +/// +/// The intended workflow for using this module is the following: +/// +/// 1) Initialize the executor using setup(). Keep the returned object +/// around through the lifetime of the next operations. Only one +/// instance of the executor can be alive at once. +/// 2) Spawn one or more processes with spawn(). On the caller side, keep +/// track of any per-process data you may need using the returned +/// exec_handle, which is unique among the set of active processes. +/// 3) Call wait() or wait_any() to wait for completion of a process started +/// in the previous step. Repeat as desired. +/// 4) Use the returned exit_handle object by wait() or wait_any() to query +/// the status of the terminated process and/or to access any of its +/// data files. +/// 5) Invoke cleanup() on the exit_handle to wipe any stale data. +/// 6) Invoke cleanup() on the object returned by setup(). +/// +/// It is the responsibility of the caller to ensure that calls to +/// spawn() and spawn_followup() are balanced with wait() and wait_any() calls. +/// +/// Processes executed in this manner have access to two different "unique" +/// directories: the first is the "work directory", which is an empty directory +/// that acts as the subprocess' work directory; the second is the "control +/// directory", which is the location where the in-process code may place files +/// that are not clobbered by activities in the work directory. + +#if !defined(UTILS_PROCESS_EXECUTOR_HPP) +#define UTILS_PROCESS_EXECUTOR_HPP + +#include "utils/process/executor_fwd.hpp" + +#include +#include + +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional.hpp" +#include "utils/passwd_fwd.hpp" +#include "utils/process/child_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace utils { +namespace process { +namespace executor { + + +namespace detail { + + +extern const char* stdout_name; +extern const char* stderr_name; +extern const char* work_subdir; + + +/// Shared reference counter. +typedef std::shared_ptr< std::size_t > refcnt_t; + + +void setup_child(const utils::optional< utils::passwd::user >, + const utils::fs::path&, const utils::fs::path&); + + +} // namespace detail + + +/// Maintenance data held while a subprocess is being executed. +/// +/// This data structure exists from the moment a subprocess is executed via +/// executor::spawn() to when it is cleaned up with exit_handle::cleanup(). +/// +/// The caller NEED NOT maintain this object alive for the execution of the +/// subprocess. However, the PID contained in here can be used to match +/// exec_handle objects with corresponding exit_handle objects via their +/// original_pid() method. +/// +/// Objects of this type can be copied around but their implementation is +/// shared. The implication of this is that only the last copy of a given exit +/// handle will execute the automatic cleanup() on destruction. +class exec_handle { + struct impl; + + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class executor_handle; + exec_handle(std::shared_ptr< impl >); + +public: + ~exec_handle(void); + + int pid(void) const; + utils::fs::path control_directory(void) const; + utils::fs::path work_directory(void) const; + const utils::fs::path& stdout_file(void) const; + const utils::fs::path& stderr_file(void) const; +}; + + +/// Container for the data of a process termination. +/// +/// This handle provides access to the details of the process that terminated +/// and serves as the owner of the remaining on-disk files. The caller is +/// expected to call cleanup() before destruction to remove the on-disk state. +/// +/// Objects of this type can be copied around but their implementation is +/// shared. The implication of this is that only the last copy of a given exit +/// handle will execute the automatic cleanup() on destruction. +class exit_handle { + struct impl; + + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class executor_handle; + exit_handle(std::shared_ptr< impl >); + + detail::refcnt_t state_owners(void) const; + +public: + ~exit_handle(void); + + void cleanup(void); + + int original_pid(void) const; + const utils::optional< utils::process::status >& status(void) const; + const utils::optional< utils::passwd::user >& unprivileged_user(void) const; + const utils::datetime::timestamp& start_time() const; + const utils::datetime::timestamp& end_time() const; + utils::fs::path control_directory(void) const; + utils::fs::path work_directory(void) const; + const utils::fs::path& stdout_file(void) const; + const utils::fs::path& stderr_file(void) const; +}; + + +/// Handler for the livelihood of the executor. +/// +/// Objects of this type can be copied around (because we do not have move +/// semantics...) but their implementation is shared. Only one instance of the +/// executor can exist at any point in time. +class executor_handle { + struct impl; + /// Pointer to internal implementation. + std::shared_ptr< impl > _pimpl; + + friend executor_handle setup(void); + executor_handle(void) throw(); + + utils::fs::path spawn_pre(void); + exec_handle spawn_post(const utils::fs::path&, + const utils::fs::path&, + const utils::fs::path&, + const utils::datetime::delta&, + const utils::optional< utils::passwd::user >, + std::auto_ptr< utils::process::child >); + + void spawn_followup_pre(void); + exec_handle spawn_followup_post(const exit_handle&, + const utils::datetime::delta&, + std::auto_ptr< utils::process::child >); + +public: + ~executor_handle(void); + + const utils::fs::path& root_work_directory(void) const; + + void cleanup(void); + + template< class Hook > + exec_handle spawn(Hook, + const datetime::delta&, + const utils::optional< utils::passwd::user >, + const utils::optional< utils::fs::path > = utils::none, + const utils::optional< utils::fs::path > = utils::none); + + template< class Hook > + exec_handle spawn_followup(Hook, + const exit_handle&, + const datetime::delta&); + + exit_handle wait(const exec_handle); + exit_handle wait_any(void); + + void check_interrupt(void) const; +}; + + +executor_handle setup(void); + + +} // namespace executor +} // namespace process +} // namespace utils + + +#endif // !defined(UTILS_PROCESS_EXECUTOR_HPP) diff --git a/utils/process/executor.ipp b/utils/process/executor.ipp new file mode 100644 index 000000000000..e91f994673d7 --- /dev/null +++ b/utils/process/executor.ipp @@ -0,0 +1,182 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_PROCESS_EXECUTOR_IPP) +#define UTILS_PROCESS_EXECUTOR_IPP + +#include "utils/process/executor.hpp" + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/child.ipp" + +namespace utils { +namespace process { + + +namespace executor { +namespace detail { + +/// Functor to execute a hook in a child process. +/// +/// The hook is executed after the process has been isolated per the logic in +/// utils::process::isolation based on the input parameters at construction +/// time. +template< class Hook > +class run_child { + /// Function or functor to invoke in the child. + Hook _hook; + + /// Directory where the hook may place control files. + const fs::path& _control_directory; + + /// Directory to enter when running the subprocess. + /// + /// This is a subdirectory of _control_directory but is separate so that + /// subprocess operations do not inadvertently affect our files. + const fs::path& _work_directory; + + /// User to switch to when running the subprocess. + /// + /// If not none, the subprocess will be executed as the provided user and + /// the control and work directories will be writable by this user. + const optional< passwd::user > _unprivileged_user; + +public: + /// Constructor. + /// + /// \param hook Function or functor to invoke in the child. + /// \param control_directory Directory where control files can be placed. + /// \param work_directory Directory to enter when running the subprocess. + /// \param unprivileged_user If set, user to switch to before execution. + run_child(Hook hook, + const fs::path& control_directory, + const fs::path& work_directory, + const optional< passwd::user > unprivileged_user) : + _hook(hook), + _control_directory(control_directory), + _work_directory(work_directory), + _unprivileged_user(unprivileged_user) + { + } + + /// Body of the subprocess. + void + operator()(void) + { + executor::detail::setup_child(_unprivileged_user, + _control_directory, _work_directory); + _hook(_control_directory); + } +}; + +} // namespace detail +} // namespace executor + + +/// Forks and executes a subprocess asynchronously. +/// +/// \tparam Hook Type of the hook. +/// \param hook Function or functor to run in the subprocess. +/// \param timeout Maximum amount of time the subprocess can run for. +/// \param unprivileged_user If not none, user to switch to before execution. +/// \param stdout_target If not none, file to which to write the stdout of the +/// test case. +/// \param stderr_target If not none, file to which to write the stderr of the +/// test case. +/// +/// \return A handle for the background operation. Used to match the result of +/// the execution returned by wait_any() with this invocation. +template< class Hook > +executor::exec_handle +executor::executor_handle::spawn( + Hook hook, + const datetime::delta& timeout, + const optional< passwd::user > unprivileged_user, + const optional< fs::path > stdout_target, + const optional< fs::path > stderr_target) +{ + const fs::path unique_work_directory = spawn_pre(); + + const fs::path stdout_path = stdout_target ? + stdout_target.get() : (unique_work_directory / detail::stdout_name); + const fs::path stderr_path = stderr_target ? + stderr_target.get() : (unique_work_directory / detail::stderr_name); + + std::auto_ptr< process::child > child = process::child::fork_files( + detail::run_child< Hook >(hook, + unique_work_directory, + unique_work_directory / detail::work_subdir, + unprivileged_user), + stdout_path, stderr_path); + + return spawn_post(unique_work_directory, stdout_path, stderr_path, + timeout, unprivileged_user, child); +} + + +/// Forks and executes a subprocess asynchronously in the context of another. +/// +/// By context we understand the on-disk state of a previously-executed process, +/// thus the new subprocess spawned by this function will run with the same +/// control and work directories as another process. +/// +/// \tparam Hook Type of the hook. +/// \param hook Function or functor to run in the subprocess. +/// \param base Context of the subprocess in which to run this one. The +/// exit_handle provided here must remain alive throughout the existence of +/// this other object because the original exit_handle is the one that owns +/// the on-disk state. +/// \param timeout Maximum amount of time the subprocess can run for. +/// +/// \return A handle for the background operation. Used to match the result of +/// the execution returned by wait_any() with this invocation. +template< class Hook > +executor::exec_handle +executor::executor_handle::spawn_followup(Hook hook, + const exit_handle& base, + const datetime::delta& timeout) +{ + spawn_followup_pre(); + + std::auto_ptr< process::child > child = process::child::fork_files( + detail::run_child< Hook >(hook, + base.control_directory(), + base.work_directory(), + base.unprivileged_user()), + base.stdout_file(), base.stderr_file()); + + return spawn_followup_post(base, timeout, child); +} + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_EXECUTOR_IPP) diff --git a/utils/process/executor_fwd.hpp b/utils/process/executor_fwd.hpp new file mode 100644 index 000000000000..ec63227993f3 --- /dev/null +++ b/utils/process/executor_fwd.hpp @@ -0,0 +1,49 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/executor_fwd.hpp +/// Forward declarations for utils/process/executor.hpp + +#if !defined(UTILS_PROCESS_EXECUTOR_FWD_HPP) +#define UTILS_PROCESS_EXECUTOR_FWD_HPP + +namespace utils { +namespace process { +namespace executor { + + +class exec_handle; +class executor_handle; +class exit_handle; + + +} // namespace executor +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_EXECUTOR_FWD_HPP) diff --git a/utils/process/executor_test.cpp b/utils/process/executor_test.cpp new file mode 100644 index 000000000000..13ae69bd44ed --- /dev/null +++ b/utils/process/executor_test.cpp @@ -0,0 +1,940 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/executor.ipp" + +extern "C" { +#include +#include +#include + +#include +#include +} + +#include +#include +#include +#include +#include + +#include + +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" +#include "utils/stacktrace.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace datetime = utils::datetime; +namespace executor = utils::process::executor; +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace process = utils::process; +namespace signals = utils::signals; +namespace text = utils::text; + +using utils::none; +using utils::optional; + + +/// Large timeout for the processes we spawn. +/// +/// This number is supposed to be (much) larger than the timeout of the test +/// cases that use it so that children processes are not killed spuriously. +static const datetime::delta infinite_timeout(1000000, 0); + + +static void do_exit(const int) UTILS_NORETURN; + + +/// Terminates a subprocess without invoking destructors. +/// +/// This is just a simple wrapper over _exit(2) because we cannot use std::exit +/// on exit from a subprocess. The reason is that we do not want to invoke any +/// destructors as otherwise we'd clear up the global executor state by mistake. +/// This wouldn't be a major problem if it wasn't because doing so deletes +/// on-disk files and we want to leave them in place so that the parent process +/// can test for them! +/// +/// \param exit_code Code to exit with. +static void +do_exit(const int exit_code) +{ + std::cout.flush(); + std::cerr.flush(); + ::_exit(exit_code); +} + + +/// Subprocess that creates a cookie file in its work directory. +class child_create_cookie { + /// Name of the cookie to create. + const std::string _cookie_name; + +public: + /// Constructor. + /// + /// \param cookie_name Name of the cookie to create. + child_create_cookie(const std::string& cookie_name) : + _cookie_name(cookie_name) + { + } + + /// Runs the subprocess. + void + operator()(const fs::path& /* control_directory */) + UTILS_NORETURN + { + std::cout << "Creating cookie: " << _cookie_name << " (stdout)\n"; + std::cerr << "Creating cookie: " << _cookie_name << " (stderr)\n"; + atf::utils::create_file(_cookie_name, ""); + do_exit(EXIT_SUCCESS); + } +}; + + +static void child_delete_all(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that deletes all files in the current directory. +/// +/// This is intended to validate that the test runs in an empty directory, +/// separate from any control files that the executor may have created. +/// +/// \param control_directory Directory where control files separate from the +/// work directory can be placed. +static void +child_delete_all(const fs::path& control_directory) +{ + const fs::path cookie = control_directory / "exec_was_called"; + std::ofstream control_file(cookie.c_str()); + if (!control_file) { + std::cerr << "Failed to create " << cookie << '\n'; + std::abort(); + } + + const int exit_code = ::system("rm *") == -1 + ? EXIT_FAILURE : EXIT_SUCCESS; + do_exit(exit_code); +} + + +static void child_dump_unprivileged_user(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that dumps user configuration. +static void +child_dump_unprivileged_user(const fs::path& /* control_directory */) +{ + const passwd::user current_user = passwd::current_user(); + std::cout << F("UID = %s\n") % current_user.uid; + do_exit(EXIT_SUCCESS); +} + + +/// Subprocess that returns a specific exit code. +class child_exit { + /// Exit code to return. + int _exit_code; + +public: + /// Constructor. + /// + /// \param exit_code Exit code to return. + child_exit(const int exit_code) : _exit_code(exit_code) + { + } + + /// Runs the subprocess. + void + operator()(const fs::path& /* control_directory */) + UTILS_NORETURN + { + do_exit(_exit_code); + } +}; + + +static void child_pause(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that just blocks. +static void +child_pause(const fs::path& /* control_directory */) +{ + sigset_t mask; + sigemptyset(&mask); + for (;;) { + ::sigsuspend(&mask); + } + std::abort(); +} + + +static void child_print(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that writes to stdout and stderr. +static void +child_print(const fs::path& /* control_directory */) +{ + std::cout << "stdout: some text\n"; + std::cerr << "stderr: some other text\n"; + + do_exit(EXIT_SUCCESS); +} + + +/// Subprocess that sleeps for a period of time before exiting. +class child_sleep { + /// Seconds to sleep for before termination. + int _seconds; + +public: + /// Construtor. + /// + /// \param seconds Seconds to sleep for before termination. + child_sleep(const int seconds) : _seconds(seconds) + { + } + + /// Runs the subprocess. + void + operator()(const fs::path& /* control_directory */) + UTILS_NORETURN + { + ::sleep(_seconds); + do_exit(EXIT_SUCCESS); + } +}; + + +static void child_spawn_blocking_child(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that spawns a subchild that gets stuck. +/// +/// Used by the caller to validate that the whole process tree is terminated +/// when this subprocess is killed. +static void +child_spawn_blocking_child( + const fs::path& /* control_directory */) +{ + pid_t pid = ::fork(); + if (pid == -1) { + std::cerr << "Cannot fork subprocess\n"; + do_exit(EXIT_FAILURE); + } else if (pid == 0) { + for (;;) + ::pause(); + } else { + const fs::path name = fs::path(utils::getenv("CONTROL_DIR").get()) / + "pid"; + std::ofstream pidfile(name.c_str()); + if (!pidfile) { + std::cerr << "Failed to create the pidfile\n"; + do_exit(EXIT_FAILURE); + } + pidfile << pid; + pidfile.close(); + do_exit(EXIT_SUCCESS); + } +} + + +static void child_validate_isolation(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that checks if isolate_child() has been called. +static void +child_validate_isolation(const fs::path& /* control_directory */) +{ + if (utils::getenv("HOME").get() == "fake-value") { + std::cerr << "HOME not reset\n"; + do_exit(EXIT_FAILURE); + } + if (utils::getenv("LANG")) { + std::cerr << "LANG not unset\n"; + do_exit(EXIT_FAILURE); + } + do_exit(EXIT_SUCCESS); +} + + +/// Invokes executor::spawn() with default arguments. +/// +/// \param handle The executor on which to invoke spawn(). +/// \param args Arguments to the binary. +/// \param timeout Maximum time the program can run for. +/// \param unprivileged_user If set, user to switch to when running the child +/// program. +/// \param stdout_target If not none, file to which to write the stdout of the +/// test case. +/// \param stderr_target If not none, file to which to write the stderr of the +/// test case. +/// +/// \return The exec handle for the spawned binary. +template< class Hook > +static executor::exec_handle +do_spawn(executor::executor_handle& handle, Hook hook, + const datetime::delta& timeout = infinite_timeout, + const optional< passwd::user > unprivileged_user = none, + const optional< fs::path > stdout_target = none, + const optional< fs::path > stderr_target = none) +{ + const executor::exec_handle exec_handle = handle.spawn< Hook >( + hook, timeout, unprivileged_user, stdout_target, stderr_target); + return exec_handle; +} + + +/// Checks for a specific exit status in the status of a exit_handle. +/// +/// \param exit_status The expected exit status. +/// \param status The value of exit_handle.status(). +/// +/// \post Terminates the calling test case if the status does not match the +/// required value. +static void +require_exit(const int exit_status, const optional< process::status > status) +{ + ATF_REQUIRE(status); + ATF_REQUIRE(status.get().exited()); + ATF_REQUIRE_EQ(exit_status, status.get().exitstatus()); +} + + +/// Ensures that a killed process is gone. +/// +/// The way we do this is by sending an idempotent signal to the given PID +/// and checking if the signal was delivered. If it was, the process is +/// still alive; if it was not, then it is gone. +/// +/// Note that this might be inaccurate for two reasons: +/// +/// 1) The system may have spawned a new process with the same pid as +/// our subchild... but in practice, this does not happen because +/// most systems do not immediately reuse pid numbers. If that +/// happens... well, we get a false test failure. +/// +/// 2) We ran so fast that even if the process was sent a signal to +/// die, it has not had enough time to process it yet. This is why +/// we retry this a few times. +/// +/// \param pid PID of the process to check. +static void +ensure_dead(const pid_t pid) +{ + int attempts = 30; +retry: + if (::kill(pid, SIGCONT) != -1 || errno != ESRCH) { + if (attempts > 0) { + std::cout << "Subprocess not dead yet; retrying wait\n"; + --attempts; + ::usleep(100000); + goto retry; + } + ATF_FAIL(F("The subprocess %s of our child was not killed") % pid); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_one); +ATF_TEST_CASE_BODY(integration__run_one) +{ + executor::executor_handle handle = executor::setup(); + + const executor::exec_handle exec_handle = do_spawn(handle, child_exit(41)); + + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid()); + require_exit(41, exit_handle.status()); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__run_many); +ATF_TEST_CASE_BODY(integration__run_many) +{ + static const std::size_t num_children = 30; + + executor::executor_handle handle = executor::setup(); + + std::size_t total_children = 0; + std::map< int, int > exp_exit_statuses; + std::map< int, datetime::timestamp > exp_start_times; + for (std::size_t i = 0; i < num_children; ++i) { + const datetime::timestamp start_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 40, 0, i); + + for (std::size_t j = 0; j < 3; j++) { + const std::size_t id = i * 3 + j; + + datetime::set_mock_now(start_time); + const int pid = do_spawn(handle, child_exit(id)).pid(); + exp_exit_statuses.insert(std::make_pair(pid, id)); + exp_start_times.insert(std::make_pair(pid, start_time)); + ++total_children; + } + } + + for (std::size_t i = 0; i < total_children; ++i) { + const datetime::timestamp end_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 50, 10, i); + datetime::set_mock_now(end_time); + executor::exit_handle exit_handle = handle.wait_any(); + const int original_pid = exit_handle.original_pid(); + + const int exit_status = exp_exit_statuses.find(original_pid)->second; + const datetime::timestamp& start_time = exp_start_times.find( + original_pid)->second; + + require_exit(exit_status, exit_handle.status()); + + ATF_REQUIRE_EQ(start_time, exit_handle.start_time()); + ATF_REQUIRE_EQ(end_time, exit_handle.end_time()); + + exit_handle.cleanup(); + + ATF_REQUIRE(!atf::utils::file_exists( + exit_handle.stdout_file().str())); + ATF_REQUIRE(!atf::utils::file_exists( + exit_handle.stderr_file().str())); + ATF_REQUIRE(!atf::utils::file_exists( + exit_handle.work_directory().str())); + } + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__parameters_and_output); +ATF_TEST_CASE_BODY(integration__parameters_and_output) +{ + executor::executor_handle handle = executor::setup(); + + const executor::exec_handle exec_handle = do_spawn(handle, child_print); + + executor::exit_handle exit_handle = handle.wait_any(); + + ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid()); + + require_exit(EXIT_SUCCESS, exit_handle.status()); + + const fs::path stdout_file = exit_handle.stdout_file(); + ATF_REQUIRE(atf::utils::compare_file( + stdout_file.str(), "stdout: some text\n")); + const fs::path stderr_file = exit_handle.stderr_file(); + ATF_REQUIRE(atf::utils::compare_file( + stderr_file.str(), "stderr: some other text\n")); + + exit_handle.cleanup(); + ATF_REQUIRE(!fs::exists(stdout_file)); + ATF_REQUIRE(!fs::exists(stderr_file)); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__custom_output_files); +ATF_TEST_CASE_BODY(integration__custom_output_files) +{ + executor::executor_handle handle = executor::setup(); + + const fs::path stdout_file("custom-stdout.txt"); + const fs::path stderr_file("custom-stderr.txt"); + + const executor::exec_handle exec_handle = do_spawn( + handle, child_print, infinite_timeout, none, + utils::make_optional(stdout_file), + utils::make_optional(stderr_file)); + + executor::exit_handle exit_handle = handle.wait_any(); + + ATF_REQUIRE_EQ(exec_handle.pid(), exit_handle.original_pid()); + + require_exit(EXIT_SUCCESS, exit_handle.status()); + + ATF_REQUIRE_EQ(stdout_file, exit_handle.stdout_file()); + ATF_REQUIRE_EQ(stderr_file, exit_handle.stderr_file()); + + exit_handle.cleanup(); + + handle.cleanup(); + + // Must compare after cleanup to ensure the files did not get deleted. + ATF_REQUIRE(atf::utils::compare_file( + stdout_file.str(), "stdout: some text\n")); + ATF_REQUIRE(atf::utils::compare_file( + stderr_file.str(), "stderr: some other text\n")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__timestamps); +ATF_TEST_CASE_BODY(integration__timestamps) +{ + executor::executor_handle handle = executor::setup(); + + const datetime::timestamp start_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 35, 10, 1000); + const datetime::timestamp end_time = datetime::timestamp::from_values( + 2014, 12, 8, 9, 35, 20, 2000); + + datetime::set_mock_now(start_time); + do_spawn(handle, child_exit(70)); + + datetime::set_mock_now(end_time); + executor::exit_handle exit_handle = handle.wait_any(); + + require_exit(70, exit_handle.status()); + + ATF_REQUIRE_EQ(start_time, exit_handle.start_time()); + ATF_REQUIRE_EQ(end_time, exit_handle.end_time()); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__files); +ATF_TEST_CASE_BODY(integration__files) +{ + executor::executor_handle handle = executor::setup(); + + do_spawn(handle, child_create_cookie("cookie.12345")); + + executor::exit_handle exit_handle = handle.wait_any(); + + ATF_REQUIRE(atf::utils::file_exists( + (exit_handle.work_directory() / "cookie.12345").str())); + + exit_handle.cleanup(); + + ATF_REQUIRE(!atf::utils::file_exists(exit_handle.stdout_file().str())); + ATF_REQUIRE(!atf::utils::file_exists(exit_handle.stderr_file().str())); + ATF_REQUIRE(!atf::utils::file_exists(exit_handle.work_directory().str())); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__followup); +ATF_TEST_CASE_BODY(integration__followup) +{ + executor::executor_handle handle = executor::setup(); + + (void)handle.spawn(child_create_cookie("cookie.1"), infinite_timeout, none); + executor::exit_handle exit_1_handle = handle.wait_any(); + + (void)handle.spawn_followup(child_create_cookie("cookie.2"), exit_1_handle, + infinite_timeout); + executor::exit_handle exit_2_handle = handle.wait_any(); + + ATF_REQUIRE_EQ(exit_1_handle.stdout_file(), exit_2_handle.stdout_file()); + ATF_REQUIRE_EQ(exit_1_handle.stderr_file(), exit_2_handle.stderr_file()); + ATF_REQUIRE_EQ(exit_1_handle.control_directory(), + exit_2_handle.control_directory()); + ATF_REQUIRE_EQ(exit_1_handle.work_directory(), + exit_2_handle.work_directory()); + + (void)handle.spawn_followup(child_create_cookie("cookie.3"), exit_2_handle, + infinite_timeout); + exit_2_handle.cleanup(); + exit_1_handle.cleanup(); + executor::exit_handle exit_3_handle = handle.wait_any(); + + ATF_REQUIRE_EQ(exit_1_handle.stdout_file(), exit_3_handle.stdout_file()); + ATF_REQUIRE_EQ(exit_1_handle.stderr_file(), exit_3_handle.stderr_file()); + ATF_REQUIRE_EQ(exit_1_handle.control_directory(), + exit_3_handle.control_directory()); + ATF_REQUIRE_EQ(exit_1_handle.work_directory(), + exit_3_handle.work_directory()); + + ATF_REQUIRE(atf::utils::file_exists( + (exit_1_handle.work_directory() / "cookie.1").str())); + ATF_REQUIRE(atf::utils::file_exists( + (exit_1_handle.work_directory() / "cookie.2").str())); + ATF_REQUIRE(atf::utils::file_exists( + (exit_1_handle.work_directory() / "cookie.3").str())); + + ATF_REQUIRE(atf::utils::compare_file( + exit_1_handle.stdout_file().str(), + "Creating cookie: cookie.1 (stdout)\n" + "Creating cookie: cookie.2 (stdout)\n" + "Creating cookie: cookie.3 (stdout)\n")); + + ATF_REQUIRE(atf::utils::compare_file( + exit_1_handle.stderr_file().str(), + "Creating cookie: cookie.1 (stderr)\n" + "Creating cookie: cookie.2 (stderr)\n" + "Creating cookie: cookie.3 (stderr)\n")); + + exit_3_handle.cleanup(); + + ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.stdout_file().str())); + ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.stderr_file().str())); + ATF_REQUIRE(!atf::utils::file_exists(exit_1_handle.work_directory().str())); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__output_files_always_exist); +ATF_TEST_CASE_BODY(integration__output_files_always_exist) +{ + executor::executor_handle handle = executor::setup(); + + // This test is racy: we specify a very short timeout for the subprocess so + // that we cause the subprocess to exit before it has had time to set up the + // output files. However, for scheduling reasons, the subprocess may + // actually run to completion before the timer triggers. Retry this a few + // times to attempt to catch a "good test". + for (int i = 0; i < 50; i++) { + const executor::exec_handle exec_handle = + do_spawn(handle, child_exit(0), datetime::delta(0, 100000)); + executor::exit_handle exit_handle = handle.wait(exec_handle); + ATF_REQUIRE(fs::exists(exit_handle.stdout_file())); + ATF_REQUIRE(fs::exists(exit_handle.stderr_file())); + exit_handle.cleanup(); + } + + handle.cleanup(); +} + + +ATF_TEST_CASE(integration__timeouts); +ATF_TEST_CASE_HEAD(integration__timeouts) +{ + set_md_var("timeout", "60"); +} +ATF_TEST_CASE_BODY(integration__timeouts) +{ + executor::executor_handle handle = executor::setup(); + + const executor::exec_handle exec_handle1 = + do_spawn(handle, child_sleep(30), datetime::delta(2, 0)); + const executor::exec_handle exec_handle2 = + do_spawn(handle, child_sleep(40), datetime::delta(5, 0)); + const executor::exec_handle exec_handle3 = + do_spawn(handle, child_exit(15)); + + { + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE_EQ(exec_handle3.pid(), exit_handle.original_pid()); + require_exit(15, exit_handle.status()); + exit_handle.cleanup(); + } + + { + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE_EQ(exec_handle1.pid(), exit_handle.original_pid()); + ATF_REQUIRE(!exit_handle.status()); + const datetime::delta duration = + exit_handle.end_time() - exit_handle.start_time(); + ATF_REQUIRE(duration < datetime::delta(10, 0)); + ATF_REQUIRE(duration >= datetime::delta(2, 0)); + exit_handle.cleanup(); + } + + { + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE_EQ(exec_handle2.pid(), exit_handle.original_pid()); + ATF_REQUIRE(!exit_handle.status()); + const datetime::delta duration = + exit_handle.end_time() - exit_handle.start_time(); + ATF_REQUIRE(duration < datetime::delta(10, 0)); + ATF_REQUIRE(duration >= datetime::delta(4, 0)); + exit_handle.cleanup(); + } + + handle.cleanup(); +} + + +ATF_TEST_CASE(integration__unprivileged_user); +ATF_TEST_CASE_HEAD(integration__unprivileged_user) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(integration__unprivileged_user) +{ + executor::executor_handle handle = executor::setup(); + + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + + do_spawn(handle, child_dump_unprivileged_user, + infinite_timeout, utils::make_optional(unprivileged_user)); + + executor::exit_handle exit_handle = handle.wait_any(); + ATF_REQUIRE(atf::utils::compare_file( + exit_handle.stdout_file().str(), + F("UID = %s\n") % unprivileged_user.uid)); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__auto_cleanup); +ATF_TEST_CASE_BODY(integration__auto_cleanup) +{ + std::vector< int > pids; + std::vector< fs::path > paths; + { + executor::executor_handle handle = executor::setup(); + + pids.push_back(do_spawn(handle, child_exit(10)).pid()); + pids.push_back(do_spawn(handle, child_exit(20)).pid()); + + // This invocation is never waited for below. This is intentional: we + // want the destructor to clean the "leaked" test automatically so that + // the clean up of the parent work directory also happens correctly. + pids.push_back(do_spawn(handle, child_pause).pid()); + + executor::exit_handle exit_handle1 = handle.wait_any(); + paths.push_back(exit_handle1.stdout_file()); + paths.push_back(exit_handle1.stderr_file()); + paths.push_back(exit_handle1.work_directory()); + + executor::exit_handle exit_handle2 = handle.wait_any(); + paths.push_back(exit_handle2.stdout_file()); + paths.push_back(exit_handle2.stderr_file()); + paths.push_back(exit_handle2.work_directory()); + } + for (std::vector< int >::const_iterator iter = pids.begin(); + iter != pids.end(); ++iter) { + ensure_dead(*iter); + } + for (std::vector< fs::path >::const_iterator iter = paths.begin(); + iter != paths.end(); ++iter) { + ATF_REQUIRE(!atf::utils::file_exists((*iter).str())); + } +} + + +/// Ensures that interrupting an executor cleans things up correctly. +/// +/// This test scenario is tricky. We spawn a master child process that runs the +/// executor code and we send a signal to it externally. The child process +/// spawns a bunch of tests that block indefinitely and tries to wait for their +/// results. When the signal is received, we expect an interrupt_error to be +/// raised, which in turn should clean up all test resources and exit the master +/// child process successfully. +/// +/// \param signo Signal to deliver to the executor. +static void +do_signal_handling_test(const int signo) +{ + static const char* cookie = "spawned.txt"; + + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + static const std::size_t num_children = 3; + + optional< fs::path > root_work_directory; + try { + executor::executor_handle handle = executor::setup(); + root_work_directory = handle.root_work_directory(); + + for (std::size_t i = 0; i < num_children; ++i) { + std::cout << "Spawned child number " << i << '\n'; + do_spawn(handle, child_pause); + } + + std::cout << "Creating " << cookie << " cookie\n"; + atf::utils::create_file(cookie, ""); + + std::cout << "Waiting for subprocess termination\n"; + for (std::size_t i = 0; i < num_children; ++i) { + executor::exit_handle exit_handle = handle.wait_any(); + // We may never reach this point in the test, but if we do let's + // make sure the subprocess was terminated as expected. + if (exit_handle.status()) { + if (exit_handle.status().get().signaled() && + exit_handle.status().get().termsig() == SIGKILL) { + // OK. + } else { + std::cerr << "Child exited with unexpected code: " + << exit_handle.status().get(); + std::exit(EXIT_FAILURE); + } + } else { + std::cerr << "Child timed out\n"; + std::exit(EXIT_FAILURE); + } + exit_handle.cleanup(); + } + std::cerr << "Terminating without reception of signal\n"; + std::exit(EXIT_FAILURE); + } catch (const signals::interrupted_error& unused_error) { + std::cerr << "Terminating due to interrupted_error\n"; + // We never kill ourselves until the cookie is created, so it is + // guaranteed that the optional root_work_directory has been + // initialized at this point. + if (atf::utils::file_exists(root_work_directory.get().str())) { + // Some cleanup did not happen; error out. + std::exit(EXIT_FAILURE); + } else { + std::exit(EXIT_SUCCESS); + } + } + std::abort(); + } + + std::cout << "Waiting for " << cookie << " cookie creation\n"; + while (!atf::utils::file_exists(cookie)) { + // Wait for processes. + } + ATF_REQUIRE(::unlink(cookie) != -1); + std::cout << "Killing process\n"; + ATF_REQUIRE(::kill(pid, signo) != -1); + + int status; + std::cout << "Waiting for process termination\n"; + ATF_REQUIRE(::waitpid(pid, &status, 0) != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__signal_handling); +ATF_TEST_CASE_BODY(integration__signal_handling) +{ + // This test scenario is racy so run it multiple times to have higher + // chances of exposing problems. + const std::size_t rounds = 20; + + for (std::size_t i = 0; i < rounds; ++i) { + std::cout << F("Testing round %s\n") % i; + do_signal_handling_test(SIGHUP); + do_signal_handling_test(SIGINT); + do_signal_handling_test(SIGTERM); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__isolate_child_is_called); +ATF_TEST_CASE_BODY(integration__isolate_child_is_called) +{ + executor::executor_handle handle = executor::setup(); + + utils::setenv("HOME", "fake-value"); + utils::setenv("LANG", "es_ES"); + do_spawn(handle, child_validate_isolation); + + executor::exit_handle exit_handle = handle.wait_any(); + require_exit(EXIT_SUCCESS, exit_handle.status()); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__process_group_is_terminated); +ATF_TEST_CASE_BODY(integration__process_group_is_terminated) +{ + utils::setenv("CONTROL_DIR", fs::current_path().str()); + + executor::executor_handle handle = executor::setup(); + do_spawn(handle, child_spawn_blocking_child); + + executor::exit_handle exit_handle = handle.wait_any(); + require_exit(EXIT_SUCCESS, exit_handle.status()); + exit_handle.cleanup(); + + handle.cleanup(); + + if (!fs::exists(fs::path("pid"))) + fail("The pid file was not created"); + + std::ifstream pidfile("pid"); + ATF_REQUIRE(pidfile); + pid_t pid; + pidfile >> pid; + pidfile.close(); + + ensure_dead(pid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__prevent_clobbering_control_files); +ATF_TEST_CASE_BODY(integration__prevent_clobbering_control_files) +{ + executor::executor_handle handle = executor::setup(); + + do_spawn(handle, child_delete_all); + + executor::exit_handle exit_handle = handle.wait_any(); + require_exit(EXIT_SUCCESS, exit_handle.status()); + ATF_REQUIRE(atf::utils::file_exists( + (exit_handle.control_directory() / "exec_was_called").str())); + ATF_REQUIRE(!atf::utils::file_exists( + (exit_handle.work_directory() / "exec_was_called").str())); + exit_handle.cleanup(); + + handle.cleanup(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, integration__run_one); + ATF_ADD_TEST_CASE(tcs, integration__run_many); + + ATF_ADD_TEST_CASE(tcs, integration__parameters_and_output); + ATF_ADD_TEST_CASE(tcs, integration__custom_output_files); + ATF_ADD_TEST_CASE(tcs, integration__timestamps); + ATF_ADD_TEST_CASE(tcs, integration__files); + + ATF_ADD_TEST_CASE(tcs, integration__followup); + + ATF_ADD_TEST_CASE(tcs, integration__output_files_always_exist); + ATF_ADD_TEST_CASE(tcs, integration__timeouts); + ATF_ADD_TEST_CASE(tcs, integration__unprivileged_user); + ATF_ADD_TEST_CASE(tcs, integration__auto_cleanup); + ATF_ADD_TEST_CASE(tcs, integration__signal_handling); + ATF_ADD_TEST_CASE(tcs, integration__isolate_child_is_called); + ATF_ADD_TEST_CASE(tcs, integration__process_group_is_terminated); + ATF_ADD_TEST_CASE(tcs, integration__prevent_clobbering_control_files); +} diff --git a/utils/process/fdstream.cpp b/utils/process/fdstream.cpp new file mode 100644 index 000000000000..ccd7a1f91b0c --- /dev/null +++ b/utils/process/fdstream.cpp @@ -0,0 +1,76 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/fdstream.hpp" + +#include "utils/noncopyable.hpp" +#include "utils/process/systembuf.hpp" + + +namespace utils { +namespace process { + + +/// Private implementation fields for ifdstream. +struct ifdstream::impl : utils::noncopyable { + /// The systembuf backing this file descriptor. + systembuf _systembuf; + + /// Initializes private implementation data. + /// + /// \param fd The file descriptor. + impl(const int fd) : _systembuf(fd) {} +}; + + +} // namespace process +} // namespace utils + + +namespace process = utils::process; + + +/// Constructs a new ifdstream based on an open file descriptor. +/// +/// This grabs ownership of the file descriptor. +/// +/// \param fd The file descriptor to read from. Must be open and valid. +process::ifdstream::ifdstream(const int fd) : + std::istream(NULL), + _pimpl(new impl(fd)) +{ + rdbuf(&_pimpl->_systembuf); +} + + +/// Destroys an ifdstream object. +/// +/// \post The file descriptor attached to this stream is closed. +process::ifdstream::~ifdstream(void) +{ +} diff --git a/utils/process/fdstream.hpp b/utils/process/fdstream.hpp new file mode 100644 index 000000000000..e785b0ac4282 --- /dev/null +++ b/utils/process/fdstream.hpp @@ -0,0 +1,66 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/fdstream.hpp +/// Provides the utils::process::ifdstream class. + +#if !defined(UTILS_PROCESS_FDSTREAM_HPP) +#define UTILS_PROCESS_FDSTREAM_HPP + +#include "utils/process/fdstream_fwd.hpp" + +#include +#include + +#include "utils/noncopyable.hpp" + +namespace utils { +namespace process { + + +/// An input stream backed by a file descriptor. +/// +/// This class grabs ownership of the file descriptor. I.e. when the class is +/// destroyed, the file descriptor is closed unconditionally. +class ifdstream : public std::istream, noncopyable +{ + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +public: + explicit ifdstream(const int); + ~ifdstream(void); +}; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_FDSTREAM_HPP) diff --git a/utils/process/fdstream_fwd.hpp b/utils/process/fdstream_fwd.hpp new file mode 100644 index 000000000000..8d369ea0bfa5 --- /dev/null +++ b/utils/process/fdstream_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/fdstream_fwd.hpp +/// Forward declarations for utils/process/fdstream.hpp + +#if !defined(UTILS_PROCESS_FDSTREAM_FWD_HPP) +#define UTILS_PROCESS_FDSTREAM_FWD_HPP + +namespace utils { +namespace process { + + +class ifdstream; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_FDSTREAM_FWD_HPP) diff --git a/utils/process/fdstream_test.cpp b/utils/process/fdstream_test.cpp new file mode 100644 index 000000000000..8420568216f0 --- /dev/null +++ b/utils/process/fdstream_test.cpp @@ -0,0 +1,73 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/fdstream.hpp" + +extern "C" { +#include +} + +#include + +#include "utils/process/systembuf.hpp" + +using utils::process::ifdstream; +using utils::process::systembuf; + + +ATF_TEST_CASE(ifdstream); +ATF_TEST_CASE_HEAD(ifdstream) +{ + set_md_var("descr", "Tests the ifdstream class"); +} +ATF_TEST_CASE_BODY(ifdstream) +{ + int fds[2]; + ATF_REQUIRE(::pipe(fds) != -1); + + ifdstream rend(fds[0]); + + systembuf wbuf(fds[1]); + std::ostream wend(&wbuf); + + // XXX This assumes that the pipe's buffer is big enough to accept + // the data written without blocking! + wend << "1Test 1message\n"; + wend.flush(); + std::string tmp; + rend >> tmp; + ATF_REQUIRE_EQ(tmp, "1Test"); + rend >> tmp; + ATF_REQUIRE_EQ(tmp, "1message"); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, ifdstream); +} diff --git a/utils/process/helpers.cpp b/utils/process/helpers.cpp new file mode 100644 index 000000000000..15deecd95f24 --- /dev/null +++ b/utils/process/helpers.cpp @@ -0,0 +1,74 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include +#include +#include + + +static int +print_args(int argc, char* argv[]) +{ + for (int i = 0; i < argc; i++) + std::cout << "argv[" << i << "] = " << argv[i] << "\n"; + std::cout << "argv[" << argc << "] = NULL"; + return EXIT_SUCCESS; +} + + +static int +return_code(int argc, char* argv[]) +{ + if (argc != 3) + std::abort(); + + std::istringstream iss(argv[2]); + int code; + iss >> code; + return code; +} + + +int +main(int argc, char* argv[]) +{ + if (argc < 2) { + std::cerr << "Must provide a helper name\n"; + std::exit(EXIT_FAILURE); + } + + if (std::strcmp(argv[1], "print-args") == 0) { + return print_args(argc, argv); + } else if (std::strcmp(argv[1], "return-code") == 0) { + return return_code(argc, argv); + } else { + std::cerr << "Unknown helper\n"; + return EXIT_FAILURE; + } +} diff --git a/utils/process/isolation.cpp b/utils/process/isolation.cpp new file mode 100644 index 000000000000..90dd08d5772d --- /dev/null +++ b/utils/process/isolation.cpp @@ -0,0 +1,207 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/isolation.hpp" + +extern "C" { +#include + +#include +#include +#include +} + +#include +#include +#include +#include + +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/env.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/misc.hpp" +#include "utils/stacktrace.hpp" + +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace process = utils::process; +namespace signals = utils::signals; + +using utils::optional; + + +/// Magic exit code to denote an error while preparing the subprocess. +const int process::exit_isolation_failure = 124; + + +namespace { + + +static void fail(const std::string&, const int) UTILS_NORETURN; + + +/// Fails the process with an errno-based error message. +/// +/// \param message The message to print. The errno-based string will be +/// appended to this, just like in perror(3). +/// \param original_errno The error code to format. +static void +fail(const std::string& message, const int original_errno) +{ + std::cerr << message << ": " << std::strerror(original_errno) << '\n'; + std::exit(process::exit_isolation_failure); +} + + +/// Changes the owner of a path. +/// +/// This function is intended to be called from a subprocess getting ready to +/// invoke an external binary. Therefore, if there is any error during the +/// setup, the new process is terminated with an error code. +/// +/// \param file The path to the file or directory to affect. +/// \param uid The UID to set on the path. +/// \param gid The GID to set on the path. +static void +do_chown(const fs::path& file, const uid_t uid, const gid_t gid) +{ + if (::chown(file.c_str(), uid, gid) == -1) + fail(F("chown(%s, %s, %s) failed; UID is %s and GID is %s") + % file % uid % gid % ::getuid() % ::getgid(), errno); +} + + +/// Resets the environment of the process to a known state. +/// +/// \param work_directory Path to the work directory being used. +/// +/// \throw std::runtime_error If there is a problem setting up the environment. +static void +prepare_environment(const fs::path& work_directory) +{ + const char* to_unset[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", + "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", + "LC_TIME", NULL }; + const char** iter; + for (iter = to_unset; *iter != NULL; ++iter) { + utils::unsetenv(*iter); + } + + utils::setenv("HOME", work_directory.str()); + utils::setenv("TMPDIR", work_directory.str()); + utils::setenv("TZ", "UTC"); +} + + +} // anonymous namespace + + +/// Cleans up the container process to run a new child. +/// +/// If there is any error during the setup, the new process is terminated +/// with an error code. +/// +/// \param unprivileged_user Unprivileged user to run the test case as. +/// \param work_directory Path to the test case-specific work directory. +void +process::isolate_child(const optional< passwd::user >& unprivileged_user, + const fs::path& work_directory) +{ + isolate_path(unprivileged_user, work_directory); + if (::chdir(work_directory.c_str()) == -1) + fail(F("chdir(%s) failed") % work_directory, errno); + + utils::unlimit_core_size(); + if (!signals::reset_all()) { + LW("Failed to reset one or more signals to their default behavior"); + } + prepare_environment(work_directory); + (void)::umask(0022); + + if (unprivileged_user && passwd::current_user().is_root()) { + const passwd::user& user = unprivileged_user.get(); + + if (user.gid != ::getgid()) { + if (::setgid(user.gid) == -1) + fail(F("setgid(%s) failed; UID is %s and GID is %s") + % user.gid % ::getuid() % ::getgid(), errno); + if (::getuid() == 0) { + ::gid_t groups[1]; + groups[0] = user.gid; + if (::setgroups(1, groups) == -1) + fail(F("setgroups(1, [%s]) failed; UID is %s and GID is %s") + % user.gid % ::getuid() % ::getgid(), errno); + } + } + if (user.uid != ::getuid()) { + if (::setuid(user.uid) == -1) + fail(F("setuid(%s) failed; UID is %s and GID is %s") + % user.uid % ::getuid() % ::getgid(), errno); + } + } +} + + +/// Sets up a path to be writable by a child isolated with isolate_child. +/// +/// If there is any error during the setup, the new process is terminated +/// with an error code. +/// +/// The caller should use this to prepare any directory or file that the child +/// should be able to write to *before* invoking isolate_child(). Note that +/// isolate_child() will use isolate_path() on the work directory though. +/// +/// \param unprivileged_user Unprivileged user to run the test case as. +/// \param file Path to the file to modify. +void +process::isolate_path(const optional< passwd::user >& unprivileged_user, + const fs::path& file) +{ + if (!unprivileged_user || !passwd::current_user().is_root()) + return; + const passwd::user& user = unprivileged_user.get(); + + const bool change_group = user.gid != ::getgid(); + const bool change_user = user.uid != ::getuid(); + + if (!change_user && !change_group) { + // Keep same permissions. + } else if (change_user && change_group) { + do_chown(file, user.uid, user.gid); + } else if (!change_user && change_group) { + do_chown(file, ::getuid(), user.gid); + } else { + INV(change_user && !change_group); + do_chown(file, user.uid, ::getgid()); + } +} diff --git a/utils/process/isolation.hpp b/utils/process/isolation.hpp new file mode 100644 index 000000000000..69793a76c7b4 --- /dev/null +++ b/utils/process/isolation.hpp @@ -0,0 +1,60 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/isolation.hpp +/// Utilities to isolate a process. +/// +/// By "isolation" in this context we mean forcing a process to run in a +/// more-or-less deterministic environment. + +#if !defined(UTILS_PROCESS_ISOLATION_HPP) +#define UTILS_PROCESS_ISOLATION_HPP + +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" +#include "utils/passwd_fwd.hpp" + +namespace utils { +namespace process { + + +extern const int exit_isolation_failure; + + +void isolate_child(const utils::optional< utils::passwd::user >&, + const utils::fs::path&); + +void isolate_path(const utils::optional< utils::passwd::user >&, + const utils::fs::path&); + + +} // namespace process +} // namespace utils + + +#endif // !defined(UTILS_PROCESS_ISOLATION_HPP) diff --git a/utils/process/isolation_test.cpp b/utils/process/isolation_test.cpp new file mode 100644 index 000000000000..dc723cc65c88 --- /dev/null +++ b/utils/process/isolation_test.cpp @@ -0,0 +1,622 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/isolation.hpp" + +extern "C" { +#include +#include +#include + +#include +} + +#include +#include +#include +#include + +#include + +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/passwd.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; +namespace passwd = utils::passwd; +namespace process = utils::process; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Runs the given hook in a subprocess. +/// +/// \param hook The code to run in the subprocess. +/// +/// \return The status of the subprocess for further validation. +/// +/// \post The subprocess.stdout and subprocess.stderr files, created in the +/// current directory, contain the output of the subprocess. +template< typename Hook > +static process::status +fork_and_run(Hook hook) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + hook, fs::path("subprocess.stdout"), fs::path("subprocess.stderr")); + const process::status status = child->wait(); + + atf::utils::cat_file("subprocess.stdout", "isolated child stdout: "); + atf::utils::cat_file("subprocess.stderr", "isolated child stderr: "); + + return status; +} + + +/// Subprocess that validates the cleanliness of the environment. +/// +/// \post Exits with success if the environment is clean; failure otherwise. +static void +check_clean_environment(void) +{ + fs::mkdir(fs::path("some-directory"), 0755); + process::isolate_child(none, fs::path("some-directory")); + + bool failed = false; + + const char* empty[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", + "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", + "LC_TIME", NULL }; + const char** iter; + for (iter = empty; *iter != NULL; ++iter) { + if (utils::getenv(*iter)) { + failed = true; + std::cout << F("%s was not unset\n") % *iter; + } + } + + if (utils::getenv_with_default("HOME", "") != "some-directory") { + failed = true; + std::cout << "HOME was not set to the work directory\n"; + } + + if (utils::getenv_with_default("TMPDIR", "") != "some-directory") { + failed = true; + std::cout << "TMPDIR was not set to the work directory\n"; + } + + if (utils::getenv_with_default("TZ", "") != "UTC") { + failed = true; + std::cout << "TZ was not set to UTC\n"; + } + + if (utils::getenv_with_default("LEAVE_ME_ALONE", "") != "kill-some-day") { + failed = true; + std::cout << "LEAVE_ME_ALONE was modified while it should not have " + "been\n"; + } + + std::exit(failed ? EXIT_FAILURE : EXIT_SUCCESS); +} + + +/// Subprocess that checks if user privileges are dropped. +class check_drop_privileges { + /// The user to drop the privileges to. + const passwd::user _unprivileged_user; + +public: + /// Constructor. + /// + /// \param unprivileged_user The user to drop the privileges to. + check_drop_privileges(const passwd::user& unprivileged_user) : + _unprivileged_user(unprivileged_user) + { + } + + /// Body of the subprocess. + /// + /// \post Exits with success if the process has dropped privileges as + /// expected. + void + operator()(void) const + { + fs::mkdir(fs::path("subdir"), 0755); + process::isolate_child(utils::make_optional(_unprivileged_user), + fs::path("subdir")); + + if (::getuid() == 0) { + std::cout << "UID is still 0\n"; + std::exit(EXIT_FAILURE); + } + + if (::getgid() == 0) { + std::cout << "GID is still 0\n"; + std::exit(EXIT_FAILURE); + } + + ::gid_t groups[1]; + if (::getgroups(1, groups) == -1) { + // Should only fail if we get more than one group notifying about + // not enough space in the groups variable to store the whole + // result. + INV(errno == EINVAL); + std::exit(EXIT_FAILURE); + } + if (groups[0] == 0) { + std::cout << "Primary group is still 0\n"; + std::exit(EXIT_FAILURE); + } + + std::ofstream output("file.txt"); + if (!output) { + std::cout << "Cannot write to isolated directory; owner not " + "changed?\n"; + std::exit(EXIT_FAILURE); + } + + std::exit(EXIT_SUCCESS); + } +}; + + +/// Subprocess that dumps core to validate core dumping abilities. +static void +check_enable_core_dumps(void) +{ + process::isolate_child(none, fs::path(".")); + std::abort(); +} + + +/// Subprocess that checks if the work directory is entered. +class check_enter_work_directory { + /// Directory to enter. May be releative. + const fs::path _directory; + +public: + /// Constructor. + /// + /// \param directory Directory to enter. + check_enter_work_directory(const fs::path& directory) : + _directory(directory) + { + } + + /// Body of the subprocess. + /// + /// \post Exits with success if the process has entered the given work + /// directory; false otherwise. + void + operator()(void) const + { + const fs::path exp_subdir = fs::current_path() / _directory; + process::isolate_child(none, _directory); + std::exit(fs::current_path() == exp_subdir ? + EXIT_SUCCESS : EXIT_FAILURE); + } +}; + + +/// Subprocess that validates that it owns a session. +/// +/// \post Exits with success if the process lives in its own session; +/// failure otherwise. +static void +check_new_session(void) +{ + process::isolate_child(none, fs::path(".")); + std::exit(::getsid(::getpid()) == ::getpid() ? EXIT_SUCCESS : EXIT_FAILURE); +} + + +/// Subprocess that validates the disconnection from any terminal. +/// +/// \post Exits with success if the environment is clean; failure otherwise. +static void +check_no_terminal(void) +{ + process::isolate_child(none, fs::path(".")); + + const char* const args[] = { + "/bin/sh", + "-i", + "-c", + "echo success", + NULL + }; + ::execv("/bin/sh", UTILS_UNCONST(char*, args)); + std::abort(); +} + + +/// Subprocess that validates that it has become the leader of a process group. +/// +/// \post Exits with success if the process lives in its own process group; +/// failure otherwise. +static void +check_process_group(void) +{ + process::isolate_child(none, fs::path(".")); + std::exit(::getpgid(::getpid()) == ::getpid() ? + EXIT_SUCCESS : EXIT_FAILURE); +} + + +/// Subprocess that validates that the umask has been reset. +/// +/// \post Exits with success if the umask matches the expected value; failure +/// otherwise. +static void +check_umask(void) +{ + process::isolate_child(none, fs::path(".")); + std::exit(::umask(0) == 0022 ? EXIT_SUCCESS : EXIT_FAILURE); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__clean_environment); +ATF_TEST_CASE_BODY(isolate_child__clean_environment) +{ + utils::setenv("HOME", "/non-existent/directory"); + utils::setenv("TMPDIR", "/non-existent/directory"); + utils::setenv("LANG", "C"); + utils::setenv("LC_ALL", "C"); + utils::setenv("LC_COLLATE", "C"); + utils::setenv("LC_CTYPE", "C"); + utils::setenv("LC_MESSAGES", "C"); + utils::setenv("LC_MONETARY", "C"); + utils::setenv("LC_NUMERIC", "C"); + utils::setenv("LC_TIME", "C"); + utils::setenv("LEAVE_ME_ALONE", "kill-some-day"); + utils::setenv("TZ", "EST+5"); + + const process::status status = fork_and_run(check_clean_environment); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE(isolate_child__other_user_when_unprivileged); +ATF_TEST_CASE_HEAD(isolate_child__other_user_when_unprivileged) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(isolate_child__other_user_when_unprivileged) +{ + const passwd::user user = passwd::current_user(); + + passwd::user other_user = user; + other_user.uid += 1; + other_user.gid += 1; + process::isolate_child(utils::make_optional(other_user), fs::path(".")); + + ATF_REQUIRE_EQ(user.uid, ::getuid()); + ATF_REQUIRE_EQ(user.gid, ::getgid()); +} + + +ATF_TEST_CASE(isolate_child__drop_privileges); +ATF_TEST_CASE_HEAD(isolate_child__drop_privileges) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(isolate_child__drop_privileges) +{ + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + + const process::status status = fork_and_run(check_drop_privileges( + unprivileged_user)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE(isolate_child__drop_privileges_fail_uid); +ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_uid) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_uid) +{ + // Fake the current user as root so that we bypass the protections in + // isolate_child that prevent us from attempting a user switch when we are + // not root. We do this so we can trigger the setuid failure. + passwd::user root = passwd::user("root", 0, 0); + ATF_REQUIRE(root.is_root()); + passwd::set_current_user_for_testing(root); + + passwd::user unprivileged_user = passwd::current_user(); + unprivileged_user.uid += 1; + + const process::status status = fork_and_run(check_drop_privileges( + unprivileged_user)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("(chown|setuid).*failed", + "subprocess.stderr")); +} + + +ATF_TEST_CASE(isolate_child__drop_privileges_fail_gid); +ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_gid) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_gid) +{ + // Fake the current user as root so that we bypass the protections in + // isolate_child that prevent us from attempting a user switch when we are + // not root. We do this so we can trigger the setgid failure. + passwd::user root = passwd::user("root", 0, 0); + ATF_REQUIRE(root.is_root()); + passwd::set_current_user_for_testing(root); + + passwd::user unprivileged_user = passwd::current_user(); + unprivileged_user.gid += 1; + + const process::status status = fork_and_run(check_drop_privileges( + unprivileged_user)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("(chown|setgid).*failed", + "subprocess.stderr")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enable_core_dumps); +ATF_TEST_CASE_BODY(isolate_child__enable_core_dumps) +{ + utils::require_run_coredump_tests(this); + + struct ::rlimit rl; + if (::getrlimit(RLIMIT_CORE, &rl) == -1) + fail("Failed to query the core size limit"); + if (rl.rlim_cur == 0 || rl.rlim_max == 0) + skip("Maximum core size is zero; cannot run test"); + rl.rlim_cur = 0; + if (::setrlimit(RLIMIT_CORE, &rl) == -1) + fail("Failed to lower the core size limit"); + + const process::status status = fork_and_run(check_enable_core_dumps); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(status.coredump()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory); +ATF_TEST_CASE_BODY(isolate_child__enter_work_directory) +{ + const fs::path directory("some/sub/directory"); + fs::mkdir_p(directory, 0755); + const process::status status = fork_and_run( + check_enter_work_directory(directory)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory_failure); +ATF_TEST_CASE_BODY(isolate_child__enter_work_directory_failure) +{ + const fs::path directory("some/sub/directory"); + const process::status status = fork_and_run( + check_enter_work_directory(directory)); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("chdir\\(some/sub/directory\\) failed", + "subprocess.stderr")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__new_session); +ATF_TEST_CASE_BODY(isolate_child__new_session) +{ + const process::status status = fork_and_run(check_new_session); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__no_terminal); +ATF_TEST_CASE_BODY(isolate_child__no_terminal) +{ + const process::status status = fork_and_run(check_no_terminal); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__process_group); +ATF_TEST_CASE_BODY(isolate_child__process_group) +{ + const process::status status = fork_and_run(check_process_group); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__reset_umask); +ATF_TEST_CASE_BODY(isolate_child__reset_umask) +{ + const process::status status = fork_and_run(check_umask); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +/// Executes isolate_path() and compares the on-disk changes to expected values. +/// +/// \param unprivileged_user The user to pass to isolate_path; may be none. +/// \param exp_uid Expected UID or none to expect the old value. +/// \param exp_gid Expected GID or none to expect the old value. +static void +do_isolate_path_test(const optional< passwd::user >& unprivileged_user, + const optional< uid_t >& exp_uid, + const optional< gid_t >& exp_gid) +{ + const fs::path dir("dir"); + fs::mkdir(dir, 0755); + struct ::stat old_sb; + ATF_REQUIRE(::stat(dir.c_str(), &old_sb) != -1); + + process::isolate_path(unprivileged_user, dir); + + struct ::stat new_sb; + ATF_REQUIRE(::stat(dir.c_str(), &new_sb) != -1); + + if (exp_uid) + ATF_REQUIRE_EQ(exp_uid.get(), new_sb.st_uid); + else + ATF_REQUIRE_EQ(old_sb.st_uid, new_sb.st_uid); + + if (exp_gid) + ATF_REQUIRE_EQ(exp_gid.get(), new_sb.st_gid); + else + ATF_REQUIRE_EQ(old_sb.st_gid, new_sb.st_gid); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__no_user); +ATF_TEST_CASE_BODY(isolate_path__no_user) +{ + do_isolate_path_test(none, none, none); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__same_user); +ATF_TEST_CASE_BODY(isolate_path__same_user) +{ + do_isolate_path_test(utils::make_optional(passwd::current_user()), + none, none); +} + + +ATF_TEST_CASE(isolate_path__other_user_when_unprivileged); +ATF_TEST_CASE_HEAD(isolate_path__other_user_when_unprivileged) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(isolate_path__other_user_when_unprivileged) +{ + passwd::user user = passwd::current_user(); + user.uid += 1; + user.gid += 1; + + do_isolate_path_test(utils::make_optional(user), none, none); +} + + +ATF_TEST_CASE(isolate_path__drop_privileges); +ATF_TEST_CASE_HEAD(isolate_path__drop_privileges) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(isolate_path__drop_privileges) +{ + const passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + do_isolate_path_test(utils::make_optional(unprivileged_user), + utils::make_optional(unprivileged_user.uid), + utils::make_optional(unprivileged_user.gid)); +} + + +ATF_TEST_CASE(isolate_path__drop_privileges_only_uid); +ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_uid) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_uid) +{ + passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + unprivileged_user.gid = ::getgid(); + do_isolate_path_test(utils::make_optional(unprivileged_user), + utils::make_optional(unprivileged_user.uid), + none); +} + + +ATF_TEST_CASE(isolate_path__drop_privileges_only_gid); +ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_gid) +{ + set_md_var("require.config", "unprivileged-user"); + set_md_var("require.user", "root"); +} +ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_gid) +{ + passwd::user unprivileged_user = passwd::find_user_by_name( + get_config_var("unprivileged-user")); + unprivileged_user.uid = ::getuid(); + do_isolate_path_test(utils::make_optional(unprivileged_user), + none, + utils::make_optional(unprivileged_user.gid)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, isolate_child__clean_environment); + ATF_ADD_TEST_CASE(tcs, isolate_child__other_user_when_unprivileged); + ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges); + ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_uid); + ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_gid); + ATF_ADD_TEST_CASE(tcs, isolate_child__enable_core_dumps); + ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory); + ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory_failure); + ATF_ADD_TEST_CASE(tcs, isolate_child__new_session); + ATF_ADD_TEST_CASE(tcs, isolate_child__no_terminal); + ATF_ADD_TEST_CASE(tcs, isolate_child__process_group); + ATF_ADD_TEST_CASE(tcs, isolate_child__reset_umask); + + ATF_ADD_TEST_CASE(tcs, isolate_path__no_user); + ATF_ADD_TEST_CASE(tcs, isolate_path__same_user); + ATF_ADD_TEST_CASE(tcs, isolate_path__other_user_when_unprivileged); + ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges); + ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_uid); + ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_gid); +} diff --git a/utils/process/operations.cpp b/utils/process/operations.cpp new file mode 100644 index 000000000000..abcc49f2a443 --- /dev/null +++ b/utils/process/operations.cpp @@ -0,0 +1,273 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/operations.hpp" + +extern "C" { +#include +#include + +#include +#include +} + +#include +#include +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/process/exceptions.hpp" +#include "utils/process/system.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/interrupts.hpp" + +namespace fs = utils::fs; +namespace process = utils::process; +namespace signals = utils::signals; + + +/// Maximum number of arguments supported by exec. +/// +/// We need this limit to avoid having to allocate dynamic memory in the child +/// process to construct the arguments list, which would have side-effects in +/// the parent's memory if we use vfork(). +#define MAX_ARGS 128 + + +namespace { + + +/// Exception-based, type-improved version of wait(2). +/// +/// \return The PID of the terminated process and its termination status. +/// +/// \throw process::system_error If the call to wait(2) fails. +static process::status +safe_wait(void) +{ + LD("Waiting for any child process"); + int stat_loc; + const pid_t pid = ::wait(&stat_loc); + if (pid == -1) { + const int original_errno = errno; + throw process::system_error("Failed to wait for any child process", + original_errno); + } + return process::status(pid, stat_loc); +} + + +/// Exception-based, type-improved version of waitpid(2). +/// +/// \param pid The identifier of the process to wait for. +/// +/// \return The termination status of the process. +/// +/// \throw process::system_error If the call to waitpid(2) fails. +static process::status +safe_waitpid(const pid_t pid) +{ + LD(F("Waiting for pid=%s") % pid); + int stat_loc; + if (process::detail::syscall_waitpid(pid, &stat_loc, 0) == -1) { + const int original_errno = errno; + throw process::system_error(F("Failed to wait for PID %s") % pid, + original_errno); + } + return process::status(pid, stat_loc); +} + + +} // anonymous namespace + + +/// Executes an external binary and replaces the current process. +/// +/// This function must not use any of the logging features so that the output +/// of the subprocess is not "polluted" by our own messages. +/// +/// This function must also not affect the global state of the current process +/// as otherwise we would not be able to use vfork(). Only state stored in the +/// stack can be touched. +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +void +process::exec(const fs::path& program, const args_vector& args) throw() +{ + try { + exec_unsafe(program, args); + } catch (const system_error& error) { + // Error message already printed by exec_unsafe. + std::abort(); + } +} + + +/// Executes an external binary and replaces the current process. +/// +/// This differs from process::exec() in that this function reports errors +/// caused by the exec(2) system call to let the caller decide how to handle +/// them. +/// +/// This function must not use any of the logging features so that the output +/// of the subprocess is not "polluted" by our own messages. +/// +/// This function must also not affect the global state of the current process +/// as otherwise we would not be able to use vfork(). Only state stored in the +/// stack can be touched. +/// +/// \param program The binary to execute. +/// \param args The arguments to pass to the binary, without the program name. +/// +/// \throw system_error If the exec(2) call fails. +void +process::exec_unsafe(const fs::path& program, const args_vector& args) +{ + PRE(args.size() < MAX_ARGS); + int original_errno = 0; + try { + const char* argv[MAX_ARGS + 1]; + + argv[0] = program.c_str(); + for (args_vector::size_type i = 0; i < args.size(); i++) + argv[1 + i] = args[i].c_str(); + argv[1 + args.size()] = NULL; + + const int ret = ::execv(program.c_str(), + (char* const*)(unsigned long)(const void*)argv); + original_errno = errno; + INV(ret == -1); + std::cerr << "Failed to execute " << program << ": " + << std::strerror(original_errno) << "\n"; + } catch (const std::runtime_error& error) { + std::cerr << "Failed to execute " << program << ": " + << error.what() << "\n"; + std::abort(); + } catch (...) { + std::cerr << "Failed to execute " << program << "; got unexpected " + "exception during exec\n"; + std::abort(); + } + + // We must do this here to prevent our exception from being caught by the + // generic handlers above. + INV(original_errno != 0); + throw system_error("Failed to execute " + program.str(), original_errno); +} + + +/// Forcibly kills a process group started by us. +/// +/// This function is safe to call from an signal handler context. +/// +/// Pretty much all of our subprocesses run in their own process group so that +/// we can terminate them and thier children should we need to. Because of +/// this, the very first thing our subprocesses do is create a new process group +/// for themselves. +/// +/// The implication of the above is that simply issuing a killpg() call on the +/// process group is racy: if the subprocess has not yet had a chance to prepare +/// its own process group, then we will not be killing anything. To solve this, +/// we must also kill() the process group leader itself, and we must do so after +/// the call to killpg(). Doing this is safe because: 1) the process group must +/// have the same ID as the PID of the process that created it; and 2) we have +/// not yet issued a wait() call so we still own the PID. +/// +/// The sideffect of doing what we do here is that the process group leader may +/// receive a signal twice. But we don't care because we are forcibly +/// terminating the process group and none of the processes can controlledly +/// react to SIGKILL. +/// +/// \param pgid PID or process group ID to terminate. +void +process::terminate_group(const int pgid) +{ + (void)::killpg(pgid, SIGKILL); + (void)::kill(pgid, SIGKILL); +} + + +/// Terminates the current process reproducing the given status. +/// +/// The caller process is abruptly terminated. In particular, no output streams +/// are flushed, no destructors are called, and no atexit(2) handlers are run. +/// +/// \param status The status to "re-deliver" to the caller process. +void +process::terminate_self_with(const status& status) +{ + if (status.exited()) { + ::_exit(status.exitstatus()); + } else { + INV(status.signaled()); + (void)::kill(::getpid(), status.termsig()); + UNREACHABLE_MSG(F("Signal %s terminated %s but did not terminate " + "ourselves") % status.termsig() % status.dead_pid()); + } +} + + +/// Blocks to wait for completion of a subprocess. +/// +/// \param pid Identifier of the process to wait for. +/// +/// \return The termination status of the child process that terminated. +/// +/// \throw process::system_error If the call to wait(2) fails. +process::status +process::wait(const int pid) +{ + const process::status status = safe_waitpid(pid); + { + signals::interrupts_inhibiter inhibiter; + signals::remove_pid_to_kill(pid); + } + return status; +} + + +/// Blocks to wait for completion of any subprocess. +/// +/// \return The termination status of the child process that terminated. +/// +/// \throw process::system_error If the call to wait(2) fails. +process::status +process::wait_any(void) +{ + const process::status status = safe_wait(); + { + signals::interrupts_inhibiter inhibiter; + signals::remove_pid_to_kill(status.dead_pid()); + } + return status; +} diff --git a/utils/process/operations.hpp b/utils/process/operations.hpp new file mode 100644 index 000000000000..773f9d38bb74 --- /dev/null +++ b/utils/process/operations.hpp @@ -0,0 +1,56 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/operations.hpp +/// Collection of utilities for process management. + +#if !defined(UTILS_PROCESS_OPERATIONS_HPP) +#define UTILS_PROCESS_OPERATIONS_HPP + +#include "utils/process/operations_fwd.hpp" + +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace utils { +namespace process { + + +void exec(const utils::fs::path&, const args_vector&) throw() UTILS_NORETURN; +void exec_unsafe(const utils::fs::path&, const args_vector&) UTILS_NORETURN; +void terminate_group(const int); +void terminate_self_with(const status&) UTILS_NORETURN; +status wait(const int); +status wait_any(void); + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_OPERATIONS_HPP) diff --git a/utils/process/operations_fwd.hpp b/utils/process/operations_fwd.hpp new file mode 100644 index 000000000000..bd23fdc2c691 --- /dev/null +++ b/utils/process/operations_fwd.hpp @@ -0,0 +1,49 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/operations_fwd.hpp +/// Forward declarations for utils/process/operations.hpp + +#if !defined(UTILS_PROCESS_OPERATIONS_FWD_HPP) +#define UTILS_PROCESS_OPERATIONS_FWD_HPP + +#include +#include + +namespace utils { +namespace process { + + +/// Arguments to a program, without the program name. +typedef std::vector< std::string > args_vector; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_OPERATIONS_FWD_HPP) diff --git a/utils/process/operations_test.cpp b/utils/process/operations_test.cpp new file mode 100644 index 000000000000..e9c1ebb65a3d --- /dev/null +++ b/utils/process/operations_test.cpp @@ -0,0 +1,471 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/operations.hpp" + +extern "C" { +#include +#include + +#include +#include +} + +#include +#include + +#include + +#include "utils/defs.hpp" +#include "utils/format/containers.ipp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/process/exceptions.hpp" +#include "utils/process/status.hpp" +#include "utils/stacktrace.hpp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; +namespace process = utils::process; + + +namespace { + + +/// Type of the process::exec() and process::exec_unsafe() functions. +typedef void (*exec_function)(const fs::path&, const process::args_vector&); + + +/// Calculates the path to the test helpers binary. +/// +/// \param tc A pointer to the caller test case, needed to extract the value of +/// the "srcdir" property. +/// +/// \return The path to the helpers binary. +static fs::path +get_helpers(const atf::tests::tc* tc) +{ + return fs::path(tc->get_config_var("srcdir")) / "helpers"; +} + + +/// Body for a subprocess that runs exec(). +class child_exec { + /// Function to do the exec. + const exec_function _do_exec; + + /// Path to the binary to exec. + const fs::path& _program; + + /// Arguments to the binary, not including argv[0]. + const process::args_vector& _args; + +public: + /// Constructor. + /// + /// \param do_exec Function to do the exec. + /// \param program Path to the binary to exec. + /// \param args Arguments to the binary, not including argv[0]. + child_exec(const exec_function do_exec, const fs::path& program, + const process::args_vector& args) : + _do_exec(do_exec), _program(program), _args(args) + { + } + + /// Body for the subprocess. + void + operator()(void) + { + _do_exec(_program, _args); + } +}; + + +/// Body for a process that returns a specific exit code. +/// +/// \tparam ExitStatus The exit status for the subprocess. +template< int ExitStatus > +static void +child_exit(void) +{ + std::exit(ExitStatus); +} + + +static void suspend(void) UTILS_NORETURN; + + +/// Blocks a subprocess from running indefinitely. +static void +suspend(void) +{ + sigset_t mask; + sigemptyset(&mask); + for (;;) { + ::sigsuspend(&mask); + } +} + + +static void write_loop(const int) UTILS_NORETURN; + + +/// Provides an infinite stream of data in a subprocess. +/// +/// \param fd Descriptor into which to write. +static void +write_loop(const int fd) +{ + const int cookie = 0x12345678; + for (;;) { + std::cerr << "Still alive in PID " << ::getpid() << '\n'; + if (::write(fd, &cookie, sizeof(cookie)) != sizeof(cookie)) + std::exit(EXIT_FAILURE); + ::sleep(1); + } +} + + +} // anonymous namespace + + +/// Tests an exec function with no arguments. +/// +/// \param tc The calling test case. +/// \param do_exec The exec function to test. +static void +check_exec_no_args(const atf::tests::tc* tc, const exec_function do_exec) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + child_exec(do_exec, get_helpers(tc), process::args_vector()), + fs::path("stdout"), fs::path("stderr")); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_FAILURE, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("Must provide a helper name", "stderr")); +} + + +/// Tests an exec function with some arguments. +/// +/// \param tc The calling test case. +/// \param do_exec The exec function to test. +static void +check_exec_some_args(const atf::tests::tc* tc, const exec_function do_exec) +{ + process::args_vector args; + args.push_back("print-args"); + args.push_back("foo"); + args.push_back("bar"); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_exec(do_exec, get_helpers(tc), args), + fs::path("stdout"), fs::path("stderr")); + const process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("argv\\[1\\] = print-args", "stdout")); + ATF_REQUIRE(atf::utils::grep_file("argv\\[2\\] = foo", "stdout")); + ATF_REQUIRE(atf::utils::grep_file("argv\\[3\\] = bar", "stdout")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__no_args); +ATF_TEST_CASE_BODY(exec__no_args) +{ + check_exec_no_args(this, process::exec); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__some_args); +ATF_TEST_CASE_BODY(exec__some_args) +{ + check_exec_some_args(this, process::exec); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__fail); +ATF_TEST_CASE_BODY(exec__fail) +{ + utils::avoid_coredump_on_crash(); + + std::auto_ptr< process::child > child = process::child::fork_files( + child_exec(process::exec, fs::path("non-existent"), + process::args_vector()), + fs::path("stdout"), fs::path("stderr")); + const process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(atf::utils::grep_file("Failed to execute non-existent", + "stderr")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec_unsafe__no_args); +ATF_TEST_CASE_BODY(exec_unsafe__no_args) +{ + check_exec_no_args(this, process::exec_unsafe); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec_unsafe__some_args); +ATF_TEST_CASE_BODY(exec_unsafe__some_args) +{ + check_exec_some_args(this, process::exec_unsafe); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec_unsafe__fail); +ATF_TEST_CASE_BODY(exec_unsafe__fail) +{ + ATF_REQUIRE_THROW_RE( + process::system_error, "Failed to execute missing-program", + process::exec_unsafe(fs::path("missing-program"), + process::args_vector())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_group__setpgrp_executed); +ATF_TEST_CASE_BODY(terminate_group__setpgrp_executed) +{ + int first_fds[2], second_fds[2]; + ATF_REQUIRE(::pipe(first_fds) != -1); + ATF_REQUIRE(::pipe(second_fds) != -1); + + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + ::setpgid(::getpid(), ::getpid()); + const pid_t pid2 = ::fork(); + if (pid2 == -1) { + std::exit(EXIT_FAILURE); + } else if (pid2 == 0) { + ::close(first_fds[0]); + ::close(first_fds[1]); + ::close(second_fds[0]); + write_loop(second_fds[1]); + } + ::close(first_fds[0]); + ::close(second_fds[0]); + ::close(second_fds[1]); + write_loop(first_fds[1]); + } + ::close(first_fds[1]); + ::close(second_fds[1]); + + int dummy; + std::cerr << "Waiting for children to start\n"; + while (::read(first_fds[0], &dummy, sizeof(dummy)) <= 0 || + ::read(second_fds[0], &dummy, sizeof(dummy)) <= 0) { + // Wait for children to come up. + } + + process::terminate_group(pid); + std::cerr << "Waiting for children to die\n"; + while (::read(first_fds[0], &dummy, sizeof(dummy)) > 0 || + ::read(second_fds[0], &dummy, sizeof(dummy)) > 0) { + // Wait for children to terminate. If they don't, then the test case + // will time out. + } + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE(WTERMSIG(status) == SIGKILL); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_group__setpgrp_not_executed); +ATF_TEST_CASE_BODY(terminate_group__setpgrp_not_executed) +{ + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + // We do not call setgprp() here to simulate the race that happens when + // we invoke terminate_group on a process that has not yet had a chance + // to run the setpgrp() call. + suspend(); + } + + process::terminate_group(pid); + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE(WTERMSIG(status) == SIGKILL); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_self_with__exitstatus); +ATF_TEST_CASE_BODY(terminate_self_with__exitstatus) +{ + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + const process::status status = process::status::fake_exited(123); + process::terminate_self_with(status); + } + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE(WEXITSTATUS(status) == 123); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_self_with__termsig); +ATF_TEST_CASE_BODY(terminate_self_with__termsig) +{ + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + const process::status status = process::status::fake_signaled( + SIGKILL, false); + process::terminate_self_with(status); + } + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE(WTERMSIG(status) == SIGKILL); + ATF_REQUIRE(!WCOREDUMP(status)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(terminate_self_with__termsig_and_core); +ATF_TEST_CASE_BODY(terminate_self_with__termsig_and_core) +{ + utils::prepare_coredump_test(this); + + const pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + const process::status status = process::status::fake_signaled( + SIGABRT, true); + process::terminate_self_with(status); + } + + int status; + ATF_REQUIRE(::wait(&status) != -1); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE(WTERMSIG(status) == SIGABRT); + ATF_REQUIRE(WCOREDUMP(status)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait__ok); +ATF_TEST_CASE_BODY(wait__ok) +{ + std::auto_ptr< process::child > child = process::child::fork_capture( + child_exit< 15 >); + const pid_t pid = child->pid(); + child.reset(); // Ensure there is no conflict between destructor and wait. + + const process::status status = process::wait(pid); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(15, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait__fail); +ATF_TEST_CASE_BODY(wait__fail) +{ + ATF_REQUIRE_THROW(process::system_error, process::wait(1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait_any__one); +ATF_TEST_CASE_BODY(wait_any__one) +{ + process::child::fork_capture(child_exit< 15 >); + + const process::status status = process::wait_any(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(15, status.exitstatus()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait_any__many); +ATF_TEST_CASE_BODY(wait_any__many) +{ + process::child::fork_capture(child_exit< 15 >); + process::child::fork_capture(child_exit< 30 >); + process::child::fork_capture(child_exit< 45 >); + + std::set< int > exit_codes; + for (int i = 0; i < 3; i++) { + const process::status status = process::wait_any(); + ATF_REQUIRE(status.exited()); + exit_codes.insert(status.exitstatus()); + } + + std::set< int > exp_exit_codes; + exp_exit_codes.insert(15); + exp_exit_codes.insert(30); + exp_exit_codes.insert(45); + ATF_REQUIRE_EQ(exp_exit_codes, exit_codes); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(wait_any__none_is_failure); +ATF_TEST_CASE_BODY(wait_any__none_is_failure) +{ + try { + const process::status status = process::wait_any(); + fail("Expected exception but none raised"); + } catch (const process::system_error& e) { + ATF_REQUIRE(atf::utils::grep_string("Failed to wait", e.what())); + ATF_REQUIRE_EQ(ECHILD, e.original_errno()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, exec__no_args); + ATF_ADD_TEST_CASE(tcs, exec__some_args); + ATF_ADD_TEST_CASE(tcs, exec__fail); + + ATF_ADD_TEST_CASE(tcs, exec_unsafe__no_args); + ATF_ADD_TEST_CASE(tcs, exec_unsafe__some_args); + ATF_ADD_TEST_CASE(tcs, exec_unsafe__fail); + + ATF_ADD_TEST_CASE(tcs, terminate_group__setpgrp_executed); + ATF_ADD_TEST_CASE(tcs, terminate_group__setpgrp_not_executed); + + ATF_ADD_TEST_CASE(tcs, terminate_self_with__exitstatus); + ATF_ADD_TEST_CASE(tcs, terminate_self_with__termsig); + ATF_ADD_TEST_CASE(tcs, terminate_self_with__termsig_and_core); + + ATF_ADD_TEST_CASE(tcs, wait__ok); + ATF_ADD_TEST_CASE(tcs, wait__fail); + + ATF_ADD_TEST_CASE(tcs, wait_any__one); + ATF_ADD_TEST_CASE(tcs, wait_any__many); + ATF_ADD_TEST_CASE(tcs, wait_any__none_is_failure); +} diff --git a/utils/process/status.cpp b/utils/process/status.cpp new file mode 100644 index 000000000000..a3cea8e09ebd --- /dev/null +++ b/utils/process/status.cpp @@ -0,0 +1,200 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/status.hpp" + +extern "C" { +#include +} + +#include "utils/format/macros.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace process = utils::process; + +using utils::none; +using utils::optional; + +#if !defined(WCOREDUMP) +# define WCOREDUMP(x) false +#endif + + +/// Constructs a new status object based on the status value of waitpid(2). +/// +/// \param dead_pid_ The PID of the process this status belonged to. +/// \param stat_loc The status value returnd by waitpid(2). +process::status::status(const int dead_pid_, int stat_loc) : + _dead_pid(dead_pid_), + _exited(WIFEXITED(stat_loc) ? + optional< int >(WEXITSTATUS(stat_loc)) : none), + _signaled(WIFSIGNALED(stat_loc) ? + optional< std::pair< int, bool > >( + std::make_pair(WTERMSIG(stat_loc), WCOREDUMP(stat_loc))) : + none) +{ +} + + +/// Constructs a new status object based on fake values. +/// +/// \param exited_ If not none, specifies the exit status of the program. +/// \param signaled_ If not none, specifies the termination signal and whether +/// the process dumped core or not. +process::status::status(const optional< int >& exited_, + const optional< std::pair< int, bool > >& signaled_) : + _dead_pid(-1), + _exited(exited_), + _signaled(signaled_) +{ +} + + +/// Constructs a new status object based on a fake exit status. +/// +/// \param exitstatus_ The exit code of the process. +/// +/// \return A status object with fake data. +process::status +process::status::fake_exited(const int exitstatus_) +{ + return status(utils::make_optional(exitstatus_), none); +} + + +/// Constructs a new status object based on a fake exit status. +/// +/// \param termsig_ The termination signal of the process. +/// \param coredump_ Whether the process dumped core or not. +/// +/// \return A status object with fake data. +process::status +process::status::fake_signaled(const int termsig_, const bool coredump_) +{ + return status(none, utils::make_optional(std::make_pair(termsig_, + coredump_))); +} + + +/// Returns the PID of the process this status was taken from. +/// +/// Please note that the process is already dead and gone from the system. This +/// PID can only be used for informational reasons and not to address the +/// process in any way. +/// +/// \return The PID of the original process. +int +process::status::dead_pid(void) const +{ + return _dead_pid; +} + + +/// Returns whether the process exited cleanly or not. +/// +/// \return True if the process exited cleanly, false otherwise. +bool +process::status::exited(void) const +{ + return _exited; +} + + +/// Returns the exit code of the process. +/// +/// \pre The process must have exited cleanly (i.e. exited() must be true). +/// +/// \return The exit code. +int +process::status::exitstatus(void) const +{ + PRE(exited()); + return _exited.get(); +} + + +/// Returns whether the process terminated due to a signal or not. +/// +/// \return True if the process terminated due to a signal, false otherwise. +bool +process::status::signaled(void) const +{ + return _signaled; +} + + +/// Returns the signal that terminated the process. +/// +/// \pre The process must have terminated by a signal (i.e. signaled() must be +/// true. +/// +/// \return The signal number. +int +process::status::termsig(void) const +{ + PRE(signaled()); + return _signaled.get().first; +} + + +/// Returns whether the process core dumped or not. +/// +/// This functionality may be unsupported in some platforms. In such cases, +/// this method returns false unconditionally. +/// +/// \pre The process must have terminated by a signal (i.e. signaled() must be +/// true. +/// +/// \return True if the process dumped core, false otherwise. +bool +process::status::coredump(void) const +{ + PRE(signaled()); + return _signaled.get().second; +} + + +/// Injects the object into a stream. +/// +/// \param output The stream into which to inject the object. +/// \param status The object to format. +/// +/// \return The output stream. +std::ostream& +process::operator<<(std::ostream& output, const status& status) +{ + if (status.exited()) { + output << F("status{exitstatus=%s}") % status.exitstatus(); + } else { + INV(status.signaled()); + output << F("status{termsig=%s, coredump=%s}") % status.termsig() % + status.coredump(); + } + return output; +} diff --git a/utils/process/status.hpp b/utils/process/status.hpp new file mode 100644 index 000000000000..b14ff55c01a2 --- /dev/null +++ b/utils/process/status.hpp @@ -0,0 +1,84 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/status.hpp +/// Provides the utils::process::status class. + +#if !defined(UTILS_PROCESS_STATUS_HPP) +#define UTILS_PROCESS_STATUS_HPP + +#include "utils/process/status_fwd.hpp" + +#include +#include + +#include "utils/optional.ipp" + +namespace utils { +namespace process { + + +/// Representation of the termination status of a process. +class status { + /// The PID of the process that generated this status. + /// + /// Note that the process has exited already and been awaited for, so the + /// PID cannot be used to address the process. + int _dead_pid; + + /// The exit status of the process, if it exited cleanly. + optional< int > _exited; + + /// The signal that terminated the program, if any, and if it dumped core. + optional< std::pair< int, bool > > _signaled; + + status(const optional< int >&, const optional< std::pair< int, bool > >&); + +public: + status(const int, int); + static status fake_exited(const int); + static status fake_signaled(const int, const bool); + + int dead_pid(void) const; + + bool exited(void) const; + int exitstatus(void) const; + + bool signaled(void) const; + int termsig(void) const; + bool coredump(void) const; +}; + + +std::ostream& operator<<(std::ostream&, const status&); + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_STATUS_HPP) diff --git a/utils/process/status_fwd.hpp b/utils/process/status_fwd.hpp new file mode 100644 index 000000000000..3a14683dc15c --- /dev/null +++ b/utils/process/status_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/status_fwd.hpp +/// Forward declarations for utils/process/status.hpp + +#if !defined(UTILS_PROCESS_STATUS_FWD_HPP) +#define UTILS_PROCESS_STATUS_FWD_HPP + +namespace utils { +namespace process { + + +class status; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_STATUS_FWD_HPP) diff --git a/utils/process/status_test.cpp b/utils/process/status_test.cpp new file mode 100644 index 000000000000..5a3e19eeaf18 --- /dev/null +++ b/utils/process/status_test.cpp @@ -0,0 +1,209 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/status.hpp" + +extern "C" { +#include + +#include +#include +} + +#include + +#include + +#include "utils/test_utils.ipp" + +using utils::process::status; + + +namespace { + + +/// Body of a subprocess that exits with a particular exit status. +/// +/// \tparam ExitStatus The status to exit with. +template< int ExitStatus > +void child_exit(void) +{ + std::exit(ExitStatus); +} + + +/// Body of a subprocess that sends a particular signal to itself. +/// +/// \tparam Signo The signal to send to self. +template< int Signo > +void child_signal(void) +{ + ::kill(::getpid(), Signo); +} + + +/// Spawns a process and waits for completion. +/// +/// \param hook The function to run within the child. Should not return. +/// +/// \return The termination status of the spawned subprocess. +status +fork_and_wait(void (*hook)(void)) +{ + pid_t pid = ::fork(); + ATF_REQUIRE(pid != -1); + if (pid == 0) { + hook(); + std::abort(); + } else { + int stat_loc; + ATF_REQUIRE(::waitpid(pid, &stat_loc, 0) != -1); + const status s = status(pid, stat_loc); + ATF_REQUIRE_EQ(pid, s.dead_pid()); + return s; + } +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(fake_exited) +ATF_TEST_CASE_BODY(fake_exited) +{ + const status fake = status::fake_exited(123); + ATF_REQUIRE_EQ(-1, fake.dead_pid()); + ATF_REQUIRE(fake.exited()); + ATF_REQUIRE_EQ(123, fake.exitstatus()); + ATF_REQUIRE(!fake.signaled()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(fake_signaled) +ATF_TEST_CASE_BODY(fake_signaled) +{ + const status fake = status::fake_signaled(567, true); + ATF_REQUIRE_EQ(-1, fake.dead_pid()); + ATF_REQUIRE(!fake.exited()); + ATF_REQUIRE(fake.signaled()); + ATF_REQUIRE_EQ(567, fake.termsig()); + ATF_REQUIRE(fake.coredump()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__exitstatus); +ATF_TEST_CASE_BODY(output__exitstatus) +{ + const status fake = status::fake_exited(123); + std::ostringstream str; + str << fake; + ATF_REQUIRE_EQ("status{exitstatus=123}", str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__signaled_without_core); +ATF_TEST_CASE_BODY(output__signaled_without_core) +{ + const status fake = status::fake_signaled(8, false); + std::ostringstream str; + str << fake; + ATF_REQUIRE_EQ("status{termsig=8, coredump=false}", str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(output__signaled_with_core); +ATF_TEST_CASE_BODY(output__signaled_with_core) +{ + const status fake = status::fake_signaled(9, true); + std::ostringstream str; + str << fake; + ATF_REQUIRE_EQ("status{termsig=9, coredump=true}", str.str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__exited); +ATF_TEST_CASE_BODY(integration__exited) +{ + const status exit_success = fork_and_wait(child_exit< EXIT_SUCCESS >); + ATF_REQUIRE(exit_success.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, exit_success.exitstatus()); + ATF_REQUIRE(!exit_success.signaled()); + + const status exit_failure = fork_and_wait(child_exit< EXIT_FAILURE >); + ATF_REQUIRE(exit_failure.exited()); + ATF_REQUIRE_EQ(EXIT_FAILURE, exit_failure.exitstatus()); + ATF_REQUIRE(!exit_failure.signaled()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__signaled); +ATF_TEST_CASE_BODY(integration__signaled) +{ + const status sigterm = fork_and_wait(child_signal< SIGTERM >); + ATF_REQUIRE(!sigterm.exited()); + ATF_REQUIRE(sigterm.signaled()); + ATF_REQUIRE_EQ(SIGTERM, sigterm.termsig()); + ATF_REQUIRE(!sigterm.coredump()); + + const status sigkill = fork_and_wait(child_signal< SIGKILL >); + ATF_REQUIRE(!sigkill.exited()); + ATF_REQUIRE(sigkill.signaled()); + ATF_REQUIRE_EQ(SIGKILL, sigkill.termsig()); + ATF_REQUIRE(!sigkill.coredump()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__coredump); +ATF_TEST_CASE_BODY(integration__coredump) +{ + utils::prepare_coredump_test(this); + + const status coredump = fork_and_wait(child_signal< SIGQUIT >); + ATF_REQUIRE(!coredump.exited()); + ATF_REQUIRE(coredump.signaled()); + ATF_REQUIRE_EQ(SIGQUIT, coredump.termsig()); +#if !defined(WCOREDUMP) + expect_fail("Platform does not support checking for coredump"); +#endif + ATF_REQUIRE(coredump.coredump()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, fake_exited); + ATF_ADD_TEST_CASE(tcs, fake_signaled); + + ATF_ADD_TEST_CASE(tcs, output__exitstatus); + ATF_ADD_TEST_CASE(tcs, output__signaled_without_core); + ATF_ADD_TEST_CASE(tcs, output__signaled_with_core); + + ATF_ADD_TEST_CASE(tcs, integration__exited); + ATF_ADD_TEST_CASE(tcs, integration__signaled); + ATF_ADD_TEST_CASE(tcs, integration__coredump); +} diff --git a/utils/process/system.cpp b/utils/process/system.cpp new file mode 100644 index 000000000000..ac41ddb7daa7 --- /dev/null +++ b/utils/process/system.cpp @@ -0,0 +1,59 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/system.hpp" + +extern "C" { +#include +#include + +#include +#include +} + +namespace detail = utils::process::detail; + + +/// Indirection to execute the dup2(2) system call. +int (*detail::syscall_dup2)(const int, const int) = ::dup2; + + +/// Indirection to execute the fork(2) system call. +pid_t (*detail::syscall_fork)(void) = ::fork; + + +/// Indirection to execute the open(2) system call. +int (*detail::syscall_open)(const char*, const int, ...) = ::open; + + +/// Indirection to execute the pipe(2) system call. +int (*detail::syscall_pipe)(int[2]) = ::pipe; + + +/// Indirection to execute the waitpid(2) system call. +pid_t (*detail::syscall_waitpid)(const pid_t, int*, const int) = ::waitpid; diff --git a/utils/process/system.hpp b/utils/process/system.hpp new file mode 100644 index 000000000000..a794876f3579 --- /dev/null +++ b/utils/process/system.hpp @@ -0,0 +1,66 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/system.hpp +/// Indirection to perform system calls. +/// +/// The indirections exposed in this file are provided to allow unit-testing of +/// particular system behaviors (e.g. failures). The caller of a routine in +/// this library is allowed, for testing purposes only, to explicitly replace +/// the pointers in this file with custom functions to inject a particular +/// behavior into the library code. +/// +/// Do not include this header from other header files. +/// +/// It may be nice to go one step further and completely abstract the library +/// functions in here to provide exception-based error reporting. + +#if !defined(UTILS_PROCESS_SYSTEM_HPP) +#define UTILS_PROCESS_SYSTEM_HPP + +extern "C" { +#include +} + +namespace utils { +namespace process { +namespace detail { + + +extern int (*syscall_dup2)(const int, const int); +extern pid_t (*syscall_fork)(void); +extern int (*syscall_open)(const char*, const int, ...); +extern int (*syscall_pipe)(int[2]); +extern pid_t (*syscall_waitpid)(const pid_t, int*, const int); + + +} // namespace detail +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_SYSTEM_HPP) diff --git a/utils/process/systembuf.cpp b/utils/process/systembuf.cpp new file mode 100644 index 000000000000..661b336221ac --- /dev/null +++ b/utils/process/systembuf.cpp @@ -0,0 +1,152 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/systembuf.hpp" + +extern "C" { +#include +} + +#include "utils/auto_array.ipp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" + +using utils::process::systembuf; + + +/// Private implementation fields for systembuf. +struct systembuf::impl : utils::noncopyable { + /// File descriptor attached to the systembuf. + int _fd; + + /// Size of the _read_buf and _write_buf buffers. + std::size_t _bufsize; + + /// In-memory buffer for read operations. + utils::auto_array< char > _read_buf; + + /// In-memory buffer for write operations. + utils::auto_array< char > _write_buf; + + /// Initializes private implementation data. + /// + /// \param fd The file descriptor. + /// \param bufsize The size of the created read and write buffers. + impl(const int fd, const std::size_t bufsize) : + _fd(fd), + _bufsize(bufsize), + _read_buf(new char[bufsize]), + _write_buf(new char[bufsize]) + { + } +}; + + +/// Constructs a new systembuf based on an open file descriptor. +/// +/// This grabs ownership of the file descriptor. +/// +/// \param fd The file descriptor to wrap. Must be open and valid. +/// \param bufsize The size to use for the internal read/write buffers. +systembuf::systembuf(const int fd, std::size_t bufsize) : + _pimpl(new impl(fd, bufsize)) +{ + setp(_pimpl->_write_buf.get(), _pimpl->_write_buf.get() + _pimpl->_bufsize); +} + + +/// Destroys a systembuf object. +/// +/// \post The file descriptor attached to this systembuf is closed. +systembuf::~systembuf(void) +{ + ::close(_pimpl->_fd); +} + + +/// Reads new data when the systembuf read buffer underflows. +/// +/// \return The new character to be read, or EOF if no more. +systembuf::int_type +systembuf::underflow(void) +{ + PRE(gptr() >= egptr()); + + bool ok; + ssize_t cnt = ::read(_pimpl->_fd, _pimpl->_read_buf.get(), + _pimpl->_bufsize); + ok = (cnt != -1 && cnt != 0); + + if (!ok) + return traits_type::eof(); + else { + setg(_pimpl->_read_buf.get(), _pimpl->_read_buf.get(), + _pimpl->_read_buf.get() + cnt); + return traits_type::to_int_type(*gptr()); + } +} + + +/// Writes data to the file descriptor when the write buffer overflows. +/// +/// \param c The character that causes the overflow. +/// +/// \return EOF if error, some other value for success. +/// +/// \throw something TODO(jmmv): According to the documentation, it is OK for +/// this method to throw in case of errors. Revisit this code to see if we +/// can do better. +systembuf::int_type +systembuf::overflow(int c) +{ + PRE(pptr() >= epptr()); + if (sync() == -1) + return traits_type::eof(); + if (!traits_type::eq_int_type(c, traits_type::eof())) { + traits_type::assign(*pptr(), c); + pbump(1); + } + return traits_type::not_eof(c); +} + + +/// Synchronizes the stream with the file descriptor. +/// +/// \return 0 on success, -1 on error. +int +systembuf::sync(void) +{ + ssize_t cnt = pptr() - pbase(); + + bool ok; + ok = ::write(_pimpl->_fd, pbase(), cnt) == cnt; + + if (ok) + pbump(-cnt); + return ok ? 0 : -1; +} diff --git a/utils/process/systembuf.hpp b/utils/process/systembuf.hpp new file mode 100644 index 000000000000..c89c9108dc4b --- /dev/null +++ b/utils/process/systembuf.hpp @@ -0,0 +1,71 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/systembuf.hpp +/// Provides the utils::process::systembuf class. + +#if !defined(UTILS_PROCESS_SYSTEMBUF_HPP) +#define UTILS_PROCESS_SYSTEMBUF_HPP + +#include "utils/process/systembuf_fwd.hpp" + +#include +#include +#include + +#include "utils/noncopyable.hpp" + +namespace utils { +namespace process { + + +/// A std::streambuf implementation for raw file descriptors. +/// +/// This class grabs ownership of the file descriptor. I.e. when the class is +/// destroyed, the file descriptor is closed unconditionally. +class systembuf : public std::streambuf, noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +protected: + int_type underflow(void); + int_type overflow(int); + int sync(void); + +public: + explicit systembuf(const int, std::size_t = 8192); + ~systembuf(void); +}; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_SYSTEMBUF_HPP) diff --git a/utils/process/systembuf_fwd.hpp b/utils/process/systembuf_fwd.hpp new file mode 100644 index 000000000000..b3e341336b1d --- /dev/null +++ b/utils/process/systembuf_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/systembuf_fwd.hpp +/// Forward declarations for utils/process/systembuf.hpp + +#if !defined(UTILS_PROCESS_SYSTEMBUF_FWD_HPP) +#define UTILS_PROCESS_SYSTEMBUF_FWD_HPP + +namespace utils { +namespace process { + + +class systembuf; + + +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_SYSTEMBUF_FWD_HPP) diff --git a/utils/process/systembuf_test.cpp b/utils/process/systembuf_test.cpp new file mode 100644 index 000000000000..ef9ff1930cf6 --- /dev/null +++ b/utils/process/systembuf_test.cpp @@ -0,0 +1,166 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/systembuf.hpp" + +extern "C" { +#include + +#include +#include +} + +#include + +#include + +using utils::process::systembuf; + + +static void +check_data(std::istream& is, std::size_t length) +{ + char ch = 'A', chr; + std::size_t cnt = 0; + while (is >> chr) { + ATF_REQUIRE_EQ(ch, chr); + if (ch == 'Z') + ch = 'A'; + else + ch++; + cnt++; + } + ATF_REQUIRE_EQ(cnt, length); +} + + +static void +write_data(std::ostream& os, std::size_t length) +{ + char ch = 'A'; + for (std::size_t i = 0; i < length; i++) { + os << ch; + if (ch == 'Z') + ch = 'A'; + else + ch++; + } + os.flush(); +} + + +static void +test_read(std::size_t length, std::size_t bufsize) +{ + std::ofstream f("test_read.txt"); + write_data(f, length); + f.close(); + + int fd = ::open("test_read.txt", O_RDONLY); + ATF_REQUIRE(fd != -1); + systembuf sb(fd, bufsize); + std::istream is(&sb); + check_data(is, length); + ::close(fd); + ::unlink("test_read.txt"); +} + + +static void +test_write(std::size_t length, std::size_t bufsize) +{ + int fd = ::open("test_write.txt", O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + ATF_REQUIRE(fd != -1); + systembuf sb(fd, bufsize); + std::ostream os(&sb); + write_data(os, length); + ::close(fd); + + std::ifstream is("test_write.txt"); + check_data(is, length); + is.close(); + ::unlink("test_write.txt"); +} + + +ATF_TEST_CASE(short_read); +ATF_TEST_CASE_HEAD(short_read) +{ + set_md_var("descr", "Tests that a short read (one that fits in the " + "internal buffer) works when using systembuf"); +} +ATF_TEST_CASE_BODY(short_read) +{ + test_read(64, 1024); +} + + +ATF_TEST_CASE(long_read); +ATF_TEST_CASE_HEAD(long_read) +{ + set_md_var("descr", "Tests that a long read (one that does not fit in " + "the internal buffer) works when using systembuf"); +} +ATF_TEST_CASE_BODY(long_read) +{ + test_read(64 * 1024, 1024); +} + + +ATF_TEST_CASE(short_write); +ATF_TEST_CASE_HEAD(short_write) +{ + set_md_var("descr", "Tests that a short write (one that fits in the " + "internal buffer) works when using systembuf"); +} +ATF_TEST_CASE_BODY(short_write) +{ + test_write(64, 1024); +} + + +ATF_TEST_CASE(long_write); +ATF_TEST_CASE_HEAD(long_write) +{ + set_md_var("descr", "Tests that a long write (one that does not fit " + "in the internal buffer) works when using systembuf"); +} +ATF_TEST_CASE_BODY(long_write) +{ + test_write(64 * 1024, 1024); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, short_read); + ATF_ADD_TEST_CASE(tcs, long_read); + ATF_ADD_TEST_CASE(tcs, short_write); + ATF_ADD_TEST_CASE(tcs, long_write); +} diff --git a/utils/sanity.cpp b/utils/sanity.cpp new file mode 100644 index 000000000000..7978167d83ff --- /dev/null +++ b/utils/sanity.cpp @@ -0,0 +1,194 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sanity.hpp" + +#if defined(HAVE_CONFIG_H) +#include "config.h" +#endif + +extern "C" { +#include +#include +} + +#include +#include +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" + + +namespace { + + +/// List of fatal signals to be intercepted by the sanity code. +/// +/// The tests hardcode this list; update them whenever the list gets updated. +static int fatal_signals[] = { SIGABRT, SIGBUS, SIGSEGV, 0 }; + + +/// The path to the log file to report on crashes. Be aware that this is empty +/// until install_crash_handlers() is called. +static std::string logfile; + + +/// Prints a message to stderr. +/// +/// Note that this runs from a signal handler. Calling write() is OK. +/// +/// \param message The message to print. +static void +err_write(const std::string& message) +{ + if (::write(STDERR_FILENO, message.c_str(), message.length()) == -1) { + // We are crashing. If ::write fails, there is not much we could do, + // specially considering that we are running within a signal handler. + // Just ignore the error. + } +} + + +/// The crash handler for fatal signals. +/// +/// The sole purpose of this is to print some informational data before +/// reraising the original signal. +/// +/// \param signo The received signal. +static void +crash_handler(const int signo) +{ + PRE(!logfile.empty()); + + err_write(F("*** Fatal signal %s received\n") % signo); + err_write(F("*** Log file is %s\n") % logfile); + err_write(F("*** Please report this problem to %s detailing what you were " + "doing before the crash happened; if possible, include the log " + "file mentioned above\n") % PACKAGE_BUGREPORT); + + /// The handler is installed with SA_RESETHAND, so this is safe to do. We + /// really want to call the default handler to generate any possible core + /// dumps. + ::kill(::getpid(), signo); +} + + +/// Installs a handler for a fatal signal representing a crash. +/// +/// When the specified signal is captured, the crash_handler() will be called to +/// print some informational details to the user and, later, the signal will be +/// redelivered using the default handler to obtain a core dump. +/// +/// \param signo The fatal signal for which to install a handler. +static void +install_one_crash_handler(const int signo) +{ + struct ::sigaction sa; + sa.sa_handler = crash_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESETHAND; + + if (::sigaction(signo, &sa, NULL) == -1) { + const int original_errno = errno; + LW(F("Could not install crash handler for signal %s: %s") % + signo % std::strerror(original_errno)); + } else + LD(F("Installed crash handler for signal %s") % signo); +} + + +/// Returns a textual representation of an assertion type. +/// +/// The textual representation is user facing. +/// +/// \param type The type of the assertion. If the type is unknown for whatever +/// reason, a special message is returned. The code cannot abort in such a +/// case because this code is dealing for assertion errors. +/// +/// \return A textual description of the assertion type. +static std::string +format_type(const utils::assert_type type) +{ + switch (type) { + case utils::invariant: return "Invariant check failed"; + case utils::postcondition: return "Postcondition check failed"; + case utils::precondition: return "Precondition check failed"; + case utils::unreachable: return "Unreachable point reached"; + default: return "UNKNOWN ASSERTION TYPE"; + } +} + + +} // anonymous namespace + + +/// Raises an assertion error. +/// +/// This function prints information about the assertion failure and terminates +/// execution immediately by calling std::abort(). This ensures a coredump so +/// that the failure can be analyzed later. +/// +/// \param type The assertion type; this influences the printed message. +/// \param file The file in which the assertion failed. +/// \param line The line in which the assertion failed. +/// \param message The failure message associated to the condition. +void +utils::sanity_failure(const assert_type type, const char* file, + const size_t line, const std::string& message) +{ + std::cerr << "*** " << file << ":" << line << ": " << format_type(type); + if (!message.empty()) + std::cerr << ": " << message << "\n"; + else + std::cerr << "\n"; + std::abort(); +} + + +/// Installs persistent handlers for crash signals. +/// +/// Should be called at the very beginning of the execution of the program to +/// ensure that a signal handler for fatal crash signals is installed. +/// +/// \pre The function has not been called before. +/// +/// \param logfile_ The path to the log file to report during a crash. +void +utils::install_crash_handlers(const std::string& logfile_) +{ + static bool installed = false; + PRE(!installed); + logfile = logfile_; + + for (const int* iter = &fatal_signals[0]; *iter != 0; iter++) + install_one_crash_handler(*iter); + + installed = true; +} diff --git a/utils/sanity.hpp b/utils/sanity.hpp new file mode 100644 index 000000000000..6b126f984999 --- /dev/null +++ b/utils/sanity.hpp @@ -0,0 +1,183 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sanity.hpp +/// +/// Set of macros that replace the standard assert macro with more semantical +/// expressivity and meaningful diagnostics. Code should never use assert +/// directly. +/// +/// In general, the checks performed by the macros in this code are only +/// executed if the code is built with debugging support (that is, if the NDEBUG +/// macro is NOT defined). + +#if !defined(UTILS_SANITY_HPP) +#define UTILS_SANITY_HPP + +#include "utils/sanity_fwd.hpp" + +#include +#include + +#include "utils/defs.hpp" + +namespace utils { + + +void sanity_failure(const assert_type, const char*, const size_t, + const std::string&) UTILS_NORETURN; + + +void install_crash_handlers(const std::string&); + + +} // namespace utils + + +/// \def _UTILS_ASSERT(type, expr, message) +/// \brief Performs an assertion check. +/// +/// This macro is internal and should not be used directly. +/// +/// Ensures that the given expression expr is true and, if not, terminates +/// execution by calling utils::sanity_failure(). The check is only performed +/// in debug builds. +/// +/// \param type The assertion type as defined by assert_type. +/// \param expr A boolean expression. +/// \param message A string describing the nature of the error. +#if !defined(NDEBUG) +# define _UTILS_ASSERT(type, expr, message) \ + do { \ + if (!(expr)) \ + utils::sanity_failure(type, __FILE__, __LINE__, message); \ + } while (0) +#else // defined(NDEBUG) +# define _UTILS_ASSERT(type, expr, message) do {} while (0) +#endif // !defined(NDEBUG) + + +/// Ensures that an invariant holds. +/// +/// If the invariant does not hold, execution is immediately terminated. The +/// check is only performed in debug builds. +/// +/// The error message printed by this macro is a textual representation of the +/// boolean condition. If you want to provide a custom error message, use +/// INV_MSG instead. +/// +/// \param expr A boolean expression describing the invariant. +#define INV(expr) _UTILS_ASSERT(utils::invariant, expr, #expr) + + +/// Ensures that an invariant holds using a custom error message. +/// +/// If the invariant does not hold, execution is immediately terminated. The +/// check is only performed in debug builds. +/// +/// \param expr A boolean expression describing the invariant. +/// \param msg The error message to print if the condition is false. +#define INV_MSG(expr, msg) _UTILS_ASSERT(utils::invariant, expr, msg) + + +/// Ensures that a precondition holds. +/// +/// If the precondition does not hold, execution is immediately terminated. The +/// check is only performed in debug builds. +/// +/// The error message printed by this macro is a textual representation of the +/// boolean condition. If you want to provide a custom error message, use +/// PRE_MSG instead. +/// +/// \param expr A boolean expression describing the precondition. +#define PRE(expr) _UTILS_ASSERT(utils::precondition, expr, #expr) + + +/// Ensures that a precondition holds using a custom error message. +/// +/// If the precondition does not hold, execution is immediately terminated. The +/// check is only performed in debug builds. +/// +/// \param expr A boolean expression describing the precondition. +/// \param msg The error message to print if the condition is false. +#define PRE_MSG(expr, msg) _UTILS_ASSERT(utils::precondition, expr, msg) + + +/// Ensures that an postcondition holds. +/// +/// If the postcondition does not hold, execution is immediately terminated. +/// The check is only performed in debug builds. +/// +/// The error message printed by this macro is a textual representation of the +/// boolean condition. If you want to provide a custom error message, use +/// POST_MSG instead. +/// +/// \param expr A boolean expression describing the postcondition. +#define POST(expr) _UTILS_ASSERT(utils::postcondition, expr, #expr) + + +/// Ensures that a postcondition holds using a custom error message. +/// +/// If the postcondition does not hold, execution is immediately terminated. +/// The check is only performed in debug builds. +/// +/// \param expr A boolean expression describing the postcondition. +/// \param msg The error message to print if the condition is false. +#define POST_MSG(expr, msg) _UTILS_ASSERT(utils::postcondition, expr, msg) + + +/// Ensures that a code path is not reached. +/// +/// If the code path in which this macro is located is reached, execution is +/// immediately terminated. Given that such a condition is critical for the +/// execution of the program (and to prevent build failures due to some code +/// paths not initializing variables, for example), this condition is fatal both +/// in debug and production builds. +/// +/// The error message printed by this macro is a textual representation of the +/// boolean condition. If you want to provide a custom error message, use +/// POST_MSG instead. +#define UNREACHABLE UNREACHABLE_MSG("") + + +/// Ensures that a code path is not reached using a custom error message. +/// +/// If the code path in which this macro is located is reached, execution is +/// immediately terminated. Given that such a condition is critical for the +/// execution of the program (and to prevent build failures due to some code +/// paths not initializing variables, for example), this condition is fatal both +/// in debug and production builds. +/// +/// \param msg The error message to print if the condition is false. +#define UNREACHABLE_MSG(msg) \ + do { \ + utils::sanity_failure(utils::unreachable, __FILE__, __LINE__, msg); \ + } while (0) + + +#endif // !defined(UTILS_SANITY_HPP) diff --git a/utils/sanity_fwd.hpp b/utils/sanity_fwd.hpp new file mode 100644 index 000000000000..98a897c0ff39 --- /dev/null +++ b/utils/sanity_fwd.hpp @@ -0,0 +1,52 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sanity_fwd.hpp +/// Forward declarations for utils/sanity.hpp + +#if !defined(UTILS_SANITY_FWD_HPP) +#define UTILS_SANITY_FWD_HPP + +namespace utils { + + +/// Enumeration to define the assertion type. +/// +/// The assertion type is used by the module to format the assertion messages +/// appropriately. +enum assert_type { + invariant, + postcondition, + precondition, + unreachable, +}; + + +} // namespace utils + +#endif // !defined(UTILS_SANITY_FWD_HPP) diff --git a/utils/sanity_test.cpp b/utils/sanity_test.cpp new file mode 100644 index 000000000000..54844fb75d64 --- /dev/null +++ b/utils/sanity_test.cpp @@ -0,0 +1,322 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sanity.hpp" + +extern "C" { +#include +#include +} + +#include +#include + +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/test_utils.ipp" + +namespace fs = utils::fs; +namespace process = utils::process; + + +#define FILE_REGEXP __FILE__ ":[0-9]+: " + + +static const fs::path Stdout_File("stdout.txt"); +static const fs::path Stderr_File("stderr.txt"); + + +#if NDEBUG +static bool NDebug = true; +#else +static bool NDebug = false; +#endif + + +template< typename Function > +static process::status +run_test(Function function) +{ + utils::avoid_coredump_on_crash(); + + const process::status status = process::child::fork_files( + function, Stdout_File, Stderr_File)->wait(); + atf::utils::cat_file(Stdout_File.str(), "Helper stdout: "); + atf::utils::cat_file(Stderr_File.str(), "Helper stderr: "); + return status; +} + + +static void +verify_success(const process::status& status) +{ + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); + ATF_REQUIRE(atf::utils::grep_file("Before test", Stdout_File.str())); + ATF_REQUIRE(atf::utils::grep_file("After test", Stdout_File.str())); +} + + +static void +verify_failed(const process::status& status, const char* type, + const char* exp_message, const bool check_ndebug) +{ + if (check_ndebug && NDebug) { + std::cout << "Built with NDEBUG; skipping verification\n"; + verify_success(status); + } else { + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(atf::utils::grep_file("Before test", Stdout_File.str())); + ATF_REQUIRE(!atf::utils::grep_file("After test", Stdout_File.str())); + if (exp_message != NULL) + ATF_REQUIRE(atf::utils::grep_file(F(FILE_REGEXP "%s: %s") % + type % exp_message, + Stderr_File.str())); + else + ATF_REQUIRE(atf::utils::grep_file(F(FILE_REGEXP "%s") % type, + Stderr_File.str())); + } +} + + +template< bool Expression, bool WithMessage > +static void +do_inv_test(void) +{ + std::cout << "Before test\n"; + if (WithMessage) + INV_MSG(Expression, "Custom message"); + else + INV(Expression); + std::cout << "After test\n"; + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(inv__holds); +ATF_TEST_CASE_BODY(inv__holds) +{ + const process::status status = run_test(do_inv_test< true, false >); + verify_success(status); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(inv__triggers_default_message); +ATF_TEST_CASE_BODY(inv__triggers_default_message) +{ + const process::status status = run_test(do_inv_test< false, false >); + verify_failed(status, "Invariant check failed", "Expression", true); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(inv__triggers_custom_message); +ATF_TEST_CASE_BODY(inv__triggers_custom_message) +{ + const process::status status = run_test(do_inv_test< false, true >); + verify_failed(status, "Invariant check failed", "Custom", true); +} + + +template< bool Expression, bool WithMessage > +static void +do_pre_test(void) +{ + std::cout << "Before test\n"; + if (WithMessage) + PRE_MSG(Expression, "Custom message"); + else + PRE(Expression); + std::cout << "After test\n"; + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pre__holds); +ATF_TEST_CASE_BODY(pre__holds) +{ + const process::status status = run_test(do_pre_test< true, false >); + verify_success(status); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pre__triggers_default_message); +ATF_TEST_CASE_BODY(pre__triggers_default_message) +{ + const process::status status = run_test(do_pre_test< false, false >); + verify_failed(status, "Precondition check failed", "Expression", true); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(pre__triggers_custom_message); +ATF_TEST_CASE_BODY(pre__triggers_custom_message) +{ + const process::status status = run_test(do_pre_test< false, true >); + verify_failed(status, "Precondition check failed", "Custom", true); +} + + +template< bool Expression, bool WithMessage > +static void +do_post_test(void) +{ + std::cout << "Before test\n"; + if (WithMessage) + POST_MSG(Expression, "Custom message"); + else + POST(Expression); + std::cout << "After test\n"; + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(post__holds); +ATF_TEST_CASE_BODY(post__holds) +{ + const process::status status = run_test(do_post_test< true, false >); + verify_success(status); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(post__triggers_default_message); +ATF_TEST_CASE_BODY(post__triggers_default_message) +{ + const process::status status = run_test(do_post_test< false, false >); + verify_failed(status, "Postcondition check failed", "Expression", true); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(post__triggers_custom_message); +ATF_TEST_CASE_BODY(post__triggers_custom_message) +{ + const process::status status = run_test(do_post_test< false, true >); + verify_failed(status, "Postcondition check failed", "Custom", true); +} + + +template< bool WithMessage > +static void +do_unreachable_test(void) +{ + std::cout << "Before test\n"; + if (WithMessage) + UNREACHABLE_MSG("Custom message"); + else + UNREACHABLE; + std::cout << "After test\n"; + std::exit(EXIT_SUCCESS); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unreachable__default_message); +ATF_TEST_CASE_BODY(unreachable__default_message) +{ + const process::status status = run_test(do_unreachable_test< false >); + verify_failed(status, "Unreachable point reached", NULL, false); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unreachable__custom_message); +ATF_TEST_CASE_BODY(unreachable__custom_message) +{ + const process::status status = run_test(do_unreachable_test< true >); + verify_failed(status, "Unreachable point reached", "Custom", false); +} + + +template< int Signo > +static void +do_crash_handler_test(void) +{ + utils::install_crash_handlers("test-log.txt"); + ::kill(::getpid(), Signo); + std::cout << "After signal\n"; + std::exit(EXIT_FAILURE); +} + + +template< int Signo > +static void +crash_handler_test(void) +{ + utils::avoid_coredump_on_crash(); + + const process::status status = run_test(do_crash_handler_test< Signo >); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(Signo, status.termsig()); + ATF_REQUIRE(atf::utils::grep_file(F("Fatal signal %s") % Signo, + Stderr_File.str())); + ATF_REQUIRE(atf::utils::grep_file("Log file is test-log.txt", + Stderr_File.str())); + ATF_REQUIRE(!atf::utils::grep_file("After signal", Stdout_File.str())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(install_crash_handlers__sigabrt); +ATF_TEST_CASE_BODY(install_crash_handlers__sigabrt) +{ + crash_handler_test< SIGABRT >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(install_crash_handlers__sigbus); +ATF_TEST_CASE_BODY(install_crash_handlers__sigbus) +{ + crash_handler_test< SIGBUS >(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(install_crash_handlers__sigsegv); +ATF_TEST_CASE_BODY(install_crash_handlers__sigsegv) +{ + crash_handler_test< SIGSEGV >(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, inv__holds); + ATF_ADD_TEST_CASE(tcs, inv__triggers_default_message); + ATF_ADD_TEST_CASE(tcs, inv__triggers_custom_message); + ATF_ADD_TEST_CASE(tcs, pre__holds); + ATF_ADD_TEST_CASE(tcs, pre__triggers_default_message); + ATF_ADD_TEST_CASE(tcs, pre__triggers_custom_message); + ATF_ADD_TEST_CASE(tcs, post__holds); + ATF_ADD_TEST_CASE(tcs, post__triggers_default_message); + ATF_ADD_TEST_CASE(tcs, post__triggers_custom_message); + ATF_ADD_TEST_CASE(tcs, unreachable__default_message); + ATF_ADD_TEST_CASE(tcs, unreachable__custom_message); + + ATF_ADD_TEST_CASE(tcs, install_crash_handlers__sigabrt); + ATF_ADD_TEST_CASE(tcs, install_crash_handlers__sigbus); + ATF_ADD_TEST_CASE(tcs, install_crash_handlers__sigsegv); +} diff --git a/utils/signals/Kyuafile b/utils/signals/Kyuafile new file mode 100644 index 000000000000..09da3e166cd2 --- /dev/null +++ b/utils/signals/Kyuafile @@ -0,0 +1,9 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="exceptions_test"} +atf_test_program{name="interrupts_test"} +atf_test_program{name="misc_test"} +atf_test_program{name="programmer_test"} +atf_test_program{name="timer_test"} diff --git a/utils/signals/Makefile.am.inc b/utils/signals/Makefile.am.inc new file mode 100644 index 000000000000..b01089c80fea --- /dev/null +++ b/utils/signals/Makefile.am.inc @@ -0,0 +1,73 @@ +# Copyright 2010 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +libutils_a_SOURCES += utils/signals/exceptions.cpp +libutils_a_SOURCES += utils/signals/exceptions.hpp +libutils_a_SOURCES += utils/signals/interrupts.cpp +libutils_a_SOURCES += utils/signals/interrupts.hpp +libutils_a_SOURCES += utils/signals/interrupts_fwd.hpp +libutils_a_SOURCES += utils/signals/misc.cpp +libutils_a_SOURCES += utils/signals/misc.hpp +libutils_a_SOURCES += utils/signals/programmer.cpp +libutils_a_SOURCES += utils/signals/programmer.hpp +libutils_a_SOURCES += utils/signals/programmer_fwd.hpp +libutils_a_SOURCES += utils/signals/timer.cpp +libutils_a_SOURCES += utils/signals/timer.hpp +libutils_a_SOURCES += utils/signals/timer_fwd.hpp + +if WITH_ATF +tests_utils_signalsdir = $(pkgtestsdir)/utils/signals + +tests_utils_signals_DATA = utils/signals/Kyuafile +EXTRA_DIST += $(tests_utils_signals_DATA) + +tests_utils_signals_PROGRAMS = utils/signals/exceptions_test +utils_signals_exceptions_test_SOURCES = utils/signals/exceptions_test.cpp +utils_signals_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_signals_PROGRAMS += utils/signals/interrupts_test +utils_signals_interrupts_test_SOURCES = utils/signals/interrupts_test.cpp +utils_signals_interrupts_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_interrupts_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_signals_PROGRAMS += utils/signals/misc_test +utils_signals_misc_test_SOURCES = utils/signals/misc_test.cpp +utils_signals_misc_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_misc_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_signals_PROGRAMS += utils/signals/programmer_test +utils_signals_programmer_test_SOURCES = utils/signals/programmer_test.cpp +utils_signals_programmer_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_programmer_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_signals_PROGRAMS += utils/signals/timer_test +utils_signals_timer_test_SOURCES = utils/signals/timer_test.cpp +utils_signals_timer_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_signals_timer_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/signals/exceptions.cpp b/utils/signals/exceptions.cpp new file mode 100644 index 000000000000..70e0dbe8a5d1 --- /dev/null +++ b/utils/signals/exceptions.cpp @@ -0,0 +1,102 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/exceptions.hpp" + +#include + +#include "utils/format/macros.hpp" + +namespace signals = utils::signals; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +signals::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +signals::error::~error(void) throw() +{ +} + + +/// Constructs a new interrupted error. +/// +/// \param signo_ The signal that caused the interrupt. +signals::interrupted_error::interrupted_error(const int signo_) : + error(F("Interrupted by signal %s") % signo_), + _signo(signo_) +{ +} + + +/// Destructor for the error. +signals::interrupted_error::~interrupted_error(void) throw() +{ +} + + +/// Queries the signal number of the interruption. +/// +/// \return A signal number. +int +signals::interrupted_error::signo(void) const +{ + return _signo; +} + + +/// Constructs a new error based on an errno code. +/// +/// \param message_ The message describing what caused the error. +/// \param errno_ The error code. +signals::system_error::system_error(const std::string& message_, + const int errno_) : + error(F("%s: %s") % message_ % strerror(errno_)), + _original_errno(errno_) +{ +} + + +/// Destructor for the error. +signals::system_error::~system_error(void) throw() +{ +} + + +/// \return The original errno value. +int +signals::system_error::original_errno(void) const throw() +{ + return _original_errno; +} diff --git a/utils/signals/exceptions.hpp b/utils/signals/exceptions.hpp new file mode 100644 index 000000000000..35cd2c9e8168 --- /dev/null +++ b/utils/signals/exceptions.hpp @@ -0,0 +1,83 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/signals/exceptions.hpp +/// Exception types raised by the signals module. + +#if !defined(UTILS_SIGNALS_EXCEPTIONS_HPP) +#define UTILS_SIGNALS_EXCEPTIONS_HPP + +#include + +namespace utils { +namespace signals { + + +/// Base exceptions for signals errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Denotes the reception of a signal to controlledly terminate execution. +class interrupted_error : public error { + /// Signal that caused the interrupt. + int _signo; + +public: + explicit interrupted_error(const int signo_); + ~interrupted_error(void) throw(); + + int signo(void) const; +}; + + +/// Exceptions for errno-based errors. +/// +/// TODO(jmmv): This code is duplicated in, at least, utils::fs. Figure +/// out a way to reuse this exception while maintaining the correct inheritance +/// (i.e. be able to keep it as a child of signals::error). +class system_error : public error { + /// Error number describing this libc error condition. + int _original_errno; + +public: + explicit system_error(const std::string&, const int); + ~system_error(void) throw(); + + int original_errno(void) const throw(); +}; + + +} // namespace signals +} // namespace utils + + +#endif // !defined(UTILS_SIGNALS_EXCEPTIONS_HPP) diff --git a/utils/signals/exceptions_test.cpp b/utils/signals/exceptions_test.cpp new file mode 100644 index 000000000000..40db536f1a8c --- /dev/null +++ b/utils/signals/exceptions_test.cpp @@ -0,0 +1,73 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/exceptions.hpp" + +#include +#include + +#include + +#include "utils/format/macros.hpp" + +namespace signals = utils::signals; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const signals::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupted_error); +ATF_TEST_CASE_BODY(interrupted_error) +{ + const signals::interrupted_error e(5); + ATF_REQUIRE(std::strcmp("Interrupted by signal 5", e.what()) == 0); + ATF_REQUIRE_EQ(5, e.signo()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(system_error); +ATF_TEST_CASE_BODY(system_error) +{ + const signals::system_error e("Call failed", ENOENT); + const std::string expected = F("Call failed: %s") % std::strerror(ENOENT); + ATF_REQUIRE_EQ(expected, e.what()); + ATF_REQUIRE_EQ(ENOENT, e.original_errno()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, interrupted_error); + ATF_ADD_TEST_CASE(tcs, system_error); +} diff --git a/utils/signals/interrupts.cpp b/utils/signals/interrupts.cpp new file mode 100644 index 000000000000..956a83c66802 --- /dev/null +++ b/utils/signals/interrupts.cpp @@ -0,0 +1,309 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/interrupts.hpp" + +extern "C" { +#include + +#include +#include +} + +#include +#include +#include + +#include "utils/logging/macros.hpp" +#include "utils/process/operations.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" +#include "utils/signals/programmer.hpp" + +namespace signals = utils::signals; +namespace process = utils::process; + + +namespace { + + +/// The interrupt signal that fired, or -1 if none. +static volatile int fired_signal = -1; + + +/// Collection of PIDs. +typedef std::set< pid_t > pids_set; + + +/// List of processes to kill upon reception of a signal. +static pids_set pids_to_kill; + + +/// Programmer status for the SIGHUP signal. +static std::auto_ptr< signals::programmer > sighup_handler; +/// Programmer status for the SIGINT signal. +static std::auto_ptr< signals::programmer > sigint_handler; +/// Programmer status for the SIGTERM signal. +static std::auto_ptr< signals::programmer > sigterm_handler; + + +/// Signal mask to restore after exiting a signal inhibited section. +static sigset_t global_old_sigmask; + + +/// Whether there is an interrupts_handler object in existence or not. +static bool interrupts_handler_active = false; + + +/// Whether there is an interrupts_inhibiter object in existence or not. +static std::size_t interrupts_inhibiter_active = 0; + + +/// Generic handler to capture interrupt signals. +/// +/// From this handler, we record that an interrupt has happened so that +/// check_interrupt() can know whether there execution has to be stopped or not. +/// We also terminate any of our child processes (started by the +/// utils::process::children class) so that any ongoing wait(2) system calls +/// terminate. +/// +/// \param signo The signal that caused this handler to be called. +static void +signal_handler(const int signo) +{ + static const char* message = "[-- Signal caught; please wait for " + "cleanup --]\n"; + if (::write(STDERR_FILENO, message, std::strlen(message)) == -1) { + // We are exiting: the message printed here is only for informational + // purposes. If we fail to print it (which probably means something + // is really bad), there is not much we can do within the signal + // handler, so just ignore this. + } + + fired_signal = signo; + + for (pids_set::const_iterator iter = pids_to_kill.begin(); + iter != pids_to_kill.end(); ++iter) { + process::terminate_group(*iter); + } +} + + +/// Installs signal handlers for potential interrupts. +/// +/// \pre Must not have been called before. +/// \post The various sig*_handler global variables are atomically updated. +static void +setup_handlers(void) +{ + PRE(sighup_handler.get() == NULL); + PRE(sigint_handler.get() == NULL); + PRE(sigterm_handler.get() == NULL); + + // Create the handlers on the stack first so that, if any of them fails, the + // stack unwinding cleans things up. + std::auto_ptr< signals::programmer > tmp_sighup_handler( + new signals::programmer(SIGHUP, signal_handler)); + std::auto_ptr< signals::programmer > tmp_sigint_handler( + new signals::programmer(SIGINT, signal_handler)); + std::auto_ptr< signals::programmer > tmp_sigterm_handler( + new signals::programmer(SIGTERM, signal_handler)); + + // Now, update the global pointers, which is an operation that cannot fail. + sighup_handler = tmp_sighup_handler; + sigint_handler = tmp_sigint_handler; + sigterm_handler = tmp_sigterm_handler; +} + + +/// Uninstalls the signal handlers installed by setup_handlers(). +static void +cleanup_handlers(void) +{ + sighup_handler->unprogram(); sighup_handler.reset(NULL); + sigint_handler->unprogram(); sigint_handler.reset(NULL); + sigterm_handler->unprogram(); sigterm_handler.reset(NULL); +} + + + +/// Masks the signals installed by setup_handlers(). +/// +/// \param[out] old_sigmask The old signal mask to save via the +/// \code oset \endcode argument with sigprocmask(2). +static void +mask_signals(sigset_t* old_sigmask) +{ + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGALRM); + sigaddset(&mask, SIGHUP); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + const int ret = ::sigprocmask(SIG_BLOCK, &mask, old_sigmask); + INV(ret != -1); +} + + +/// Resets the signal masking put in place by mask_signals(). +/// +/// \param[in] old_sigmask The old signal mask to restore via the +/// \code set \endcode argument with sigprocmask(2). +static void +unmask_signals(sigset_t* old_sigmask) +{ + const int ret = ::sigprocmask(SIG_SETMASK, old_sigmask, NULL); + INV(ret != -1); +} + + +} // anonymous namespace + + +/// Constructor that sets up the signal handlers. +signals::interrupts_handler::interrupts_handler(void) : + _programmed(false) +{ + PRE(!interrupts_handler_active); + setup_handlers(); + _programmed = true; + interrupts_handler_active = true; +} + + +/// Destructor that removes the signal handlers. +/// +/// Given that this is a destructor and it can't report errors back to the +/// caller, the caller must attempt to call unprogram() on its own. +signals::interrupts_handler::~interrupts_handler(void) +{ + if (_programmed) { + LW("Destroying still-programmed signals::interrupts_handler object"); + try { + unprogram(); + } catch (const error& e) { + UNREACHABLE; + } + } +} + + +/// Unprograms all signals captured by the interrupts handler. +/// +/// \throw system_error If the unprogramming of any signal fails. +void +signals::interrupts_handler::unprogram(void) +{ + PRE(_programmed); + + // Modify the control variables first before unprogramming the handlers. If + // we fail to do the latter, we do not want to try again because we will not + // succeed (and we'll cause a crash due to failed preconditions). + _programmed = false; + interrupts_handler_active = false; + + cleanup_handlers(); + fired_signal = -1; +} + + +/// Constructor that sets up signal masking. +signals::interrupts_inhibiter::interrupts_inhibiter(void) +{ + sigset_t old_sigmask; + mask_signals(&old_sigmask); + if (interrupts_inhibiter_active == 0) { + global_old_sigmask = old_sigmask; + } + ++interrupts_inhibiter_active; +} + + +/// Destructor that removes signal masking. +signals::interrupts_inhibiter::~interrupts_inhibiter(void) +{ + if (interrupts_inhibiter_active > 1) { + --interrupts_inhibiter_active; + } else { + interrupts_inhibiter_active = false; + unmask_signals(&global_old_sigmask); + } +} + + +/// Checks if an interrupt has fired. +/// +/// Calls to this function should be sprinkled in strategic places through the +/// code protected by an interrupts_handler object. +/// +/// Only one call to this function will raise an exception per signal received. +/// This is to allow executing cleanup actions without reraising interrupt +/// exceptions unless the user has fired another interrupt. +/// +/// \throw interrupted_error If there has been an interrupt. +void +signals::check_interrupt(void) +{ + if (fired_signal != -1) { + const int original_fired_signal = fired_signal; + fired_signal = -1; + throw interrupted_error(original_fired_signal); + } +} + + +/// Registers a child process to be killed upon reception of an interrupt. +/// +/// \pre Must be called with interrupts being inhibited. The caller must ensure +/// that the call call to fork() and the addition of the PID happen atomically. +/// +/// \param pid The PID of the child process. Must not have been yet regsitered. +void +signals::add_pid_to_kill(const pid_t pid) +{ + PRE(interrupts_inhibiter_active); + PRE(pids_to_kill.find(pid) == pids_to_kill.end()); + pids_to_kill.insert(pid); +} + + +/// Unregisters a child process previously registered via add_pid_to_kill(). +/// +/// \pre Must be called with interrupts being inhibited. This is not necessary, +/// but pushing this to the caller simplifies our logic and provides consistency +/// with the add_pid_to_kill() call. +/// +/// \param pid The PID of the child process. Must have been registered +/// previously, and the process must have already been awaited for. +void +signals::remove_pid_to_kill(const pid_t pid) +{ + PRE(interrupts_inhibiter_active); + PRE(pids_to_kill.find(pid) != pids_to_kill.end()); + pids_to_kill.erase(pid); +} diff --git a/utils/signals/interrupts.hpp b/utils/signals/interrupts.hpp new file mode 100644 index 000000000000..b181114bb245 --- /dev/null +++ b/utils/signals/interrupts.hpp @@ -0,0 +1,83 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/signals/interrupts.hpp +/// Handling of interrupts. + +#if !defined(UTILS_SIGNALS_INTERRUPTS_HPP) +#define UTILS_SIGNALS_INTERRUPTS_HPP + +#include "utils/signals/interrupts_fwd.hpp" + +#include + +#include "utils/noncopyable.hpp" + +namespace utils { +namespace signals { + + +/// Provides a scope in which interrupts can be detected and handled. +/// +/// This RAII-modeled object installs signal handler when instantiated and +/// removes them upon destruction. While this object is active, the +/// check_interrupt() free function can be used to determine if an interrupt has +/// happened. +class interrupts_handler : noncopyable { + /// Whether the interrupts are still programmed or not. + /// + /// Used by the destructor to prevent double-unprogramming when unprogram() + /// is explicitly called by the user. + bool _programmed; + +public: + interrupts_handler(void); + ~interrupts_handler(void); + + void unprogram(void); +}; + + +/// Disables interrupts while the object is alive. +class interrupts_inhibiter : noncopyable { +public: + interrupts_inhibiter(void); + ~interrupts_inhibiter(void); +}; + + +void check_interrupt(void); + +void add_pid_to_kill(const pid_t); +void remove_pid_to_kill(const pid_t); + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_INTERRUPTS_HPP) diff --git a/utils/signals/interrupts_fwd.hpp b/utils/signals/interrupts_fwd.hpp new file mode 100644 index 000000000000..e4dfe68d54e2 --- /dev/null +++ b/utils/signals/interrupts_fwd.hpp @@ -0,0 +1,46 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/signals/interrupts_fwd.hpp +/// Forward declarations for utils/signals/interrupts.hpp + +#if !defined(UTILS_SIGNALS_INTERRUPTS_FWD_HPP) +#define UTILS_SIGNALS_INTERRUPTS_FWD_HPP + +namespace utils { +namespace signals { + + +class interrupts_handler; +class interrupts_inhibiter; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_INTERRUPTS_FWD_HPP) diff --git a/utils/signals/interrupts_test.cpp b/utils/signals/interrupts_test.cpp new file mode 100644 index 000000000000..ef8758d8d5f1 --- /dev/null +++ b/utils/signals/interrupts_test.cpp @@ -0,0 +1,266 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/interrupts.hpp" + +extern "C" { +#include +#include +} + +#include +#include + +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/signals/exceptions.hpp" +#include "utils/signals/programmer.hpp" + +namespace fs = utils::fs; +namespace process = utils::process; +namespace signals = utils::signals; + + +namespace { + + +/// Set to the signal that fired; -1 if none. +static volatile int fired_signal = -1; + + +/// Test handler for signals. +/// +/// \post fired_signal is set to the signal that triggered the handler. +/// +/// \param signo The signal that triggered the handler. +static void +signal_handler(const int signo) +{ + PRE(fired_signal == -1 || fired_signal == signo); + fired_signal = signo; +} + + +/// Child process that pauses waiting to be killed. +static void +pause_child(void) +{ + sigset_t mask; + sigemptyset(&mask); + // We loop waiting for signals because we want the parent process to send us + // a SIGKILL that we cannot handle, not just any non-deadly signal. + for (;;) { + std::cerr << F("Waiting for any signal; pid=%s\n") % ::getpid(); + ::sigsuspend(&mask); + std::cerr << F("Signal received; pid=%s\n") % ::getpid(); + } +} + + +/// Checks that interrupts_handler() handles a particular signal. +/// +/// This indirectly checks the check_interrupt() function, which is not part of +/// the class but is tightly related. +/// +/// \param signo The signal to check. +/// \param explicit_unprogram Whether to call interrupts_handler::unprogram() +/// explicitly before letting the object go out of scope. +static void +check_interrupts_handler(const int signo, const bool explicit_unprogram) +{ + fired_signal = -1; + + signals::programmer test_handler(signo, signal_handler); + + { + signals::interrupts_handler interrupts; + + // No pending interrupts at first. + signals::check_interrupt(); + + // Send us an interrupt and check for it. + ::kill(getpid(), signo); + ATF_REQUIRE_THROW_RE(signals::interrupted_error, + F("Interrupted by signal %s") % signo, + signals::check_interrupt()); + + // Interrupts should have been cleared now, so this should not throw. + signals::check_interrupt(); + + // Check to see if a second interrupt is detected. + ::kill(getpid(), signo); + ATF_REQUIRE_THROW_RE(signals::interrupted_error, + F("Interrupted by signal %s") % signo, + signals::check_interrupt()); + + // And ensure the interrupt was cleared again. + signals::check_interrupt(); + + if (explicit_unprogram) { + interrupts.unprogram(); + } + } + + ATF_REQUIRE_EQ(-1, fired_signal); + ::kill(getpid(), signo); + ATF_REQUIRE_EQ(signo, fired_signal); + + test_handler.unprogram(); +} + + +/// Checks that interrupts_inhibiter() handles a particular signal. +/// +/// \param signo The signal to check. +static void +check_interrupts_inhibiter(const int signo) +{ + signals::programmer test_handler(signo, signal_handler); + + { + signals::interrupts_inhibiter inhibiter; + { + signals::interrupts_inhibiter nested_inhibiter; + ::kill(::getpid(), signo); + ATF_REQUIRE_EQ(-1, fired_signal); + } + ::kill(::getpid(), signo); + ATF_REQUIRE_EQ(-1, fired_signal); + } + ATF_REQUIRE_EQ(signo, fired_signal); + + test_handler.unprogram(); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_handler__sighup); +ATF_TEST_CASE_BODY(interrupts_handler__sighup) +{ + // We run this twice in sequence to ensure that we can actually program two + // interrupts handlers in a row. + check_interrupts_handler(SIGHUP, true); + check_interrupts_handler(SIGHUP, false); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_handler__sigint); +ATF_TEST_CASE_BODY(interrupts_handler__sigint) +{ + // We run this twice in sequence to ensure that we can actually program two + // interrupts handlers in a row. + check_interrupts_handler(SIGINT, true); + check_interrupts_handler(SIGINT, false); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_handler__sigterm); +ATF_TEST_CASE_BODY(interrupts_handler__sigterm) +{ + // We run this twice in sequence to ensure that we can actually program two + // interrupts handlers in a row. + check_interrupts_handler(SIGTERM, true); + check_interrupts_handler(SIGTERM, false); +} + + +ATF_TEST_CASE(interrupts_handler__kill_children); +ATF_TEST_CASE_HEAD(interrupts_handler__kill_children) +{ + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(interrupts_handler__kill_children) +{ + std::auto_ptr< process::child > child1(process::child::fork_files( + pause_child, fs::path("/dev/stdout"), fs::path("/dev/stderr"))); + std::auto_ptr< process::child > child2(process::child::fork_files( + pause_child, fs::path("/dev/stdout"), fs::path("/dev/stderr"))); + + signals::interrupts_handler interrupts; + + // Our children pause until the reception of a signal. Interrupting + // ourselves will cause the signal to be re-delivered to our children due to + // the interrupts_handler semantics. If this does not happen, the wait + // calls below would block indefinitely and cause our test to time out. + ::kill(::getpid(), SIGHUP); + + const process::status status1 = child1->wait(); + ATF_REQUIRE(status1.signaled()); + ATF_REQUIRE_EQ(SIGKILL, status1.termsig()); + const process::status status2 = child2->wait(); + ATF_REQUIRE(status2.signaled()); + ATF_REQUIRE_EQ(SIGKILL, status2.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_inhibiter__sigalrm); +ATF_TEST_CASE_BODY(interrupts_inhibiter__sigalrm) +{ + check_interrupts_inhibiter(SIGALRM); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_inhibiter__sighup); +ATF_TEST_CASE_BODY(interrupts_inhibiter__sighup) +{ + check_interrupts_inhibiter(SIGHUP); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_inhibiter__sigint); +ATF_TEST_CASE_BODY(interrupts_inhibiter__sigint) +{ + check_interrupts_inhibiter(SIGINT); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(interrupts_inhibiter__sigterm); +ATF_TEST_CASE_BODY(interrupts_inhibiter__sigterm) +{ + check_interrupts_inhibiter(SIGTERM); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, interrupts_handler__sighup); + ATF_ADD_TEST_CASE(tcs, interrupts_handler__sigint); + ATF_ADD_TEST_CASE(tcs, interrupts_handler__sigterm); + ATF_ADD_TEST_CASE(tcs, interrupts_handler__kill_children); + + ATF_ADD_TEST_CASE(tcs, interrupts_inhibiter__sigalrm); + ATF_ADD_TEST_CASE(tcs, interrupts_inhibiter__sighup); + ATF_ADD_TEST_CASE(tcs, interrupts_inhibiter__sigint); + ATF_ADD_TEST_CASE(tcs, interrupts_inhibiter__sigterm); +} diff --git a/utils/signals/misc.cpp b/utils/signals/misc.cpp new file mode 100644 index 000000000000..b9eb1c402a28 --- /dev/null +++ b/utils/signals/misc.cpp @@ -0,0 +1,106 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/misc.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include +} + +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/signals/exceptions.hpp" + +namespace signals = utils::signals; + + +/// Number of the last valid signal. +const int utils::signals::last_signo = LAST_SIGNO; + + +/// Resets a signal handler to its default behavior. +/// +/// \param signo The number of the signal handler to reset. +/// +/// \throw signals::system_error If there is a problem trying to reset the +/// signal handler to its default behavior. +void +signals::reset(const int signo) +{ + struct ::sigaction sa; + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + if (::sigaction(signo, &sa, NULL) == -1) { + const int original_errno = errno; + throw system_error(F("Failed to reset signal %s") % signo, + original_errno); + } +} + + +/// Resets all signals to their default handlers. +/// +/// \return True if all signals could be reset properly; false otherwise. +bool +signals::reset_all(void) +{ + bool ok = true; + + for (int signo = 1; signo <= signals::last_signo; ++signo) { + if (signo == SIGKILL || signo == SIGSTOP) { + // Don't attempt to reset immutable signals. + } else { + try { + signals::reset(signo); + } catch (const signals::error& e) { +#if defined(SIGTHR) + if (signo == SIGTHR) { + // If FreeBSD's libthr is loaded, it prevents us from + // modifying SIGTHR (at least in 11.0-CURRENT as of + // 2015-01-28). Skip failures for this signal if they + // happen to avoid this corner case. + continue; + } +#endif + LW(e.what()); + ok = false; + } + } + } + + return ok; +} diff --git a/utils/signals/misc.hpp b/utils/signals/misc.hpp new file mode 100644 index 000000000000..ad3763feabc4 --- /dev/null +++ b/utils/signals/misc.hpp @@ -0,0 +1,49 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/signals/misc.hpp +/// Free functions and globals. + +#if !defined(UTILS_SIGNALS_MISC_HPP) +#define UTILS_SIGNALS_MISC_HPP + +namespace utils { +namespace signals { + + +extern const int last_signo; + + +void reset(const int); +bool reset_all(void); + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_MISC_HPP) diff --git a/utils/signals/misc_test.cpp b/utils/signals/misc_test.cpp new file mode 100644 index 000000000000..76f36b0e5082 --- /dev/null +++ b/utils/signals/misc_test.cpp @@ -0,0 +1,133 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/misc.hpp" + +extern "C" { +#include +#include +} + +#include + +#include + +#include "utils/defs.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/signals/exceptions.hpp" + +namespace fs = utils::fs; +namespace process = utils::process; +namespace signals = utils::signals; + + +namespace { + + +static void program_reset_raise(void) UTILS_NORETURN; + + +/// Body of a subprocess that tests the signals::reset function. +/// +/// This function programs a signal to be ignored, then uses signal::reset to +/// bring it back to its default handler and then delivers the signal to self. +/// The default behavior of the signal is for the process to die, so this +/// function should never return correctly (and thus the child process should +/// always die due to a signal if all goes well). +static void +program_reset_raise(void) +{ + struct ::sigaction sa; + sa.sa_handler = SIG_IGN; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + if (::sigaction(SIGUSR1, &sa, NULL) == -1) + std::exit(EXIT_FAILURE); + + signals::reset(SIGUSR1); + ::kill(::getpid(), SIGUSR1); + + // Should not be reached, but we do not assert this condition because we + // want to exit cleanly if the signal does not abort our execution to let + // the parent easily know what happened. + std::exit(EXIT_SUCCESS); +} + + +/// Body of a subprocess that executes the signals::reset_all function. +/// +/// The process exits with success if the function worked, or with a failure if +/// an error is reported. No signals are tested. +static void +run_reset_all(void) +{ + const bool ok = signals::reset_all(); + std::exit(ok ? EXIT_SUCCESS : EXIT_FAILURE); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(reset__ok); +ATF_TEST_CASE_BODY(reset__ok) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + program_reset_raise, fs::path("/dev/stdout"), fs::path("/dev/stderr")); + process::status status = child->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGUSR1, status.termsig()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(reset__invalid); +ATF_TEST_CASE_BODY(reset__invalid) +{ + ATF_REQUIRE_THROW(signals::system_error, signals::reset(-1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(reset_all); +ATF_TEST_CASE_BODY(reset_all) +{ + std::auto_ptr< process::child > child = process::child::fork_files( + run_reset_all, fs::path("/dev/stdout"), fs::path("/dev/stderr")); + process::status status = child->wait(); + ATF_REQUIRE(status.exited()); + ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, reset__ok); + ATF_ADD_TEST_CASE(tcs, reset__invalid); + ATF_ADD_TEST_CASE(tcs, reset_all); +} diff --git a/utils/signals/programmer.cpp b/utils/signals/programmer.cpp new file mode 100644 index 000000000000..c47d1cf85038 --- /dev/null +++ b/utils/signals/programmer.cpp @@ -0,0 +1,138 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/programmer.hpp" + +extern "C" { +#include +} + +#include + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" + + +namespace utils { +namespace signals { + + +/// Internal implementation for the signals::programmer class. +struct programmer::impl : utils::noncopyable { + /// The number of the signal managed by this programmer. + int signo; + + /// Whether the signal is currently programmed by us or not. + bool programmed; + + /// The signal handler that we replaced; to be restored on unprogramming. + struct ::sigaction old_sa; + + /// Initializes the internal implementation of the programmer. + /// + /// \param signo_ The signal number. + impl(const int signo_) : + signo(signo_), + programmed(false) + { + } +}; + + +} // namespace signals +} // namespace utils + + +namespace signals = utils::signals; + + +/// Programs a signal handler. +/// +/// \param signo The signal for which to install the handler. +/// \param handler The handler to install. +/// +/// \throw signals::system_error If there is an error programming the signal. +signals::programmer::programmer(const int signo, const handler_type handler) : + _pimpl(new impl(signo)) +{ + struct ::sigaction sa; + sa.sa_handler = handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + + if (::sigaction(_pimpl->signo, &sa, &_pimpl->old_sa) == -1) { + const int original_errno = errno; + throw system_error(F("Could not install handler for signal %s") % + _pimpl->signo, original_errno); + } else + _pimpl->programmed = true; +} + + +/// Destructor; unprograms the signal handler if still programmed. +/// +/// Given that this is a destructor and it can't report errors back to the +/// caller, the caller must attempt to call unprogram() on its own. +signals::programmer::~programmer(void) +{ + if (_pimpl->programmed) { + LW("Destroying still-programmed signals::programmer object"); + try { + unprogram(); + } catch (const system_error& e) { + UNREACHABLE; + } + } +} + + +/// Unprograms the signal handler. +/// +/// \pre The signal handler is programmed (i.e. this can only be called once). +/// +/// \throw system_error If unprogramming the signal failed. If this happens, +/// the signal is left programmed, this object forgets about the signal and +/// therefore there is no way to restore the original handler. +void +signals::programmer::unprogram(void) +{ + PRE(_pimpl->programmed); + + // If we fail, we don't want the destructor to attempt to unprogram the + // handler again, as it would result in a crash. + _pimpl->programmed = false; + + if (::sigaction(_pimpl->signo, &_pimpl->old_sa, NULL) == -1) { + const int original_errno = errno; + throw system_error(F("Could not reset handler for signal %s") % + _pimpl->signo, original_errno); + } +} diff --git a/utils/signals/programmer.hpp b/utils/signals/programmer.hpp new file mode 100644 index 000000000000..5ac5318f0bb9 --- /dev/null +++ b/utils/signals/programmer.hpp @@ -0,0 +1,63 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/signals/programmer.hpp +/// Provides the signals::programmer class. + +#if !defined(UTILS_SIGNALS_PROGRAMMER_HPP) +#define UTILS_SIGNALS_PROGRAMMER_HPP + +#include "utils/signals/programmer_fwd.hpp" + +#include + +#include "utils/noncopyable.hpp" + +namespace utils { +namespace signals { + + +/// A RAII class to program signal handlers. +class programmer : noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + +public: + programmer(const int, const handler_type); + ~programmer(void); + + void unprogram(void); +}; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_PROGRAMMER_HPP) diff --git a/utils/signals/programmer_fwd.hpp b/utils/signals/programmer_fwd.hpp new file mode 100644 index 000000000000..55dfd34af2eb --- /dev/null +++ b/utils/signals/programmer_fwd.hpp @@ -0,0 +1,49 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/signals/programmer_fwd.hpp +/// Forward declarations for utils/signals/programmer.hpp + +#if !defined(UTILS_SIGNALS_PROGRAMMER_FWD_HPP) +#define UTILS_SIGNALS_PROGRAMMER_FWD_HPP + +namespace utils { +namespace signals { + + +/// Function type for signal handlers. +typedef void (*handler_type)(const int); + + +class programmer; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_PROGRAMMER_FWD_HPP) diff --git a/utils/signals/programmer_test.cpp b/utils/signals/programmer_test.cpp new file mode 100644 index 000000000000..0e95f84974b1 --- /dev/null +++ b/utils/signals/programmer_test.cpp @@ -0,0 +1,140 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/programmer.hpp" + +extern "C" { +#include +#include +} + +#include + +#include "utils/sanity.hpp" + +namespace signals = utils::signals; + + +namespace { + + +namespace sigchld { + + +static bool happened_1; +static bool happened_2; + + +void handler_1(const int signo) { + PRE(signo == SIGCHLD); + happened_1 = true; +} + + +void handler_2(const int signo) { + PRE(signo == SIGCHLD); + happened_2 = true; +} + + +} // namespace sigchld + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(program_unprogram); +ATF_TEST_CASE_BODY(program_unprogram) +{ + signals::programmer programmer(SIGCHLD, sigchld::handler_1); + sigchld::happened_1 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(sigchld::happened_1); + + programmer.unprogram(); + sigchld::happened_1 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(!sigchld::happened_1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(scope); +ATF_TEST_CASE_BODY(scope) +{ + { + signals::programmer programmer(SIGCHLD, sigchld::handler_1); + sigchld::happened_1 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(sigchld::happened_1); + } + + sigchld::happened_1 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(!sigchld::happened_1); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(nested); +ATF_TEST_CASE_BODY(nested) +{ + signals::programmer programmer_1(SIGCHLD, sigchld::handler_1); + sigchld::happened_1 = false; + sigchld::happened_2 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(sigchld::happened_1); + ATF_REQUIRE(!sigchld::happened_2); + + signals::programmer programmer_2(SIGCHLD, sigchld::handler_2); + sigchld::happened_1 = false; + sigchld::happened_2 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(!sigchld::happened_1); + ATF_REQUIRE(sigchld::happened_2); + + programmer_2.unprogram(); + sigchld::happened_1 = false; + sigchld::happened_2 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(sigchld::happened_1); + ATF_REQUIRE(!sigchld::happened_2); + + programmer_1.unprogram(); + sigchld::happened_1 = false; + sigchld::happened_2 = false; + ::kill(::getpid(), SIGCHLD); + ATF_REQUIRE(!sigchld::happened_1); + ATF_REQUIRE(!sigchld::happened_2); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, program_unprogram); + ATF_ADD_TEST_CASE(tcs, scope); + ATF_ADD_TEST_CASE(tcs, nested); +} diff --git a/utils/signals/timer.cpp b/utils/signals/timer.cpp new file mode 100644 index 000000000000..698b9835dc10 --- /dev/null +++ b/utils/signals/timer.cpp @@ -0,0 +1,547 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/timer.hpp" + +extern "C" { +#include + +#include +} + +#include +#include +#include +#include + +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" +#include "utils/signals/interrupts.hpp" +#include "utils/signals/programmer.hpp" + +namespace datetime = utils::datetime; +namespace signals = utils::signals; + +using utils::none; +using utils::optional; + +namespace { + + +static void sigalrm_handler(const int); + + +/// Calls setitimer(2) with exception-based error reporting. +/// +/// This does not currently support intervals. +/// +/// \param delta The time to the first activation of the programmed timer. +/// \param old_timeval If not NULL, pointer to a timeval into which to store the +/// existing system timer. +/// +/// \throw system_error If the call to setitimer(2) fails. +static void +safe_setitimer(const datetime::delta& delta, itimerval* old_timeval) +{ + ::itimerval timeval; + timerclear(&timeval.it_interval); + timeval.it_value.tv_sec = delta.seconds; + timeval.it_value.tv_usec = delta.useconds; + + if (::setitimer(ITIMER_REAL, &timeval, old_timeval) == -1) { + const int original_errno = errno; + throw signals::system_error("Failed to program system's interval timer", + original_errno); + } +} + + +/// Deadline scheduler for all user timers on top of the unique system timer. +class global_state : utils::noncopyable { + /// Collection of active timers. + /// + /// Because this is a collection of pointers, all timers are guaranteed to + /// be unique, and we want all of these pointers to be valid. + typedef std::set< signals::timer* > timers_set; + + /// Sequence of ordered timers. + typedef std::vector< signals::timer* > timers_vector; + + /// Collection of active timestamps by their activation timestamp. + /// + /// This collection is ordered intentionally so that it can be scanned + /// sequentially to find either expired or expiring-now timers. + typedef std::map< datetime::timestamp, timers_set > timers_by_timestamp_map; + + /// The original timer before any timer was programmed. + ::itimerval _old_timeval; + + /// Programmer for the SIGALRM handler. + std::auto_ptr< signals::programmer > _sigalrm_programmer; + + /// Time of the current activation of the timer. + datetime::timestamp _timer_activation; + + /// Mapping of all active timers using their timestamp as the key. + timers_by_timestamp_map _all_timers; + + /// Adds a timer to the _all_timers map. + /// + /// \param timer The timer to add. + void + add_to_all_timers(signals::timer* timer) + { + timers_set& timers = _all_timers[timer->when()]; + INV(timers.find(timer) == timers.end()); + timers.insert(timer); + } + + /// Removes a timer from the _all_timers map. + /// + /// This ensures that empty vectors are removed from _all_timers if the + /// removal of the timer causes its bucket to be emptied. + /// + /// \param timer The timer to remove. + void + remove_from_all_timers(signals::timer* timer) + { + // We may not find the timer in _all_timers if the timer has fired, + // because fire() took it out from the map. + timers_by_timestamp_map::iterator iter = _all_timers.find( + timer->when()); + if (iter != _all_timers.end()) { + timers_set& timers = (*iter).second; + INV(timers.find(timer) != timers.end()); + timers.erase(timer); + if (timers.empty()) { + _all_timers.erase(iter); + } + } + } + + /// Calculates all timers to execute at this timestamp. + /// + /// \param now The current timestamp. + /// + /// \post _all_timers is updated to contain only the timers that are + /// strictly in the future. + /// + /// \return A sequence of valid timers that need to be invoked in the order + /// of activation. These are all previously registered timers with + /// activations in the past. + timers_vector + compute_timers_to_run_and_prune_old( + const datetime::timestamp& now, + const signals::interrupts_inhibiter& /* inhibiter */) + { + timers_vector to_run; + + timers_by_timestamp_map::iterator iter = _all_timers.begin(); + while (iter != _all_timers.end() && (*iter).first <= now) { + const timers_set& timers = (*iter).second; + to_run.insert(to_run.end(), timers.begin(), timers.end()); + + // Remove expired entries here so that we can always assume that + // the first entry in all_timers corresponds to the next + // activation. + const timers_by_timestamp_map::iterator previous_iter = iter; + ++iter; + _all_timers.erase(previous_iter); + } + + return to_run; + } + + /// Adjusts the global system timer to point to the next activation. + /// + /// \param now The current timestamp. + /// + /// \throw system_error If the programming fails. + void + reprogram_system_timer( + const datetime::timestamp& now, + const signals::interrupts_inhibiter& /* inhibiter */) + { + if (_all_timers.empty()) { + // Nothing to do. We can reach this case if all the existing timers + // are in the past and they all fired. Just ignore the request and + // leave the global timer as is. + return; + } + + // While fire() prunes old entries from the list of timers, it is + // possible for this routine to run with "expired" timers (i.e. timers + // whose deadline lies in the past but that have not yet fired for + // whatever reason that is out of our control) in the list. We have to + // iterate until we find the next activation instead of assuming that + // the first entry represents the desired value. + timers_by_timestamp_map::const_iterator iter = _all_timers.begin(); + PRE(!(*iter).second.empty()); + datetime::timestamp next = (*iter).first; + while (next < now) { + ++iter; + if (iter == _all_timers.end()) { + // Nothing to do. We can reach this case if all the existing + // timers are in the past but they have not yet fired. + return; + } + PRE(!(*iter).second.empty()); + next = (*iter).first; + } + + if (next < _timer_activation || now > _timer_activation) { + INV(next >= now); + const datetime::delta delta = next - now; + LD(F("Reprogramming timer; firing on %s; now is %s") % next % now); + safe_setitimer(delta, NULL); + _timer_activation = next; + } + } + +public: + /// Programs the first timer. + /// + /// The programming of the first timer involves setting up the SIGALRM + /// handler and installing a timer handler for the first time, which in turn + /// involves keeping track of the old handlers so that we can restore them. + /// + /// \param timer The timer being programmed. + /// \param now The current timestamp. + /// + /// \throw system_error If the programming fails. + global_state(signals::timer* timer, const datetime::timestamp& now) : + _timer_activation(timer->when()) + { + PRE(now < timer->when()); + + signals::interrupts_inhibiter inhibiter; + + const datetime::delta delta = timer->when() - now; + LD(F("Installing first timer; firing on %s; now is %s") % + timer->when() % now); + + _sigalrm_programmer.reset( + new signals::programmer(SIGALRM, sigalrm_handler)); + try { + safe_setitimer(delta, &_old_timeval); + _timer_activation = timer->when(); + add_to_all_timers(timer); + } catch (...) { + _sigalrm_programmer.reset(NULL); + throw; + } + } + + /// Unprograms all timers. + /// + /// This clears the global system timer and unsets the SIGALRM handler. + ~global_state(void) + { + signals::interrupts_inhibiter inhibiter; + + LD("Unprogramming all timers"); + + if (::setitimer(ITIMER_REAL, &_old_timeval, NULL) == -1) { + UNREACHABLE_MSG("Failed to restore original timer"); + } + + _sigalrm_programmer->unprogram(); + _sigalrm_programmer.reset(NULL); + } + + /// Programs a new timer, possibly adjusting the global system timer. + /// + /// Programming any timer other than the first one only involves reloading + /// the existing timer, not backing up the previous handler nor installing a + /// handler for SIGALRM. + /// + /// \param timer The timer being programmed. + /// \param now The current timestamp. + /// + /// \throw system_error If the programming fails. + void + program_new(signals::timer* timer, const datetime::timestamp& now) + { + signals::interrupts_inhibiter inhibiter; + + add_to_all_timers(timer); + reprogram_system_timer(now, inhibiter); + } + + /// Unprograms a timer. + /// + /// This removes the timer from the global state and reprograms the global + /// system timer if necessary. + /// + /// \param timer The timer to unprogram. + /// + /// \return True if the system interval timer has been reprogrammed to + /// another future timer; false if there are no more active timers. + bool + unprogram(signals::timer* timer) + { + signals::interrupts_inhibiter inhibiter; + + LD(F("Unprogramming timer; previously firing on %s") % timer->when()); + + remove_from_all_timers(timer); + if (_all_timers.empty()) { + return false; + } else { + reprogram_system_timer(datetime::timestamp::now(), inhibiter); + return true; + } + } + + /// Executes active timers. + /// + /// Active timers are all those that fire on or before 'now'. + /// + /// \param now The current time. + void + fire(const datetime::timestamp& now) + { + timers_vector to_run; + { + signals::interrupts_inhibiter inhibiter; + to_run = compute_timers_to_run_and_prune_old(now, inhibiter); + reprogram_system_timer(now, inhibiter); + } + + for (timers_vector::iterator iter = to_run.begin(); + iter != to_run.end(); ++iter) { + signals::detail::invoke_do_fired(*iter); + } + } +}; + + +/// Unique instance of the global state. +static std::auto_ptr< global_state > globals; + + +/// SIGALRM handler for the timer implementation. +/// +/// \param signo The signal received; must be SIGALRM. +static void +sigalrm_handler(const int signo) +{ + PRE(signo == SIGALRM); + globals->fire(datetime::timestamp::now()); +} + + +} // anonymous namespace + + +/// Indirection to invoke the private do_fired() method of a timer. +/// +/// \param timer The timer on which to run do_fired(). +void +utils::signals::detail::invoke_do_fired(timer* timer) +{ + timer->do_fired(); +} + + +/// Internal implementation for the timer. +/// +/// We assume that there is a 1-1 mapping between timer objects and impl +/// objects. If this assumption breaks, then the rest of the code in this +/// module breaks as well because we use pointers to the parent timer as the +/// identifier of the timer. +struct utils::signals::timer::impl : utils::noncopyable { + /// Timestamp when this timer is expected to fire. + /// + /// Note that the timer might be processed after this timestamp, so users of + /// this field need to check for timers that fire on or before the + /// activation time. + datetime::timestamp when; + + /// True until unprogram() is called. + bool programmed; + + /// Whether this timer has fired already or not. + /// + /// This is updated from an interrupt context, hence why it is marked + /// volatile. + volatile bool fired; + + /// Constructor. + /// + /// \param when_ Timestamp when this timer is expected to fire. + impl(const datetime::timestamp& when_) : + when(when_), programmed(true), fired(false) + { + } + + /// Destructor. + ~impl(void) { + } +}; + + +/// Constructor; programs a run-once timer. +/// +/// This programs the global timer and signal handler if this is the first timer +/// being installed. Otherwise, reprograms the global timer if this timer +/// expires earlier than all other active timers. +/// +/// \param delta The time until the timer fires. +signals::timer::timer(const datetime::delta& delta) +{ + signals::interrupts_inhibiter inhibiter; + + const datetime::timestamp now = datetime::timestamp::now(); + _pimpl.reset(new impl(now + delta)); + if (globals.get() == NULL) { + globals.reset(new global_state(this, now)); + } else { + globals->program_new(this, now); + } +} + + +/// Destructor; unprograms the timer if still programmed. +/// +/// Given that this is a destructor and it can't report errors back to the +/// caller, the caller must attempt to call unprogram() on its own. This is +/// extremely important because, otherwise, expired timers will never run! +signals::timer::~timer(void) +{ + signals::interrupts_inhibiter inhibiter; + + if (_pimpl->programmed) { + LW("Auto-destroying still-programmed signals::timer object"); + try { + unprogram(); + } catch (const system_error& e) { + UNREACHABLE; + } + } + + if (!_pimpl->fired) { + const datetime::timestamp now = datetime::timestamp::now(); + if (now > _pimpl->when) { + LW("Expired timer never fired; the code never called unprogram()!"); + } + } +} + + +/// Returns the time of the timer activation. +/// +/// \return A timestamp that has no relation to the current time (i.e. can be in +/// the future or in the past) nor the timer's activation status. +const datetime::timestamp& +signals::timer::when(void) const +{ + return _pimpl->when; +} + + +/// Callback for the SIGALRM handler when this timer expires. +/// +/// \warning This is executed from a signal handler context without signals +/// inhibited. See signal(7) for acceptable system calls. +void +signals::timer::do_fired(void) +{ + PRE(!_pimpl->fired); + _pimpl->fired = true; + callback(); +} + + +/// User-provided callback to run when the timer expires. +/// +/// The default callback does nothing. We record the activation of the timer +/// separately, which may be appropriate in the majority of the cases. +/// +/// \warning This is executed from a signal handler context without signals +/// inhibited. See signal(7) for acceptable system calls. +void +signals::timer::callback(void) +{ + // Intentionally left blank. +} + + +/// Checks whether the timer has fired already or not. +/// +/// \return Returns true if the timer has fired. +bool +signals::timer::fired(void) const +{ + return _pimpl->fired; +} + + +/// Unprograms the timer. +/// +/// \pre The timer is programmed (i.e. this can only be called once). +/// +/// \post If the timer never fired asynchronously because the signal delivery +/// did not arrive on time, make sure we invoke the timer's callback here. +/// +/// \throw system_error If unprogramming the timer failed. +void +signals::timer::unprogram(void) +{ + signals::interrupts_inhibiter inhibiter; + + if (!_pimpl->programmed) { + // We cannot assert that the timer is not programmed because it might + // have been unprogrammed asynchronously between the time we called + // unprogram() and the time we reach this. Simply return in this case. + LD("Called unprogram on already-unprogrammed timer; possibly just " + "a race"); + return; + } + + if (!globals->unprogram(this)) { + globals.reset(NULL); + } + _pimpl->programmed = false; + + // Handle the case where the timer has expired before we ever got its + // corresponding signal. Do so by invoking its callback now. + if (!_pimpl->fired) { + const datetime::timestamp now = datetime::timestamp::now(); + if (now > _pimpl->when) { + LW(F("Firing expired timer on destruction (was to fire on %s)") % + _pimpl->when); + do_fired(); + } + } +} diff --git a/utils/signals/timer.hpp b/utils/signals/timer.hpp new file mode 100644 index 000000000000..1174effe2b48 --- /dev/null +++ b/utils/signals/timer.hpp @@ -0,0 +1,86 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/signals/timer.hpp +/// Multiprogrammed support for timers. +/// +/// The timer module and class implement a mechanism to program multiple timers +/// concurrently by using a deadline scheduler and leveraging the "single timer" +/// features of the underlying operating system. + +#if !defined(UTILS_SIGNALS_TIMER_HPP) +#define UTILS_SIGNALS_TIMER_HPP + +#include "utils/signals/timer_fwd.hpp" + +#include + +#include "utils/datetime_fwd.hpp" +#include "utils/noncopyable.hpp" + +namespace utils { +namespace signals { + + +namespace detail { +void invoke_do_fired(timer*); +} // namespace detail + + +/// Individual timer. +/// +/// Asynchronously executes its callback() method, which can be overridden by +/// subclasses, when the timeout given at construction expires. +class timer : noncopyable { + struct impl; + + /// Pointer to the shared internal implementation. + std::auto_ptr< impl > _pimpl; + + friend void detail::invoke_do_fired(timer*); + void do_fired(void); + +protected: + virtual void callback(void); + +public: + timer(const utils::datetime::delta&); + virtual ~timer(void); + + const utils::datetime::timestamp& when(void) const; + + bool fired(void) const; + + void unprogram(void); +}; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_TIMER_HPP) diff --git a/utils/signals/timer_fwd.hpp b/utils/signals/timer_fwd.hpp new file mode 100644 index 000000000000..a3cf3e205d70 --- /dev/null +++ b/utils/signals/timer_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/signals/timer_fwd.hpp +/// Forward declarations for utils/signals/timer.hpp + +#if !defined(UTILS_SIGNALS_TIMER_FWD_HPP) +#define UTILS_SIGNALS_TIMER_FWD_HPP + +namespace utils { +namespace signals { + + +class timer; + + +} // namespace signals +} // namespace utils + +#endif // !defined(UTILS_SIGNALS_TIMER_FWD_HPP) diff --git a/utils/signals/timer_test.cpp b/utils/signals/timer_test.cpp new file mode 100644 index 000000000000..61e9cac6b088 --- /dev/null +++ b/utils/signals/timer_test.cpp @@ -0,0 +1,426 @@ +// Copyright 2010 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/signals/timer.hpp" + +extern "C" { +#include +#include +} + +#include +#include +#include + +#include + +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/containers.ipp" +#include "utils/format/macros.hpp" +#include "utils/signals/interrupts.hpp" +#include "utils/signals/programmer.hpp" + +namespace datetime = utils::datetime; +namespace signals = utils::signals; + + +namespace { + + +/// A timer that inserts an element into a vector on activation. +class delayed_inserter : public signals::timer { + /// Vector into which to insert the element. + std::vector< int >& _destination; + + /// Element to insert into _destination on activation. + const int _item; + + /// Timer activation callback. + void + callback(void) + { + signals::interrupts_inhibiter inhibiter; + _destination.push_back(_item); + } + +public: + /// Constructor. + /// + /// \param delta Time to the timer activation. + /// \param destination Vector into which to insert the element. + /// \param item Element to insert into destination on activation. + delayed_inserter(const datetime::delta& delta, + std::vector< int >& destination, const int item) : + signals::timer(delta), _destination(destination), _item(item) + { + } +}; + + +/// Signal handler that does nothing. +static void +null_handler(const int /* signo */) +{ +} + + +/// Waits for the activation of all given timers. +/// +/// \param timers Pointers to all the timers to wait for. +static void +wait_timers(const std::vector< signals::timer* >& timers) +{ + std::size_t n_fired, old_n_fired = 0; + do { + n_fired = 0; + for (std::vector< signals::timer* >::const_iterator + iter = timers.begin(); iter != timers.end(); ++iter) { + const signals::timer* timer = *iter; + if (timer->fired()) + ++n_fired; + } + if (old_n_fired < n_fired) { + std::cout << "Waiting; " << n_fired << " timers fired so far\n"; + old_n_fired = n_fired; + } + ::usleep(100); + } while (n_fired < timers.size()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE(program_seconds); +ATF_TEST_CASE_HEAD(program_seconds) +{ + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(program_seconds) +{ + signals::timer timer(datetime::delta(1, 0)); + ATF_REQUIRE(!timer.fired()); + while (!timer.fired()) + ::usleep(1000); +} + + +ATF_TEST_CASE(program_useconds); +ATF_TEST_CASE_HEAD(program_useconds) +{ + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(program_useconds) +{ + signals::timer timer(datetime::delta(0, 500000)); + ATF_REQUIRE(!timer.fired()); + while (!timer.fired()) + ::usleep(1000); +} + + +ATF_TEST_CASE(multiprogram_ordered); +ATF_TEST_CASE_HEAD(multiprogram_ordered) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(multiprogram_ordered) +{ + static const std::size_t n_timers = 100; + + std::vector< signals::timer* > timers; + std::vector< int > items, exp_items; + + const int initial_delay_ms = 1000000; + for (std::size_t i = 0; i < n_timers; ++i) { + exp_items.push_back(i); + + timers.push_back(new delayed_inserter( + datetime::delta(0, initial_delay_ms + (i + 1) * 10000), + items, i)); + ATF_REQUIRE(!timers[i]->fired()); + } + + wait_timers(timers); + + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(multiprogram_reorder_next_activations); +ATF_TEST_CASE_HEAD(multiprogram_reorder_next_activations) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(multiprogram_reorder_next_activations) +{ + std::vector< signals::timer* > timers; + std::vector< int > items; + + // First timer with an activation in the future. + timers.push_back(new delayed_inserter( + datetime::delta(0, 100000), items, 1)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + // Timer with an activation earlier than the previous one. + timers.push_back(new delayed_inserter( + datetime::delta(0, 50000), items, 2)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + // Timer with an activation later than all others. + timers.push_back(new delayed_inserter( + datetime::delta(0, 200000), items, 3)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + // Timer with an activation in between. + timers.push_back(new delayed_inserter( + datetime::delta(0, 150000), items, 4)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + wait_timers(timers); + + std::vector< int > exp_items; + exp_items.push_back(2); + exp_items.push_back(1); + exp_items.push_back(4); + exp_items.push_back(3); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(multiprogram_and_cancel_some); +ATF_TEST_CASE_HEAD(multiprogram_and_cancel_some) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(multiprogram_and_cancel_some) +{ + std::vector< signals::timer* > timers; + std::vector< int > items; + + // First timer with an activation in the future. + timers.push_back(new delayed_inserter( + datetime::delta(0, 100000), items, 1)); + + // Timer with an activation earlier than the previous one. + timers.push_back(new delayed_inserter( + datetime::delta(0, 50000), items, 2)); + + // Timer with an activation later than all others. + timers.push_back(new delayed_inserter( + datetime::delta(0, 200000), items, 3)); + + // Timer with an activation in between. + timers.push_back(new delayed_inserter( + datetime::delta(0, 150000), items, 4)); + + // Cancel the first timer to reprogram next activation. + timers[1]->unprogram(); delete timers[1]; timers.erase(timers.begin() + 1); + + // Cancel another timer without reprogramming next activation. + timers[2]->unprogram(); delete timers[2]; timers.erase(timers.begin() + 2); + + wait_timers(timers); + + std::vector< int > exp_items; + exp_items.push_back(1); + exp_items.push_back(3); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(multiprogram_and_expire_before_activations); +ATF_TEST_CASE_HEAD(multiprogram_and_expire_before_activations) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(multiprogram_and_expire_before_activations) +{ + std::vector< signals::timer* > timers; + std::vector< int > items; + + { + signals::interrupts_inhibiter inhibiter; + + // First timer with an activation in the future. + timers.push_back(new delayed_inserter( + datetime::delta(0, 100000), items, 1)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + // Timer with an activation earlier than the previous one. + timers.push_back(new delayed_inserter( + datetime::delta(0, 50000), items, 2)); + ATF_REQUIRE(!timers[timers.size() - 1]->fired()); + + ::sleep(1); + + // Timer with an activation later than all others. + timers.push_back(new delayed_inserter( + datetime::delta(0, 200000), items, 3)); + + ::sleep(1); + } + + wait_timers(timers); + + std::vector< int > exp_items; + exp_items.push_back(2); + exp_items.push_back(1); + exp_items.push_back(3); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(expire_before_firing); +ATF_TEST_CASE_HEAD(expire_before_firing) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(expire_before_firing) +{ + std::vector< int > items; + + // The code below causes a signal to go pending. Make sure we ignore it + // when we unblock signals. + signals::programmer sigalrm(SIGALRM, null_handler); + + { + signals::interrupts_inhibiter inhibiter; + + delayed_inserter* timer = new delayed_inserter( + datetime::delta(0, 1000), items, 1234); + ::sleep(1); + // Interrupts are inhibited so we never got a chance to execute the + // timer before it was destroyed. However, the handler should run + // regardless at some point, possibly during deletion. + timer->unprogram(); + delete timer; + } + + std::vector< int > exp_items; + exp_items.push_back(1234); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(reprogram_from_scratch); +ATF_TEST_CASE_HEAD(reprogram_from_scratch) +{ + set_md_var("timeout", "20"); +} +ATF_TEST_CASE_BODY(reprogram_from_scratch) +{ + std::vector< int > items; + + delayed_inserter* timer1 = new delayed_inserter( + datetime::delta(0, 100000), items, 1); + timer1->unprogram(); delete timer1; + + // All constructed timers are now dead, so the interval timer should have + // been reprogrammed. Let's start over. + + delayed_inserter* timer2 = new delayed_inserter( + datetime::delta(0, 200000), items, 2); + while (!timer2->fired()) + ::usleep(1000); + timer2->unprogram(); delete timer2; + + std::vector< int > exp_items; + exp_items.push_back(2); + ATF_REQUIRE_EQ(exp_items, items); +} + + +ATF_TEST_CASE(unprogram); +ATF_TEST_CASE_HEAD(unprogram) +{ + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(unprogram) +{ + signals::timer timer(datetime::delta(0, 500000)); + timer.unprogram(); + usleep(500000); + ATF_REQUIRE(!timer.fired()); +} + + +ATF_TEST_CASE(infinitesimal); +ATF_TEST_CASE_HEAD(infinitesimal) +{ + set_md_var("descr", "Ensure that the ordering in which the signal, the " + "timer and the global state are programmed is correct; do so " + "by setting an extremely small delay for the timer hoping that " + "it can trigger such conditions"); + set_md_var("timeout", "10"); +} +ATF_TEST_CASE_BODY(infinitesimal) +{ + const std::size_t rounds = 100; + const std::size_t exp_good = 90; + + std::size_t good = 0; + for (std::size_t i = 0; i < rounds; i++) { + signals::timer timer(datetime::delta(0, 1)); + + // From the setitimer(2) documentation: + // + // Time values smaller than the resolution of the system clock are + // rounded up to this resolution (typically 10 milliseconds). + // + // We don't know what this resolution is but we must wait for longer + // than we programmed; do a rough guess and hope it is good. This may + // be obviously wrong and thus lead to mysterious test failures in some + // systems, hence why we only expect a percentage of successes below. + // Still, we can fail... + ::usleep(1000); + + if (timer.fired()) + ++good; + timer.unprogram(); + } + std::cout << F("Ran %s tests, %s passed; threshold is %s\n") + % rounds % good % exp_good; + ATF_REQUIRE(good >= exp_good); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, program_seconds); + ATF_ADD_TEST_CASE(tcs, program_useconds); + ATF_ADD_TEST_CASE(tcs, multiprogram_ordered); + ATF_ADD_TEST_CASE(tcs, multiprogram_reorder_next_activations); + ATF_ADD_TEST_CASE(tcs, multiprogram_and_cancel_some); + ATF_ADD_TEST_CASE(tcs, multiprogram_and_expire_before_activations); + ATF_ADD_TEST_CASE(tcs, expire_before_firing); + ATF_ADD_TEST_CASE(tcs, reprogram_from_scratch); + ATF_ADD_TEST_CASE(tcs, unprogram); + ATF_ADD_TEST_CASE(tcs, infinitesimal); +} diff --git a/utils/sqlite/Kyuafile b/utils/sqlite/Kyuafile new file mode 100644 index 000000000000..47a8b95dac92 --- /dev/null +++ b/utils/sqlite/Kyuafile @@ -0,0 +1,9 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="c_gate_test"} +atf_test_program{name="database_test"} +atf_test_program{name="exceptions_test"} +atf_test_program{name="statement_test"} +atf_test_program{name="transaction_test"} diff --git a/utils/sqlite/Makefile.am.inc b/utils/sqlite/Makefile.am.inc new file mode 100644 index 000000000000..6064a641c14f --- /dev/null +++ b/utils/sqlite/Makefile.am.inc @@ -0,0 +1,82 @@ +# Copyright 2011 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +UTILS_CFLAGS += $(SQLITE3_CFLAGS) +UTILS_LIBS += $(SQLITE3_LIBS) + +libutils_a_CPPFLAGS += $(SQLITE3_CFLAGS) +libutils_a_SOURCES += utils/sqlite/c_gate.cpp +libutils_a_SOURCES += utils/sqlite/c_gate.hpp +libutils_a_SOURCES += utils/sqlite/c_gate_fwd.hpp +libutils_a_SOURCES += utils/sqlite/database.cpp +libutils_a_SOURCES += utils/sqlite/database.hpp +libutils_a_SOURCES += utils/sqlite/database_fwd.hpp +libutils_a_SOURCES += utils/sqlite/exceptions.cpp +libutils_a_SOURCES += utils/sqlite/exceptions.hpp +libutils_a_SOURCES += utils/sqlite/statement.cpp +libutils_a_SOURCES += utils/sqlite/statement.hpp +libutils_a_SOURCES += utils/sqlite/statement_fwd.hpp +libutils_a_SOURCES += utils/sqlite/statement.ipp +libutils_a_SOURCES += utils/sqlite/transaction.cpp +libutils_a_SOURCES += utils/sqlite/transaction.hpp +libutils_a_SOURCES += utils/sqlite/transaction_fwd.hpp + +if WITH_ATF +tests_utils_sqlitedir = $(pkgtestsdir)/utils/sqlite + +tests_utils_sqlite_DATA = utils/sqlite/Kyuafile +EXTRA_DIST += $(tests_utils_sqlite_DATA) + +tests_utils_sqlite_PROGRAMS = utils/sqlite/c_gate_test +utils_sqlite_c_gate_test_SOURCES = utils/sqlite/c_gate_test.cpp \ + utils/sqlite/test_utils.hpp +utils_sqlite_c_gate_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_c_gate_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_sqlite_PROGRAMS += utils/sqlite/database_test +utils_sqlite_database_test_SOURCES = utils/sqlite/database_test.cpp \ + utils/sqlite/test_utils.hpp +utils_sqlite_database_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_database_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_sqlite_PROGRAMS += utils/sqlite/exceptions_test +utils_sqlite_exceptions_test_SOURCES = utils/sqlite/exceptions_test.cpp +utils_sqlite_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_sqlite_PROGRAMS += utils/sqlite/statement_test +utils_sqlite_statement_test_SOURCES = utils/sqlite/statement_test.cpp \ + utils/sqlite/test_utils.hpp +utils_sqlite_statement_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_statement_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_sqlite_PROGRAMS += utils/sqlite/transaction_test +utils_sqlite_transaction_test_SOURCES = utils/sqlite/transaction_test.cpp +utils_sqlite_transaction_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_sqlite_transaction_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/sqlite/c_gate.cpp b/utils/sqlite/c_gate.cpp new file mode 100644 index 000000000000..e89ac5332ea0 --- /dev/null +++ b/utils/sqlite/c_gate.cpp @@ -0,0 +1,83 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/c_gate.hpp" + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" + +namespace sqlite = utils::sqlite; + +using utils::none; + + +/// Creates a new gateway to an existing C++ SQLite database. +/// +/// \param database_ The database to connect to. This object must remain alive +/// while the newly-constructed database_c_gate is alive. +sqlite::database_c_gate::database_c_gate(database& database_) : + _database(database_) +{ +} + + +/// Destructor. +/// +/// Destroying this object has no implications on the life cycle of the SQLite +/// database. Only the corresponding database object controls when the SQLite 3 +/// database is closed. +sqlite::database_c_gate::~database_c_gate(void) +{ +} + + +/// Creates a C++ database for a C SQLite 3 database. +/// +/// \warning The created database object does NOT own the C database. You must +/// take care to properly destroy the input sqlite3 when you are done with it to +/// not leak resources. +/// +/// \param raw_database The raw database to wrap temporarily. +/// +/// \return The wrapped database without strong ownership on the input database. +sqlite::database +sqlite::database_c_gate::connect(::sqlite3* raw_database) +{ + return database(none, static_cast< void* >(raw_database), false); +} + + +/// Returns the C native SQLite 3 database. +/// +/// \return A native sqlite3 object holding the SQLite 3 C API database. +::sqlite3* +sqlite::database_c_gate::c_database(void) +{ + return static_cast< ::sqlite3* >(_database.raw_database()); +} diff --git a/utils/sqlite/c_gate.hpp b/utils/sqlite/c_gate.hpp new file mode 100644 index 000000000000..0ca9d79c4815 --- /dev/null +++ b/utils/sqlite/c_gate.hpp @@ -0,0 +1,74 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file c_gate.hpp +/// Provides direct access to the C state of the SQLite wrappers. + +#if !defined(UTILS_SQLITE_C_GATE_HPP) +#define UTILS_SQLITE_C_GATE_HPP + +#include "utils/sqlite/c_gate_fwd.hpp" + +extern "C" { +#include +} + +#include "utils/sqlite/database_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// Gateway to the raw C database of SQLite 3. +/// +/// This class provides a mechanism to muck with the internals of the database +/// wrapper class. +/// +/// \warning The use of this class is discouraged. By using this class, you are +/// entering the world of unsafety. Anything you do through the objects exposed +/// through this class will not be controlled by RAII patterns not validated in +/// any other way, so you can end up corrupting the SQLite 3 state and later get +/// crashes on otherwise perfectly-valid C++ code. +class database_c_gate { + /// The C++ database that this class wraps. + database& _database; + +public: + database_c_gate(database&); + ~database_c_gate(void); + + static database connect(::sqlite3*); + + ::sqlite3* c_database(void); +}; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_C_GATE_HPP) diff --git a/utils/sqlite/c_gate_fwd.hpp b/utils/sqlite/c_gate_fwd.hpp new file mode 100644 index 000000000000..771efeeff463 --- /dev/null +++ b/utils/sqlite/c_gate_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sqlite/c_gate_fwd.hpp +/// Forward declarations for utils/sqlite/c_gate.hpp + +#if !defined(UTILS_SQLITE_C_GATE_FWD_HPP) +#define UTILS_SQLITE_C_GATE_FWD_HPP + +namespace utils { +namespace sqlite { + + +class database_c_gate; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_C_GATE_FWD_HPP) diff --git a/utils/sqlite/c_gate_test.cpp b/utils/sqlite/c_gate_test.cpp new file mode 100644 index 000000000000..edf46f76c902 --- /dev/null +++ b/utils/sqlite/c_gate_test.cpp @@ -0,0 +1,96 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/c_gate.hpp" + +#include + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/test_utils.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE_WITHOUT_HEAD(connect); +ATF_TEST_CASE_BODY(connect) +{ + ::sqlite3* raw_db; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_open_v2(":memory:", &raw_db, + SQLITE_OPEN_READWRITE, NULL)); + { + sqlite::database database = sqlite::database_c_gate::connect(raw_db); + create_test_table(raw(database)); + } + // If the wrapper object has closed the SQLite 3 database, we will misbehave + // here either by crashing or not finding our test table. + verify_test_table(raw_db); + ::sqlite3_close(raw_db); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(c_database); +ATF_TEST_CASE_BODY(c_database) +{ + sqlite::database db = sqlite::database::in_memory(); + create_test_table(raw(db)); + { + sqlite::database_c_gate gate(db); + ::sqlite3* raw_db = gate.c_database(); + verify_test_table(raw_db); + } +} + + +ATF_TEST_CASE(database__db_filename); +ATF_TEST_CASE_HEAD(database__db_filename) +{ + set_md_var("descr", "The current implementation of db_filename() has no " + "means to access the filename of a database connected to a raw " + "sqlite3 object"); +} +ATF_TEST_CASE_BODY(database__db_filename) +{ + ::sqlite3* raw_db; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_open_v2( + "test.db", &raw_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL)); + + sqlite::database database = sqlite::database_c_gate::connect(raw_db); + ATF_REQUIRE(!database.db_filename()); + ::sqlite3_close(raw_db); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, c_database); + ATF_ADD_TEST_CASE(tcs, connect); + ATF_ADD_TEST_CASE(tcs, database__db_filename); +} diff --git a/utils/sqlite/database.cpp b/utils/sqlite/database.cpp new file mode 100644 index 000000000000..41935c3b017d --- /dev/null +++ b/utils/sqlite/database.cpp @@ -0,0 +1,328 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/database.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" +#include "utils/sqlite/transaction.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::none; +using utils::optional; + + +/// Internal implementation for sqlite::database. +struct utils::sqlite::database::impl : utils::noncopyable { + /// Path to the database as seen at construction time. + optional< fs::path > db_filename; + + /// The SQLite 3 internal database. + ::sqlite3* db; + + /// Whether we own the database or not (to decide if we close it). + bool owned; + + /// Constructor. + /// + /// \param db_filename_ The path to the database as seen at construction + /// time, if any, or none for in-memory databases. We should use + /// sqlite3_db_filename instead, but this function appeared in 3.7.10 + /// and Ubuntu 12.04 LTS (which we support for Travis CI builds as of + /// 2015-07-07) ships with 3.7.9. + /// \param db_ The SQLite internal database. + /// \param owned_ Whether this object owns the db_ object or not. If it + /// does, the internal db_ will be released during destruction. + impl(optional< fs::path > db_filename_, ::sqlite3* db_, const bool owned_) : + db_filename(db_filename_), db(db_), owned(owned_) + { + } + + /// Destructor. + /// + /// It is important to keep this as part of the 'impl' class instead of the + /// container class. The 'impl' class is destroyed exactly once (because it + /// is managed by a shared_ptr) and thus releasing the resources here is + /// OK. However, the container class is potentially released many times, + /// which means that we would be double-freeing the internal object and + /// reusing invalid data. + ~impl(void) + { + if (owned && db != NULL) + close(); + } + + /// Exception-safe version of sqlite3_open_v2. + /// + /// \param file The path to the database file to be opened. + /// \param flags The flags to be passed to the open routine. + /// + /// \return The opened database. + /// + /// \throw std::bad_alloc If there is not enough memory to open the + /// database. + /// \throw api_error If there is any problem opening the database. + static ::sqlite3* + safe_open(const char* file, const int flags) + { + ::sqlite3* db; + const int error = ::sqlite3_open_v2(file, &db, flags, NULL); + if (error != SQLITE_OK) { + if (db == NULL) + throw std::bad_alloc(); + else { + sqlite::database error_db(utils::make_optional(fs::path(file)), + db, true); + throw sqlite::api_error::from_database(error_db, + "sqlite3_open_v2"); + } + } + INV(db != NULL); + return db; + } + + /// Shared code for the public close() method. + void + close(void) + { + PRE(db != NULL); + int error = ::sqlite3_close(db); + // For now, let's consider a return of SQLITE_BUSY an error. We should + // not be trying to close a busy database in our code. Maybe revisit + // this later to raise busy errors as exceptions. + PRE(error == SQLITE_OK); + db = NULL; + } +}; + + +/// Initializes the SQLite database. +/// +/// You must share the same database object alongside the lifetime of your +/// SQLite session. As soon as the object is destroyed, the session is +/// terminated. +/// +/// \param db_filename_ The path to the database as seen at construction +/// time, if any, or none for in-memory databases. +/// \param db_ Raw pointer to the C SQLite 3 object. +/// \param owned_ Whether this instance will own the pointer or not. +sqlite::database::database( + const utils::optional< utils::fs::path >& db_filename_, void* db_, + const bool owned_) : + _pimpl(new impl(db_filename_, static_cast< ::sqlite3* >(db_), owned_)) +{ +} + + +/// Destructor for the SQLite 3 database. +/// +/// Closes the session unless it has already been closed by calling the +/// close() method. It is recommended to explicitly close the session in the +/// code. +sqlite::database::~database(void) +{ +} + + +/// Opens a memory-based temporary SQLite database. +/// +/// \return An in-memory database instance. +/// +/// \throw std::bad_alloc If there is not enough memory to open the database. +/// \throw api_error If there is any problem opening the database. +sqlite::database +sqlite::database::in_memory(void) +{ + return database(none, impl::safe_open(":memory:", SQLITE_OPEN_READWRITE), + true); +} + + +/// Opens a named on-disk SQLite database. +/// +/// \param file The path to the database file to be opened. This does not +/// accept the values "" and ":memory:"; use temporary() and in_memory() +/// instead. +/// \param open_flags The flags to be passed to the open routine. +/// +/// \return A file-backed database instance. +/// +/// \throw std::bad_alloc If there is not enough memory to open the database. +/// \throw api_error If there is any problem opening the database. +sqlite::database +sqlite::database::open(const fs::path& file, int open_flags) +{ + PRE_MSG(!file.str().empty(), "Use database::temporary() instead"); + PRE_MSG(file.str() != ":memory:", "Use database::in_memory() instead"); + + int flags = 0; + if (open_flags & open_readonly) { + flags |= SQLITE_OPEN_READONLY; + open_flags &= ~open_readonly; + } + if (open_flags & open_readwrite) { + flags |= SQLITE_OPEN_READWRITE; + open_flags &= ~open_readwrite; + } + if (open_flags & open_create) { + flags |= SQLITE_OPEN_CREATE; + open_flags &= ~open_create; + } + PRE(open_flags == 0); + + return database(utils::make_optional(file), + impl::safe_open(file.c_str(), flags), true); +} + + +/// Opens an unnamed on-disk SQLite database. +/// +/// \return A file-backed database instance. +/// +/// \throw std::bad_alloc If there is not enough memory to open the database. +/// \throw api_error If there is any problem opening the database. +sqlite::database +sqlite::database::temporary(void) +{ + return database(none, impl::safe_open("", SQLITE_OPEN_READWRITE), true); +} + + +/// Gets the internal sqlite3 object. +/// +/// \return The raw SQLite 3 database. This is returned as a void pointer to +/// prevent including the sqlite3.h header file from our public interface. The +/// only way to call this method is by using the c_gate module, and c_gate takes +/// care of casting this object to the appropriate type. +void* +sqlite::database::raw_database(void) +{ + return _pimpl->db; +} + + +/// Terminates the connection to the database. +/// +/// It is recommended to call this instead of relying on the destructor to do +/// the cleanup, but it is not a requirement to use close(). +/// +/// \pre close() has not yet been called. +void +sqlite::database::close(void) +{ + _pimpl->close(); +} + + +/// Returns the path to the connected database. +/// +/// It is OK to call this function on a live database object, even after close() +/// has been called. The returned value is consistent at all times. +/// +/// \return The path to the file that matches the connected database or none if +/// the connection points to a transient database. +const optional< fs::path >& +sqlite::database::db_filename(void) const +{ + return _pimpl->db_filename; +} + + +/// Executes an arbitrary SQL string. +/// +/// As the documentation explains, this is unsafe. The code should really be +/// preparing statements and executing them step by step. However, it is +/// perfectly fine to use this function for, e.g. the initial creation of +/// tables in a database and in tests. +/// +/// \param sql The SQL commands to be executed. +/// +/// \throw api_error If there is any problem while processing the SQL. +void +sqlite::database::exec(const std::string& sql) +{ + const int error = ::sqlite3_exec(_pimpl->db, sql.c_str(), NULL, NULL, NULL); + if (error != SQLITE_OK) + throw api_error::from_database(*this, "sqlite3_exec"); +} + + +/// Opens a new transaction. +/// +/// \return An object representing the state of the transaction. +/// +/// \throw api_error If there is any problem while opening the transaction. +sqlite::transaction +sqlite::database::begin_transaction(void) +{ + exec("BEGIN TRANSACTION"); + return transaction(*this); +} + + +/// Prepares a new statement. +/// +/// \param sql The SQL statement to prepare. +/// +/// \return The prepared statement. +sqlite::statement +sqlite::database::create_statement(const std::string& sql) +{ + LD(F("Creating statement: %s") % sql); + sqlite3_stmt* stmt; + const int error = ::sqlite3_prepare_v2(_pimpl->db, sql.c_str(), + sql.length() + 1, &stmt, NULL); + if (error != SQLITE_OK) + throw api_error::from_database(*this, "sqlite3_prepare_v2"); + return statement(*this, static_cast< void* >(stmt)); +} + + +/// Returns the row identifier of the last insert. +/// +/// \return A row identifier. +int64_t +sqlite::database::last_insert_rowid(void) +{ + return ::sqlite3_last_insert_rowid(_pimpl->db); +} diff --git a/utils/sqlite/database.hpp b/utils/sqlite/database.hpp new file mode 100644 index 000000000000..ca91a6a360c6 --- /dev/null +++ b/utils/sqlite/database.hpp @@ -0,0 +1,111 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sqlite/database.hpp +/// Wrapper classes and utilities for the SQLite database state. +/// +/// This module contains thin RAII wrappers around the SQLite 3 structures +/// representing the database, and lightweight. + +#if !defined(UTILS_SQLITE_DATABASE_HPP) +#define UTILS_SQLITE_DATABASE_HPP + +#include "utils/sqlite/database_fwd.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" +#include "utils/sqlite/c_gate_fwd.hpp" +#include "utils/sqlite/statement_fwd.hpp" +#include "utils/sqlite/transaction_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// Constant for the database::open flags: open in read-only mode. +static const int open_readonly = 1 << 0; +/// Constant for the database::open flags: open in read-write mode. +static const int open_readwrite = 1 << 1; +/// Constant for the database::open flags: create on open. +static const int open_create = 1 << 2; + + +/// A RAII model for the SQLite 3 database. +/// +/// This class holds the database of the SQLite 3 interface during its existence +/// and provides wrappers around several SQLite 3 library functions that operate +/// on such database. +/// +/// These wrapper functions differ from the C versions in that they use the +/// implicit database hold by the class, they use C++ types where appropriate +/// and they use exceptions to report errors. +/// +/// The wrappers intend to be as lightweight as possible but, in some +/// situations, they are pretty complex because of the workarounds needed to +/// make the SQLite 3 more C++ friendly. We prefer a clean C++ interface over +/// optimal efficiency, so this is OK. +class database { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + friend class database_c_gate; + database(const utils::optional< utils::fs::path >&, void*, const bool); + void* raw_database(void); + +public: + ~database(void); + + static database in_memory(void); + static database open(const fs::path&, int); + static database temporary(void); + void close(void); + + const utils::optional< utils::fs::path >& db_filename(void) const; + + void exec(const std::string&); + + transaction begin_transaction(void); + statement create_statement(const std::string&); + + int64_t last_insert_rowid(void); +}; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_DATABASE_HPP) diff --git a/utils/sqlite/database_fwd.hpp b/utils/sqlite/database_fwd.hpp new file mode 100644 index 000000000000..209342f159d6 --- /dev/null +++ b/utils/sqlite/database_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sqlite/database_fwd.hpp +/// Forward declarations for utils/sqlite/database.hpp + +#if !defined(UTILS_SQLITE_DATABASE_FWD_HPP) +#define UTILS_SQLITE_DATABASE_FWD_HPP + +namespace utils { +namespace sqlite { + + +class database; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_DATABASE_FWD_HPP) diff --git a/utils/sqlite/database_test.cpp b/utils/sqlite/database_test.cpp new file mode 100644 index 000000000000..70f057b9b793 --- /dev/null +++ b/utils/sqlite/database_test.cpp @@ -0,0 +1,287 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/database.hpp" + +#include + +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/statement.ipp" +#include "utils/sqlite/test_utils.hpp" +#include "utils/sqlite/transaction.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::optional; + + +ATF_TEST_CASE_WITHOUT_HEAD(in_memory); +ATF_TEST_CASE_BODY(in_memory) +{ + sqlite::database db = sqlite::database::in_memory(); + create_test_table(raw(db)); + verify_test_table(raw(db)); + + ATF_REQUIRE(!fs::exists(fs::path(":memory:"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open__readonly__ok); +ATF_TEST_CASE_BODY(open__readonly__ok) +{ + { + ::sqlite3* db; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_open_v2("test.db", &db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL)); + create_test_table(db); + ::sqlite3_close(db); + } + { + sqlite::database db = sqlite::database::open(fs::path("test.db"), + sqlite::open_readonly); + verify_test_table(raw(db)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open__readonly__fail); +ATF_TEST_CASE_BODY(open__readonly__fail) +{ + REQUIRE_API_ERROR("sqlite3_open_v2", + sqlite::database::open(fs::path("missing.db"), sqlite::open_readonly)); + ATF_REQUIRE(!fs::exists(fs::path("missing.db"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open__create__ok); +ATF_TEST_CASE_BODY(open__create__ok) +{ + { + sqlite::database db = sqlite::database::open(fs::path("test.db"), + sqlite::open_readwrite | sqlite::open_create); + ATF_REQUIRE(fs::exists(fs::path("test.db"))); + create_test_table(raw(db)); + } + { + ::sqlite3* db; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_open_v2("test.db", &db, + SQLITE_OPEN_READONLY, NULL)); + verify_test_table(db); + ::sqlite3_close(db); + } +} + + +ATF_TEST_CASE(open__create__fail); +ATF_TEST_CASE_HEAD(open__create__fail) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(open__create__fail) +{ + fs::mkdir(fs::path("protected"), 0555); + REQUIRE_API_ERROR("sqlite3_open_v2", + sqlite::database::open(fs::path("protected/test.db"), + sqlite::open_readwrite | sqlite::open_create)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(temporary); +ATF_TEST_CASE_BODY(temporary) +{ + // We could validate if files go to disk by setting the temp_store_directory + // PRAGMA to a subdirectory of pwd, and then ensuring the subdirectory is + // not empty. However, there does not seem to be a way to force SQLite to + // unconditionally write the temporary database to disk (even with + // temp_store = FILE), so this scenary is hard to reproduce. + sqlite::database db = sqlite::database::temporary(); + create_test_table(raw(db)); + verify_test_table(raw(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(close); +ATF_TEST_CASE_BODY(close) +{ + sqlite::database db = sqlite::database::in_memory(); + db.close(); + // The destructor for the database will run now. If it does a second close, + // we may crash, so let's see if we don't. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(copy); +ATF_TEST_CASE_BODY(copy) +{ + sqlite::database db1 = sqlite::database::in_memory(); + { + sqlite::database db2 = sqlite::database::in_memory(); + create_test_table(raw(db2)); + db1 = db2; + verify_test_table(raw(db1)); + } + // db2 went out of scope. If the destruction is not properly managed, the + // memory of db1 may have been invalidated and this would not work. + verify_test_table(raw(db1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(db_filename__in_memory); +ATF_TEST_CASE_BODY(db_filename__in_memory) +{ + const sqlite::database db = sqlite::database::in_memory(); + ATF_REQUIRE(!db.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(db_filename__file); +ATF_TEST_CASE_BODY(db_filename__file) +{ + const sqlite::database db = sqlite::database::open(fs::path("test.db"), + sqlite::open_readwrite | sqlite::open_create); + ATF_REQUIRE(db.db_filename()); + ATF_REQUIRE_EQ(fs::path("test.db"), db.db_filename().get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(db_filename__temporary); +ATF_TEST_CASE_BODY(db_filename__temporary) +{ + const sqlite::database db = sqlite::database::temporary(); + ATF_REQUIRE(!db.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(db_filename__ok_after_close); +ATF_TEST_CASE_BODY(db_filename__ok_after_close) +{ + sqlite::database db = sqlite::database::open(fs::path("test.db"), + sqlite::open_readwrite | sqlite::open_create); + const optional< fs::path > db_filename = db.db_filename(); + ATF_REQUIRE(db_filename); + db.close(); + ATF_REQUIRE_EQ(db_filename, db.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__ok); +ATF_TEST_CASE_BODY(exec__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec(create_test_table_sql); + verify_test_table(raw(db)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(exec__fail); +ATF_TEST_CASE_BODY(exec__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + REQUIRE_API_ERROR("sqlite3_exec", + db.exec("SELECT * FROM test")); + REQUIRE_API_ERROR("sqlite3_exec", + db.exec("CREATE TABLE test (col INTEGER PRIMARY KEY);" + "FOO BAR")); + db.exec("SELECT * FROM test"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(create_statement__ok); +ATF_TEST_CASE_BODY(create_statement__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3"); + // Statement testing happens in statement_test. We are only interested here + // in ensuring that the API call exists and runs. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(begin_transaction); +ATF_TEST_CASE_BODY(begin_transaction) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::transaction stmt = db.begin_transaction(); + // Transaction testing happens in transaction_test. We are only interested + // here in ensuring that the API call exists and runs. +} + + +ATF_TEST_CASE_WITHOUT_HEAD(create_statement__fail); +ATF_TEST_CASE_BODY(create_statement__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + REQUIRE_API_ERROR("sqlite3_prepare_v2", + db.create_statement("SELECT * FROM missing")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(last_insert_rowid); +ATF_TEST_CASE_BODY(last_insert_rowid) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE test (a INTEGER PRIMARY KEY, b INTEGER)"); + db.exec("INSERT INTO test VALUES (723, 5)"); + ATF_REQUIRE_EQ(723, db.last_insert_rowid()); + db.exec("INSERT INTO test VALUES (145, 20)"); + ATF_REQUIRE_EQ(145, db.last_insert_rowid()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, in_memory); + + ATF_ADD_TEST_CASE(tcs, open__readonly__ok); + ATF_ADD_TEST_CASE(tcs, open__readonly__fail); + ATF_ADD_TEST_CASE(tcs, open__create__ok); + ATF_ADD_TEST_CASE(tcs, open__create__fail); + + ATF_ADD_TEST_CASE(tcs, temporary); + + ATF_ADD_TEST_CASE(tcs, close); + + ATF_ADD_TEST_CASE(tcs, copy); + + ATF_ADD_TEST_CASE(tcs, db_filename__in_memory); + ATF_ADD_TEST_CASE(tcs, db_filename__file); + ATF_ADD_TEST_CASE(tcs, db_filename__temporary); + ATF_ADD_TEST_CASE(tcs, db_filename__ok_after_close); + + ATF_ADD_TEST_CASE(tcs, exec__ok); + ATF_ADD_TEST_CASE(tcs, exec__fail); + + ATF_ADD_TEST_CASE(tcs, begin_transaction); + + ATF_ADD_TEST_CASE(tcs, create_statement__ok); + ATF_ADD_TEST_CASE(tcs, create_statement__fail); + + ATF_ADD_TEST_CASE(tcs, last_insert_rowid); +} diff --git a/utils/sqlite/exceptions.cpp b/utils/sqlite/exceptions.cpp new file mode 100644 index 000000000000..cc2d42cab16c --- /dev/null +++ b/utils/sqlite/exceptions.cpp @@ -0,0 +1,175 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/exceptions.hpp" + +extern "C" { +#include +} + +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/c_gate.hpp" +#include "utils/sqlite/database.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::optional; + + +namespace { + + +/// Formats the database filename returned by sqlite for user consumption. +/// +/// \param db_filename An optional database filename. +/// +/// \return A string describing the filename. +static std::string +format_db_filename(const optional< fs::path >& db_filename) +{ + if (db_filename) + return db_filename.get().str(); + else + return "in-memory or temporary"; +} + + +} // anonymous namespace + + +/// Constructs a new error with a plain-text message. +/// +/// \param db_filename_ Database filename as returned by database::db_filename() +/// for error reporting purposes. +/// \param message The plain-text error message. +sqlite::error::error(const optional< fs::path >& db_filename_, + const std::string& message) : + std::runtime_error(F("%s (sqlite db: %s)") % message % + format_db_filename(db_filename_)), + _db_filename(db_filename_) +{ +} + + +/// Destructor for the error. +sqlite::error::~error(void) throw() +{ +} + + +/// Returns the path to the database that raised this error. +/// +/// \return A database filename as returned by database::db_filename(). +const optional< fs::path >& +sqlite::error::db_filename(void) const +{ + return _db_filename; +} + + +/// Constructs a new error. +/// +/// \param db_filename_ Database filename as returned by database::db_filename() +/// for error reporting purposes. +/// \param api_function_ The name of the API function that caused the error. +/// \param message_ The plain-text error message provided by SQLite. +sqlite::api_error::api_error(const optional< fs::path >& db_filename_, + const std::string& api_function_, + const std::string& message_) : + error(db_filename_, F("%s (sqlite op: %s)") % message_ % api_function_), + _api_function(api_function_) +{ +} + + +/// Destructor for the error. +sqlite::api_error::~api_error(void) throw() +{ +} + + +/// Constructs a new api_error with the message in the SQLite database. +/// +/// \param database_ The SQLite database. +/// \param api_function_ The name of the SQLite C API function that caused the +/// error. +/// +/// \return A new api_error with the retrieved message. +sqlite::api_error +sqlite::api_error::from_database(database& database_, + const std::string& api_function_) +{ + ::sqlite3* c_db = database_c_gate(database_).c_database(); + return api_error(database_.db_filename(), api_function_, + ::sqlite3_errmsg(c_db)); +} + + +/// Gets the name of the SQlite C API function that caused this error. +/// +/// \return The name of the function. +const std::string& +sqlite::api_error::api_function(void) const +{ + return _api_function; +} + + +/// Constructs a new error. +/// +/// \param db_filename_ Database filename as returned by database::db_filename() +/// for error reporting purposes. +/// \param name_ The name of the unknown column. +sqlite::invalid_column_error::invalid_column_error( + const optional< fs::path >& db_filename_, + const std::string& name_) : + error(db_filename_, F("Unknown column '%s'") % name_), + _column_name(name_) +{ +} + + +/// Destructor for the error. +sqlite::invalid_column_error::~invalid_column_error(void) throw() +{ +} + + +/// Gets the name of the column that could not be found. +/// +/// \return The name of the column requested by the user. +const std::string& +sqlite::invalid_column_error::column_name(void) const +{ + return _column_name; +} diff --git a/utils/sqlite/exceptions.hpp b/utils/sqlite/exceptions.hpp new file mode 100644 index 000000000000..a9450fce5c33 --- /dev/null +++ b/utils/sqlite/exceptions.hpp @@ -0,0 +1,94 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sqlite/exceptions.hpp +/// Exception types raised by the sqlite module. + +#if !defined(UTILS_SQLITE_EXCEPTIONS_HPP) +#define UTILS_SQLITE_EXCEPTIONS_HPP + +#include +#include + +#include "utils/fs/path_fwd.hpp" +#include "utils/optional.hpp" +#include "utils/sqlite/database_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// Base exception for sqlite errors. +class error : public std::runtime_error { + /// Path to the database that raised this error. + utils::optional< utils::fs::path > _db_filename; + +public: + explicit error(const utils::optional< utils::fs::path >&, + const std::string&); + virtual ~error(void) throw(); + + const utils::optional< utils::fs::path >& db_filename(void) const; +}; + + +/// Exception for errors raised by the SQLite 3 API library. +class api_error : public error { + /// The name of the SQLite 3 C API function that caused this error. + std::string _api_function; + +public: + explicit api_error(const utils::optional< utils::fs::path >&, + const std::string&, const std::string&); + virtual ~api_error(void) throw(); + + static api_error from_database(database&, const std::string&); + + const std::string& api_function(void) const; +}; + + +/// The caller requested a non-existent column name. +class invalid_column_error : public error { + /// The name of the invalid column. + std::string _column_name; + +public: + explicit invalid_column_error(const utils::optional< utils::fs::path >&, + const std::string&); + virtual ~invalid_column_error(void) throw(); + + const std::string& column_name(void) const; +}; + + +} // namespace sqlite +} // namespace utils + + +#endif // !defined(UTILS_SQLITE_EXCEPTIONS_HPP) diff --git a/utils/sqlite/exceptions_test.cpp b/utils/sqlite/exceptions_test.cpp new file mode 100644 index 000000000000..d9e81038cc2f --- /dev/null +++ b/utils/sqlite/exceptions_test.cpp @@ -0,0 +1,129 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/exceptions.hpp" + +extern "C" { +#include +} + +#include +#include + +#include + +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sqlite/c_gate.hpp" +#include "utils/sqlite/database.hpp" + +namespace fs = utils::fs; +namespace sqlite = utils::sqlite; + +using utils::none; + + +ATF_TEST_CASE_WITHOUT_HEAD(error__no_filename); +ATF_TEST_CASE_BODY(error__no_filename) +{ + const sqlite::database db = sqlite::database::in_memory(); + const sqlite::error e(db.db_filename(), "Some text"); + ATF_REQUIRE_EQ("Some text (sqlite db: in-memory or temporary)", + std::string(e.what())); + ATF_REQUIRE_EQ(db.db_filename(), e.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(error__with_filename); +ATF_TEST_CASE_BODY(error__with_filename) +{ + const sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + const sqlite::error e(db.db_filename(), "Some text"); + ATF_REQUIRE_EQ("Some text (sqlite db: test.db)", std::string(e.what())); + ATF_REQUIRE_EQ(db.db_filename(), e.db_filename()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(api_error__explicit); +ATF_TEST_CASE_BODY(api_error__explicit) +{ + const sqlite::api_error e(none, "some_function", "Some text"); + ATF_REQUIRE_EQ( + "Some text (sqlite op: some_function) " + "(sqlite db: in-memory or temporary)", + std::string(e.what())); + ATF_REQUIRE_EQ("some_function", e.api_function()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(api_error__from_database); +ATF_TEST_CASE_BODY(api_error__from_database) +{ + sqlite::database db = sqlite::database::open( + fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create); + + // Use the raw sqlite3 API to cause an error. Our C++ wrappers catch all + // errors and reraise them as exceptions, but here we want to handle the raw + // error directly for testing purposes. + sqlite::database_c_gate gate(db); + ::sqlite3_stmt* dummy_stmt; + const char* query = "ABCDE INVALID QUERY"; + (void)::sqlite3_prepare_v2(gate.c_database(), query, std::strlen(query), + &dummy_stmt, NULL); + + const sqlite::api_error e = sqlite::api_error::from_database( + db, "real_function"); + ATF_REQUIRE_MATCH( + ".*ABCDE.*\\(sqlite op: real_function\\) \\(sqlite db: test.db\\)", + std::string(e.what())); + ATF_REQUIRE_EQ("real_function", e.api_function()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_column_error); +ATF_TEST_CASE_BODY(invalid_column_error) +{ + const sqlite::invalid_column_error e(none, "some_name"); + ATF_REQUIRE_EQ("Unknown column 'some_name' " + "(sqlite db: in-memory or temporary)", + std::string(e.what())); + ATF_REQUIRE_EQ("some_name", e.column_name()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error__no_filename); + ATF_ADD_TEST_CASE(tcs, error__with_filename); + + ATF_ADD_TEST_CASE(tcs, api_error__explicit); + ATF_ADD_TEST_CASE(tcs, api_error__from_database); + + ATF_ADD_TEST_CASE(tcs, invalid_column_error); +} diff --git a/utils/sqlite/statement.cpp b/utils/sqlite/statement.cpp new file mode 100644 index 000000000000..0ae2af2d57ca --- /dev/null +++ b/utils/sqlite/statement.cpp @@ -0,0 +1,621 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/statement.hpp" + +extern "C" { +#include +} + +#include + +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/c_gate.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" + +namespace sqlite = utils::sqlite; + + +namespace { + + +static sqlite::type c_type_to_cxx(const int) UTILS_PURE; + + +/// Maps a SQLite 3 data type to our own representation. +/// +/// \param original The native SQLite 3 data type. +/// +/// \return Our internal representation for the native data type. +static sqlite::type +c_type_to_cxx(const int original) +{ + switch (original) { + case SQLITE_BLOB: return sqlite::type_blob; + case SQLITE_FLOAT: return sqlite::type_float; + case SQLITE_INTEGER: return sqlite::type_integer; + case SQLITE_NULL: return sqlite::type_null; + case SQLITE_TEXT: return sqlite::type_text; + default: UNREACHABLE_MSG("Unknown data type returned by SQLite 3"); + } + UNREACHABLE; +} + + +/// Handles the return value of a sqlite3_bind_* call. +/// +/// \param db The database the call was made on. +/// \param api_function The name of the sqlite3_bind_* function called. +/// \param error The error code returned by the function; can be SQLITE_OK. +/// +/// \throw std::bad_alloc If there was no memory for the binding. +/// \throw api_error If the binding fails for any other reason. +static void +handle_bind_error(sqlite::database& db, const char* api_function, + const int error) +{ + switch (error) { + case SQLITE_OK: + return; + case SQLITE_RANGE: + UNREACHABLE_MSG("Invalid index for bind argument"); + case SQLITE_NOMEM: + throw std::bad_alloc(); + default: + throw sqlite::api_error::from_database(db, api_function); + } +} + + +} // anonymous namespace + + +/// Internal implementation for sqlite::statement. +struct utils::sqlite::statement::impl : utils::noncopyable { + /// The database this statement belongs to. + sqlite::database& db; + + /// The SQLite 3 internal statement. + ::sqlite3_stmt* stmt; + + /// Cache for the column names in a statement; lazily initialized. + std::map< std::string, int > column_cache; + + /// Constructor. + /// + /// \param db_ The database this statement belongs to. Be aware that we + /// keep a *reference* to the database; in other words, if the database + /// vanishes, this object will become invalid. (It'd be trivial to keep + /// a shallow copy here instead, but I feel that statements that outlive + /// their database represents sloppy programming.) + /// \param stmt_ The SQLite internal statement. + impl(database& db_, ::sqlite3_stmt* stmt_) : + db(db_), + stmt(stmt_) + { + } + + /// Destructor. + /// + /// It is important to keep this as part of the 'impl' class instead of the + /// container class. The 'impl' class is destroyed exactly once (because it + /// is managed by a shared_ptr) and thus releasing the resources here is + /// OK. However, the container class is potentially released many times, + /// which means that we would be double-freeing the internal object and + /// reusing invalid data. + ~impl(void) + { + (void)::sqlite3_finalize(stmt); + } +}; + + +/// Initializes a statement object. +/// +/// This is an internal function. Use database::create_statement() to +/// instantiate one of these objects. +/// +/// \param db The database this statement belongs to. +/// \param raw_stmt A void pointer representing a SQLite native statement of +/// type sqlite3_stmt. +sqlite::statement::statement(database& db, void* raw_stmt) : + _pimpl(new impl(db, static_cast< ::sqlite3_stmt* >(raw_stmt))) +{ +} + + +/// Destructor for the statement. +/// +/// Remember that statements are reference-counted, so the statement will only +/// cease to be valid once its last copy is destroyed. +sqlite::statement::~statement(void) +{ +} + + +/// Executes a statement that is not supposed to return any data. +/// +/// Use this function to execute DDL and INSERT statements; i.e. statements that +/// only have one processing step and deliver no rows. This frees the caller +/// from having to deal with the return value of the step() function. +/// +/// \pre The statement to execute will not produce any rows. +void +sqlite::statement::step_without_results(void) +{ + const bool data = step(); + INV_MSG(!data, "The statement should not have produced any rows, but it " + "did"); +} + + +/// Performs a processing step on the statement. +/// +/// \return True if the statement returned a row; false if the processing has +/// finished. +/// +/// \throw api_error If the processing of the step raises an error. +bool +sqlite::statement::step(void) +{ + const int error = ::sqlite3_step(_pimpl->stmt); + switch (error) { + case SQLITE_DONE: + LD("Step statement; no more rows"); + return false; + case SQLITE_ROW: + LD("Step statement; row available for processing"); + return true; + default: + throw api_error::from_database(_pimpl->db, "sqlite3_step"); + } + UNREACHABLE; +} + + +/// Returns the number of columns in the step result. +/// +/// \return The number of columns available for data retrieval. +int +sqlite::statement::column_count(void) +{ + return ::sqlite3_column_count(_pimpl->stmt); +} + + +/// Returns the name of a particular column in the result. +/// +/// \param index The column to request the name of. +/// +/// \return The name of the requested column. +std::string +sqlite::statement::column_name(const int index) +{ + const char* name = ::sqlite3_column_name(_pimpl->stmt, index); + if (name == NULL) + throw api_error::from_database(_pimpl->db, "sqlite3_column_name"); + return name; +} + + +/// Returns the type of a particular column in the result. +/// +/// \param index The column to request the type of. +/// +/// \return The type of the requested column. +sqlite::type +sqlite::statement::column_type(const int index) +{ + return c_type_to_cxx(::sqlite3_column_type(_pimpl->stmt, index)); +} + + +/// Finds a column by name. +/// +/// \param name The name of the column to search for. +/// +/// \return The column identifier. +/// +/// \throw value_error If the name cannot be found. +int +sqlite::statement::column_id(const char* name) +{ + std::map< std::string, int >& cache = _pimpl->column_cache; + + if (cache.empty()) { + for (int i = 0; i < column_count(); i++) { + const std::string aux_name = column_name(i); + INV(cache.find(aux_name) == cache.end()); + cache[aux_name] = i; + } + } + + const std::map< std::string, int >::const_iterator iter = cache.find(name); + if (iter == cache.end()) + throw invalid_column_error(_pimpl->db.db_filename(), name); + else + return (*iter).second; +} + + +/// Returns a particular column in the result as a blob. +/// +/// \param index The column to retrieve. +/// +/// \return A block of memory with the blob contents. Note that the pointer +/// returned by this call will be invalidated on the next call to any SQLite API +/// function. +sqlite::blob +sqlite::statement::column_blob(const int index) +{ + PRE(column_type(index) == type_blob); + return blob(::sqlite3_column_blob(_pimpl->stmt, index), + ::sqlite3_column_bytes(_pimpl->stmt, index)); +} + + +/// Returns a particular column in the result as a double. +/// +/// \param index The column to retrieve. +/// +/// \return The double value. +double +sqlite::statement::column_double(const int index) +{ + PRE(column_type(index) == type_float); + return ::sqlite3_column_double(_pimpl->stmt, index); +} + + +/// Returns a particular column in the result as an integer. +/// +/// \param index The column to retrieve. +/// +/// \return The integer value. Note that the value may not fit in an integer +/// depending on the platform. Use column_int64 to retrieve the integer without +/// truncation. +int +sqlite::statement::column_int(const int index) +{ + PRE(column_type(index) == type_integer); + return ::sqlite3_column_int(_pimpl->stmt, index); +} + + +/// Returns a particular column in the result as a 64-bit integer. +/// +/// \param index The column to retrieve. +/// +/// \return The integer value. +int64_t +sqlite::statement::column_int64(const int index) +{ + PRE(column_type(index) == type_integer); + return ::sqlite3_column_int64(_pimpl->stmt, index); +} + + +/// Returns a particular column in the result as a double. +/// +/// \param index The column to retrieve. +/// +/// \return A C string with the contents. Note that the pointer returned by +/// this call will be invalidated on the next call to any SQLite API function. +/// If you want to be extra safe, store the result in a std::string to not worry +/// about this. +std::string +sqlite::statement::column_text(const int index) +{ + PRE(column_type(index) == type_text); + return reinterpret_cast< const char* >(::sqlite3_column_text( + _pimpl->stmt, index)); +} + + +/// Returns the number of bytes stored in the column. +/// +/// \pre This is only valid for columns of type blob and text. +/// +/// \param index The column to retrieve the size of. +/// +/// \return The number of bytes in the column. Remember that strings are stored +/// in their UTF-8 representation; this call returns the number of *bytes*, not +/// characters. +int +sqlite::statement::column_bytes(const int index) +{ + PRE(column_type(index) == type_blob || column_type(index) == type_text); + return ::sqlite3_column_bytes(_pimpl->stmt, index); +} + + +/// Type-checked version of column_blob. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_blob if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +sqlite::blob +sqlite::statement::safe_column_blob(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_blob) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not a blob") % name); + return column_blob(column); +} + + +/// Type-checked version of column_double. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_double if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +double +sqlite::statement::safe_column_double(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_float) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not a float") % name); + return column_double(column); +} + + +/// Type-checked version of column_int. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_int if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +int +sqlite::statement::safe_column_int(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_integer) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not an integer") % name); + return column_int(column); +} + + +/// Type-checked version of column_int64. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_int64 if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +int64_t +sqlite::statement::safe_column_int64(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_integer) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not an integer") % name); + return column_int64(column); +} + + +/// Type-checked version of column_text. +/// +/// \param name The name of the column to retrieve. +/// +/// \return The same as column_text if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve is invalid. +/// \throw invalid_column_error If name is invalid. +std::string +sqlite::statement::safe_column_text(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_text) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not a string") % name); + return column_text(column); +} + + +/// Type-checked version of column_bytes. +/// +/// \param name The name of the column to retrieve the size of. +/// +/// \return The same as column_bytes if the value can be retrieved. +/// +/// \throw error If the type of the cell to retrieve the size of is invalid. +/// \throw invalid_column_error If name is invalid. +int +sqlite::statement::safe_column_bytes(const char* name) +{ + const int column = column_id(name); + if (column_type(column) != sqlite::type_blob && + column_type(column) != sqlite::type_text) + throw sqlite::error(_pimpl->db.db_filename(), + F("Column '%s' is not a blob or a string") % name); + return column_bytes(column); +} + + +/// Resets a statement to allow further processing. +void +sqlite::statement::reset(void) +{ + (void)::sqlite3_reset(_pimpl->stmt); +} + + +/// Binds a blob to a prepared statement. +/// +/// \param index The index of the binding. +/// \param b Description of the blob, which must remain valid during the +/// execution of the statement. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const blob& b) +{ + const int error = ::sqlite3_bind_blob(_pimpl->stmt, index, b.memory, b.size, + SQLITE_STATIC); + handle_bind_error(_pimpl->db, "sqlite3_bind_blob", error); +} + + +/// Binds a double value to a prepared statement. +/// +/// \param index The index of the binding. +/// \param value The double value to bind. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const double value) +{ + const int error = ::sqlite3_bind_double(_pimpl->stmt, index, value); + handle_bind_error(_pimpl->db, "sqlite3_bind_double", error); +} + + +/// Binds an integer value to a prepared statement. +/// +/// \param index The index of the binding. +/// \param value The integer value to bind. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const int value) +{ + const int error = ::sqlite3_bind_int(_pimpl->stmt, index, value); + handle_bind_error(_pimpl->db, "sqlite3_bind_int", error); +} + + +/// Binds a 64-bit integer value to a prepared statement. +/// +/// \param index The index of the binding. +/// \param value The 64-bin integer value to bind. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const int64_t value) +{ + const int error = ::sqlite3_bind_int64(_pimpl->stmt, index, value); + handle_bind_error(_pimpl->db, "sqlite3_bind_int64", error); +} + + +/// Binds a NULL value to a prepared statement. +/// +/// \param index The index of the binding. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const null& /* null */) +{ + const int error = ::sqlite3_bind_null(_pimpl->stmt, index); + handle_bind_error(_pimpl->db, "sqlite3_bind_null", error); +} + + +/// Binds a text string to a prepared statement. +/// +/// \param index The index of the binding. +/// \param text The string to bind. SQLite generates an internal copy of this +/// string, so the original string object does not have to remain live. We +/// do this because handling the lifetime of std::string objects is very +/// hard (think about implicit conversions), so it is very easy to shoot +/// ourselves in the foot if we don't do this. +/// +/// \throw api_error If the binding fails. +void +sqlite::statement::bind(const int index, const std::string& text) +{ + const int error = ::sqlite3_bind_text(_pimpl->stmt, index, text.c_str(), + text.length(), SQLITE_TRANSIENT); + handle_bind_error(_pimpl->db, "sqlite3_bind_text", error); +} + + +/// Returns the index of the highest parameter. +/// +/// \return A parameter index. +int +sqlite::statement::bind_parameter_count(void) +{ + return ::sqlite3_bind_parameter_count(_pimpl->stmt); +} + + +/// Returns the index of a named parameter. +/// +/// \param name The name of the parameter to be queried; must exist. +/// +/// \return A parameter index. +int +sqlite::statement::bind_parameter_index(const std::string& name) +{ + const int index = ::sqlite3_bind_parameter_index(_pimpl->stmt, + name.c_str()); + PRE_MSG(index > 0, "Parameter name not in statement"); + return index; +} + + +/// Returns the name of a parameter by index. +/// +/// \param index The index to query; must be valid. +/// +/// \return The name of the parameter. +std::string +sqlite::statement::bind_parameter_name(const int index) +{ + const char* name = ::sqlite3_bind_parameter_name(_pimpl->stmt, index); + PRE_MSG(name != NULL, "Index value out of range or nameless parameter"); + return std::string(name); +} + + +/// Clears any bindings and releases their memory. +void +sqlite::statement::clear_bindings(void) +{ + const int error = ::sqlite3_clear_bindings(_pimpl->stmt); + PRE_MSG(error == SQLITE_OK, "SQLite3 contract has changed; it should " + "only return SQLITE_OK"); +} diff --git a/utils/sqlite/statement.hpp b/utils/sqlite/statement.hpp new file mode 100644 index 000000000000..bcd1831e4841 --- /dev/null +++ b/utils/sqlite/statement.hpp @@ -0,0 +1,137 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sqlite/statement.hpp +/// Wrapper classes and utilities for SQLite statement processing. +/// +/// This module contains thin RAII wrappers around the SQLite 3 structures +/// representing statements. + +#if !defined(UTILS_SQLITE_STATEMENT_HPP) +#define UTILS_SQLITE_STATEMENT_HPP + +#include "utils/sqlite/statement_fwd.hpp" + +extern "C" { +#include +} + +#include +#include + +#include "utils/sqlite/database_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// Representation of a BLOB. +class blob { +public: + /// Memory representing the contents of the blob, or NULL if empty. + /// + /// This memory must remain valid throughout the life of this object, as we + /// do not grab ownership of the memory. + const void* memory; + + /// Number of bytes in memory. + int size; + + /// Constructs a new blob. + /// + /// \param memory_ Pointer to the contents of the blob. + /// \param size_ The size of memory_. + blob(const void* memory_, const int size_) : + memory(memory_), size(size_) + { + } +}; + + +/// Representation of a SQL NULL value. +class null { +}; + + +/// A RAII model for an SQLite 3 statement. +class statement { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + statement(database&, void*); + friend class database; + +public: + ~statement(void); + + bool step(void); + void step_without_results(void); + + int column_count(void); + std::string column_name(const int); + type column_type(const int); + int column_id(const char*); + + blob column_blob(const int); + double column_double(const int); + int column_int(const int); + int64_t column_int64(const int); + std::string column_text(const int); + int column_bytes(const int); + + blob safe_column_blob(const char*); + double safe_column_double(const char*); + int safe_column_int(const char*); + int64_t safe_column_int64(const char*); + std::string safe_column_text(const char*); + int safe_column_bytes(const char*); + + void reset(void); + + void bind(const int, const blob&); + void bind(const int, const double); + void bind(const int, const int); + void bind(const int, const int64_t); + void bind(const int, const null&); + void bind(const int, const std::string&); + template< class T > void bind(const char*, const T&); + + int bind_parameter_count(void); + int bind_parameter_index(const std::string&); + std::string bind_parameter_name(const int); + + void clear_bindings(void); +}; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_STATEMENT_HPP) diff --git a/utils/sqlite/statement.ipp b/utils/sqlite/statement.ipp new file mode 100644 index 000000000000..3f219016a2a9 --- /dev/null +++ b/utils/sqlite/statement.ipp @@ -0,0 +1,52 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_SQLITE_STATEMENT_IPP) +#define UTILS_SQLITE_STATEMENT_IPP + +#include "utils/sqlite/statement.hpp" + + +/// Binds a value to a parameter of a prepared statement. +/// +/// \param parameter The name of the parameter; must exist. This is a raw C +/// string instead of a std::string because statement parameter names are +/// known at compilation time and the program should really not be +/// constructing them dynamically. +/// \param value The value to bind to the parameter. +/// +/// \throw api_error If the binding fails. +template< class T > +void +utils::sqlite::statement::bind(const char* parameter, const T& value) +{ + bind(bind_parameter_index(parameter), value); +} + + +#endif // !defined(UTILS_SQLITE_STATEMENT_IPP) diff --git a/utils/sqlite/statement_fwd.hpp b/utils/sqlite/statement_fwd.hpp new file mode 100644 index 000000000000..26634c965018 --- /dev/null +++ b/utils/sqlite/statement_fwd.hpp @@ -0,0 +1,57 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sqlite/statement_fwd.hpp +/// Forward declarations for utils/sqlite/statement.hpp + +#if !defined(UTILS_SQLITE_STATEMENT_FWD_HPP) +#define UTILS_SQLITE_STATEMENT_FWD_HPP + +namespace utils { +namespace sqlite { + + +/// Representation of the SQLite data types. +enum type { + type_blob, + type_float, + type_integer, + type_null, + type_text, +}; + + +class blob; +class null; +class statement; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_STATEMENT_FWD_HPP) diff --git a/utils/sqlite/statement_test.cpp b/utils/sqlite/statement_test.cpp new file mode 100644 index 000000000000..40bc92cb5c0e --- /dev/null +++ b/utils/sqlite/statement_test.cpp @@ -0,0 +1,784 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/statement.ipp" + +extern "C" { +#include +} + +#include +#include + +#include + +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/test_utils.hpp" + +namespace sqlite = utils::sqlite; + + +ATF_TEST_CASE_WITHOUT_HEAD(step__ok); +ATF_TEST_CASE_BODY(step__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement( + "CREATE TABLE foo (a INTEGER PRIMARY KEY)"); + ATF_REQUIRE_THROW(sqlite::error, db.exec("SELECT * FROM foo")); + ATF_REQUIRE(!stmt.step()); + db.exec("SELECT * FROM foo"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(step__many); +ATF_TEST_CASE_BODY(step__many) +{ + sqlite::database db = sqlite::database::in_memory(); + create_test_table(raw(db)); + sqlite::statement stmt = db.create_statement( + "SELECT prime FROM test ORDER BY prime"); + for (int i = 0; i < 5; i++) + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(step__fail); +ATF_TEST_CASE_BODY(step__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement( + "CREATE TABLE foo (a INTEGER PRIMARY KEY)"); + ATF_REQUIRE(!stmt.step()); + REQUIRE_API_ERROR("sqlite3_step", stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(step_without_results__ok); +ATF_TEST_CASE_BODY(step_without_results__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement( + "CREATE TABLE foo (a INTEGER PRIMARY KEY)"); + ATF_REQUIRE_THROW(sqlite::error, db.exec("SELECT * FROM foo")); + stmt.step_without_results(); + db.exec("SELECT * FROM foo"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(step_without_results__fail); +ATF_TEST_CASE_BODY(step_without_results__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER PRIMARY KEY)"); + db.exec("INSERT INTO foo VALUES (3)"); + sqlite::statement stmt = db.create_statement( + "INSERT INTO foo VALUES (3)"); + REQUIRE_API_ERROR("sqlite3_step", stmt.step_without_results()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_count); +ATF_TEST_CASE_BODY(column_count) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER PRIMARY KEY, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (5, 3, 'foo');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(3, stmt.column_count()); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_name__ok); +ATF_TEST_CASE_BODY(column_name__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (first INTEGER PRIMARY KEY, second TEXT);" + "INSERT INTO foo VALUES (5, 'foo');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ("first", stmt.column_name(0)); + ATF_REQUIRE_EQ("second", stmt.column_name(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_name__fail); +ATF_TEST_CASE_BODY(column_name__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (first INTEGER PRIMARY KEY);" + "INSERT INTO foo VALUES (5);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ("first", stmt.column_name(0)); + REQUIRE_API_ERROR("sqlite3_column_name", stmt.column_name(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_type__ok); +ATF_TEST_CASE_BODY(column_type__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a_blob BLOB," + " a_float FLOAT," + " an_integer INTEGER," + " a_null BLOB," + " a_text TEXT);" + "INSERT INTO foo VALUES (x'0102', 0.3, 5, NULL, 'foo bar');" + "INSERT INTO foo VALUES (NULL, NULL, NULL, NULL, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_blob == stmt.column_type(0)); + ATF_REQUIRE(sqlite::type_float == stmt.column_type(1)); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(2)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(3)); + ATF_REQUIRE(sqlite::type_text == stmt.column_type(4)); + ATF_REQUIRE(stmt.step()); + for (int i = 0; i < stmt.column_count(); i++) + ATF_REQUIRE(sqlite::type_null == stmt.column_type(i)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_type__out_of_range); +ATF_TEST_CASE_BODY(column_type__out_of_range) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER PRIMARY KEY);" + "INSERT INTO foo VALUES (1);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(1)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(512)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_id__ok); +ATF_TEST_CASE_BODY(column_id__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (bar INTEGER PRIMARY KEY, " + " baz INTEGER);" + "INSERT INTO foo VALUES (1, 2);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(0, stmt.column_id("bar")); + ATF_REQUIRE_EQ(1, stmt.column_id("baz")); + ATF_REQUIRE_EQ(0, stmt.column_id("bar")); + ATF_REQUIRE_EQ(1, stmt.column_id("baz")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_id__missing); +ATF_TEST_CASE_BODY(column_id__missing) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (bar INTEGER PRIMARY KEY, " + " baz INTEGER);" + "INSERT INTO foo VALUES (1, 2);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(0, stmt.column_id("bar")); + try { + stmt.column_id("bazo"); + fail("invalid_column_error not raised"); + } catch (const sqlite::invalid_column_error& e) { + ATF_REQUIRE_EQ("bazo", e.column_name()); + } + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_blob); +ATF_TEST_CASE_BODY(column_blob) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b BLOB, c INTEGER);" + "INSERT INTO foo VALUES (NULL, x'cafe', NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + const sqlite::blob blob = stmt.column_blob(1); + ATF_REQUIRE_EQ(0xca, static_cast< const uint8_t* >(blob.memory)[0]); + ATF_REQUIRE_EQ(0xfe, static_cast< const uint8_t* >(blob.memory)[1]); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_double); +ATF_TEST_CASE_BODY(column_double) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b DOUBLE, c INTEGER);" + "INSERT INTO foo VALUES (NULL, 0.5, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(0.5, stmt.column_double(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_int__ok); +ATF_TEST_CASE_BODY(column_int__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 987, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(987, stmt.column_int(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_int__overflow); +ATF_TEST_CASE_BODY(column_int__overflow) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 4294967419, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(123, stmt.column_int(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_int64); +ATF_TEST_CASE_BODY(column_int64) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 4294967419, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(4294967419LL, stmt.column_int64(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_text); +ATF_TEST_CASE_BODY(column_text) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b TEXT, c INTEGER);" + "INSERT INTO foo VALUES (NULL, 'foo bar', NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ("foo bar", stmt.column_text(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_bytes__blob); +ATF_TEST_CASE_BODY(column_bytes__blob) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a BLOB);" + "INSERT INTO foo VALUES (x'12345678');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(4, stmt.column_bytes(0)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(column_bytes__text); +ATF_TEST_CASE_BODY(column_bytes__text) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('foo bar');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(7, stmt.column_bytes(0)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_blob__ok); +ATF_TEST_CASE_BODY(safe_column_blob__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b BLOB, c INTEGER);" + "INSERT INTO foo VALUES (NULL, x'cafe', NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + const sqlite::blob blob = stmt.safe_column_blob("b"); + ATF_REQUIRE_EQ(0xca, static_cast< const uint8_t* >(blob.memory)[0]); + ATF_REQUIRE_EQ(0xfe, static_cast< const uint8_t* >(blob.memory)[1]); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_blob__fail); +ATF_TEST_CASE_BODY(safe_column_blob__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER);" + "INSERT INTO foo VALUES (123);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_blob("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not a blob", + stmt.safe_column_blob("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_double__ok); +ATF_TEST_CASE_BODY(safe_column_double__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b DOUBLE, c INTEGER);" + "INSERT INTO foo VALUES (NULL, 0.5, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(0.5, stmt.safe_column_double("b")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_double__fail); +ATF_TEST_CASE_BODY(safe_column_double__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER);" + "INSERT INTO foo VALUES (NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_double("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not a float", + stmt.safe_column_double("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_int__ok); +ATF_TEST_CASE_BODY(safe_column_int__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 987, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(987, stmt.safe_column_int("b")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_int__fail); +ATF_TEST_CASE_BODY(safe_column_int__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('def');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_int("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not an integer", + stmt.safe_column_int("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_int64__ok); +ATF_TEST_CASE_BODY(safe_column_int64__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT, b INTEGER, c TEXT);" + "INSERT INTO foo VALUES (NULL, 4294967419, NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(4294967419LL, stmt.safe_column_int64("b")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_int64__fail); +ATF_TEST_CASE_BODY(safe_column_int64__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('abc');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_int64("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not an integer", + stmt.safe_column_int64("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_text__ok); +ATF_TEST_CASE_BODY(safe_column_text__ok) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER, b TEXT, c INTEGER);" + "INSERT INTO foo VALUES (NULL, 'foo bar', NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ("foo bar", stmt.safe_column_text("b")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_text__fail); +ATF_TEST_CASE_BODY(safe_column_text__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a INTEGER);" + "INSERT INTO foo VALUES (NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_text("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not a string", + stmt.safe_column_text("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_bytes__ok__blob); +ATF_TEST_CASE_BODY(safe_column_bytes__ok__blob) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a BLOB);" + "INSERT INTO foo VALUES (x'12345678');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(4, stmt.safe_column_bytes("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_bytes__ok__text); +ATF_TEST_CASE_BODY(safe_column_bytes__ok__text) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('foo bar');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_EQ(7, stmt.safe_column_bytes("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(safe_column_bytes__fail); +ATF_TEST_CASE_BODY(safe_column_bytes__fail) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES (NULL);"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE_THROW(sqlite::invalid_column_error, + stmt.safe_column_bytes("b")); + ATF_REQUIRE_THROW_RE(sqlite::error, "not a blob or a string", + stmt.safe_column_bytes("a")); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(reset); +ATF_TEST_CASE_BODY(reset) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE foo (a TEXT);" + "INSERT INTO foo VALUES ('foo bar');"); + sqlite::statement stmt = db.create_statement("SELECT * FROM foo"); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(!stmt.step()); + stmt.reset(); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__blob); +ATF_TEST_CASE_BODY(bind__blob) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + const unsigned char blob[] = {0xca, 0xfe}; + stmt.bind(1, sqlite::blob(static_cast< const void* >(blob), 2)); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_blob == stmt.column_type(1)); + const unsigned char* ret_blob = + static_cast< const unsigned char* >(stmt.column_blob(1).memory); + ATF_REQUIRE(std::memcmp(blob, ret_blob, 2) == 0); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__double); +ATF_TEST_CASE_BODY(bind__double) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, 0.5); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_float == stmt.column_type(1)); + ATF_REQUIRE_EQ(0.5, stmt.column_double(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__int); +ATF_TEST_CASE_BODY(bind__int) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, 123); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(1)); + ATF_REQUIRE_EQ(123, stmt.column_int(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__int64); +ATF_TEST_CASE_BODY(bind__int64) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, static_cast< int64_t >(4294967419LL)); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(1)); + ATF_REQUIRE_EQ(4294967419LL, stmt.column_int64(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__null); +ATF_TEST_CASE_BODY(bind__null) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, sqlite::null()); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__text); +ATF_TEST_CASE_BODY(bind__text) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + const std::string str = "Hello"; + stmt.bind(1, str); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_text == stmt.column_type(1)); + ATF_REQUIRE_EQ(str, stmt.column_text(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__text__transient); +ATF_TEST_CASE_BODY(bind__text__transient) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, :foo"); + + { + const std::string str = "Hello"; + stmt.bind(":foo", str); + } + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_text == stmt.column_type(1)); + ATF_REQUIRE_EQ(std::string("Hello"), stmt.column_text(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind__by_name); +ATF_TEST_CASE_BODY(bind__by_name) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, :foo"); + + const std::string str = "Hello"; + stmt.bind(":foo", str); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_text == stmt.column_type(1)); + ATF_REQUIRE_EQ(str, stmt.column_text(1)); + ATF_REQUIRE(!stmt.step()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind_parameter_count); +ATF_TEST_CASE_BODY(bind_parameter_count) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?, ?"); + ATF_REQUIRE_EQ(2, stmt.bind_parameter_count()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind_parameter_index); +ATF_TEST_CASE_BODY(bind_parameter_index) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, :foo, ?, :bar"); + ATF_REQUIRE_EQ(1, stmt.bind_parameter_index(":foo")); + ATF_REQUIRE_EQ(3, stmt.bind_parameter_index(":bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bind_parameter_name); +ATF_TEST_CASE_BODY(bind_parameter_name) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, :foo, ?, :bar"); + ATF_REQUIRE_EQ(":foo", stmt.bind_parameter_name(1)); + ATF_REQUIRE_EQ(":bar", stmt.bind_parameter_name(3)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(clear_bindings); +ATF_TEST_CASE_BODY(clear_bindings) +{ + sqlite::database db = sqlite::database::in_memory(); + sqlite::statement stmt = db.create_statement("SELECT 3, ?"); + + stmt.bind(1, 5); + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(1)); + ATF_REQUIRE_EQ(5, stmt.column_int(1)); + stmt.clear_bindings(); + stmt.reset(); + + ATF_REQUIRE(stmt.step()); + ATF_REQUIRE(sqlite::type_integer == stmt.column_type(0)); + ATF_REQUIRE_EQ(3, stmt.column_int(0)); + ATF_REQUIRE(sqlite::type_null == stmt.column_type(1)); + + ATF_REQUIRE(!stmt.step()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, step__ok); + ATF_ADD_TEST_CASE(tcs, step__many); + ATF_ADD_TEST_CASE(tcs, step__fail); + + ATF_ADD_TEST_CASE(tcs, step_without_results__ok); + ATF_ADD_TEST_CASE(tcs, step_without_results__fail); + + ATF_ADD_TEST_CASE(tcs, column_count); + + ATF_ADD_TEST_CASE(tcs, column_name__ok); + ATF_ADD_TEST_CASE(tcs, column_name__fail); + + ATF_ADD_TEST_CASE(tcs, column_type__ok); + ATF_ADD_TEST_CASE(tcs, column_type__out_of_range); + + ATF_ADD_TEST_CASE(tcs, column_id__ok); + ATF_ADD_TEST_CASE(tcs, column_id__missing); + + ATF_ADD_TEST_CASE(tcs, column_blob); + ATF_ADD_TEST_CASE(tcs, column_double); + ATF_ADD_TEST_CASE(tcs, column_int__ok); + ATF_ADD_TEST_CASE(tcs, column_int__overflow); + ATF_ADD_TEST_CASE(tcs, column_int64); + ATF_ADD_TEST_CASE(tcs, column_text); + + ATF_ADD_TEST_CASE(tcs, column_bytes__blob); + ATF_ADD_TEST_CASE(tcs, column_bytes__text); + + ATF_ADD_TEST_CASE(tcs, safe_column_blob__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_blob__fail); + ATF_ADD_TEST_CASE(tcs, safe_column_double__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_double__fail); + ATF_ADD_TEST_CASE(tcs, safe_column_int__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_int__fail); + ATF_ADD_TEST_CASE(tcs, safe_column_int64__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_int64__fail); + ATF_ADD_TEST_CASE(tcs, safe_column_text__ok); + ATF_ADD_TEST_CASE(tcs, safe_column_text__fail); + + ATF_ADD_TEST_CASE(tcs, safe_column_bytes__ok__blob); + ATF_ADD_TEST_CASE(tcs, safe_column_bytes__ok__text); + ATF_ADD_TEST_CASE(tcs, safe_column_bytes__fail); + + ATF_ADD_TEST_CASE(tcs, reset); + + ATF_ADD_TEST_CASE(tcs, bind__blob); + ATF_ADD_TEST_CASE(tcs, bind__double); + ATF_ADD_TEST_CASE(tcs, bind__int64); + ATF_ADD_TEST_CASE(tcs, bind__int); + ATF_ADD_TEST_CASE(tcs, bind__null); + ATF_ADD_TEST_CASE(tcs, bind__text); + ATF_ADD_TEST_CASE(tcs, bind__text__transient); + ATF_ADD_TEST_CASE(tcs, bind__by_name); + + ATF_ADD_TEST_CASE(tcs, bind_parameter_count); + ATF_ADD_TEST_CASE(tcs, bind_parameter_index); + ATF_ADD_TEST_CASE(tcs, bind_parameter_name); + + ATF_ADD_TEST_CASE(tcs, clear_bindings); +} diff --git a/utils/sqlite/test_utils.hpp b/utils/sqlite/test_utils.hpp new file mode 100644 index 000000000000..bf35d209a164 --- /dev/null +++ b/utils/sqlite/test_utils.hpp @@ -0,0 +1,151 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sqlite/test_utils.hpp +/// Utilities for tests of the sqlite modules. +/// +/// This file is intended to be included once, and only once, for every test +/// program that needs it. All the code is herein contained to simplify the +/// dependency chain in the build rules. + +#if !defined(UTILS_SQLITE_TEST_UTILS_HPP) +# define UTILS_SQLITE_TEST_UTILS_HPP +#else +# error "test_utils.hpp can only be included once" +#endif + +#include + +#include + +#include "utils/defs.hpp" +#include "utils/sqlite/c_gate.hpp" +#include "utils/sqlite/exceptions.hpp" + + +namespace { + + +/// Checks that a given expression raises a particular sqlite::api_error. +/// +/// We cannot make any assumptions regarding the error text provided by SQLite, +/// so we resort to checking only which API function raised the error (because +/// our code is the one hardcoding these strings). +/// +/// \param exp_api_function The name of the SQLite 3 C API function that causes +/// the error. +/// \param statement The statement to execute. +#define REQUIRE_API_ERROR(exp_api_function, statement) \ + do { \ + try { \ + statement; \ + ATF_FAIL("api_error not raised by " #statement); \ + } catch (const utils::sqlite::api_error& api_error) { \ + ATF_REQUIRE_EQ(exp_api_function, api_error.api_function()); \ + } \ + } while (0) + + +/// Gets the pointer to the internal sqlite3 of a database object. +/// +/// This is pure syntactic sugar to simplify typing in the test cases. +/// +/// \param db The SQLite database. +/// +/// \return The internal sqlite3 of the input database. +static inline ::sqlite3* +raw(utils::sqlite::database& db) +{ + return utils::sqlite::database_c_gate(db).c_database(); +} + + +/// SQL commands to create a test table. +/// +/// See create_test_table() for more details. +static const char* create_test_table_sql = + "CREATE TABLE test (prime INTEGER PRIMARY KEY);" + "INSERT INTO test (prime) VALUES (1);\n" + "INSERT INTO test (prime) VALUES (2);\n" + "INSERT INTO test (prime) VALUES (7);\n" + "INSERT INTO test (prime) VALUES (5);\n" + "INSERT INTO test (prime) VALUES (3);\n"; + + +static void create_test_table(::sqlite3*) UTILS_UNUSED; + + +/// Creates a 'test' table in a database. +/// +/// The created 'test' table is populated with a few rows. If there are any +/// problems creating the database, this fails the test case. +/// +/// Use the verify_test_table() function on the same database to verify that +/// the table is present and contains the expected data. +/// +/// \param db The database in which to create the test table. +static void +create_test_table(::sqlite3* db) +{ + std::cout << "Creating 'test' table in the database\n"; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_exec(db, create_test_table_sql, + NULL, NULL, NULL)); +} + + +static void verify_test_table(::sqlite3*) UTILS_UNUSED; + + +/// Verifies that the specified database contains the 'test' table. +/// +/// This function ensures that the provided database contains the 'test' table +/// created by the create_test_table() function on the same database. If it +/// doesn't, this fails the caller test case. +/// +/// \param db The database to validate. +static void +verify_test_table(::sqlite3* db) +{ + std::cout << "Verifying that the 'test' table is in the database\n"; + char **result; + int rows, columns; + ATF_REQUIRE_EQ(SQLITE_OK, ::sqlite3_get_table(db, + "SELECT * FROM test ORDER BY prime", &result, &rows, &columns, NULL)); + ATF_REQUIRE_EQ(5, rows); + ATF_REQUIRE_EQ(1, columns); + ATF_REQUIRE_EQ("prime", std::string(result[0])); + ATF_REQUIRE_EQ("1", std::string(result[1])); + ATF_REQUIRE_EQ("2", std::string(result[2])); + ATF_REQUIRE_EQ("3", std::string(result[3])); + ATF_REQUIRE_EQ("5", std::string(result[4])); + ATF_REQUIRE_EQ("7", std::string(result[5])); + ::sqlite3_free_table(result); +} + + +} // anonymous namespace diff --git a/utils/sqlite/transaction.cpp b/utils/sqlite/transaction.cpp new file mode 100644 index 000000000000..e0235fef9c57 --- /dev/null +++ b/utils/sqlite/transaction.cpp @@ -0,0 +1,142 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/transaction.hpp" + +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace sqlite = utils::sqlite; + + +/// Internal implementation for the transaction. +struct utils::sqlite::transaction::impl : utils::noncopyable { + /// The database this transaction belongs to. + database& db; + + /// Possible statuses of a transaction. + enum statuses { + open_status, + committed_status, + rolled_back_status, + }; + + /// The current status of the transaction. + statuses status; + + /// Constructs a new transaction. + /// + /// \param db_ The database this transaction belongs to. + /// \param status_ The status of the new transaction. + impl(database& db_, const statuses status_) : + db(db_), + status(status_) + { + } + + /// Destroys the transaction. + /// + /// This rolls back the transaction if it is open. + ~impl(void) + { + if (status == impl::open_status) { + try { + rollback(); + } catch (const sqlite::error& e) { + LW(F("Error while rolling back a transaction: %s") % e.what()); + } + } + } + + /// Commits the transaction. + /// + /// \throw api_error If there is any problem while committing the + /// transaction. + void + commit(void) + { + PRE(status == impl::open_status); + db.exec("COMMIT"); + status = impl::committed_status; + } + + /// Rolls the transaction back. + /// + /// \throw api_error If there is any problem while rolling the + /// transaction back. + void + rollback(void) + { + PRE(status == impl::open_status); + db.exec("ROLLBACK"); + status = impl::rolled_back_status; + } +}; + + +/// Initializes a transaction object. +/// +/// This is an internal function. Use database::begin_transaction() to +/// instantiate one of these objects. +/// +/// \param db The database this transaction belongs to. +sqlite::transaction::transaction(database& db) : + _pimpl(new impl(db, impl::open_status)) +{ +} + + +/// Destructor for the transaction. +sqlite::transaction::~transaction(void) +{ +} + + +/// Commits the transaction. +/// +/// \throw api_error If there is any problem while committing the transaction. +void +sqlite::transaction::commit(void) +{ + _pimpl->commit(); +} + + +/// Rolls the transaction back. +/// +/// \throw api_error If there is any problem while rolling the transaction back. +void +sqlite::transaction::rollback(void) +{ + _pimpl->rollback(); +} diff --git a/utils/sqlite/transaction.hpp b/utils/sqlite/transaction.hpp new file mode 100644 index 000000000000..71f3b0c93f4a --- /dev/null +++ b/utils/sqlite/transaction.hpp @@ -0,0 +1,69 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sqlite/transaction.hpp +/// A RAII model for SQLite transactions. + +#if !defined(UTILS_SQLITE_TRANSACTION_HPP) +#define UTILS_SQLITE_TRANSACTION_HPP + +#include "utils/sqlite/transaction_fwd.hpp" + +#include + +#include "utils/sqlite/database_fwd.hpp" + +namespace utils { +namespace sqlite { + + +/// A RAII model for an SQLite 3 statement. +/// +/// A transaction is automatically rolled back when it goes out of scope unless +/// it has been explicitly committed. +class transaction { + struct impl; + + /// Pointer to the shared internal implementation. + std::shared_ptr< impl > _pimpl; + + explicit transaction(database&); + friend class database; + +public: + ~transaction(void); + + void commit(void); + void rollback(void); +}; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_TRANSACTION_HPP) diff --git a/utils/sqlite/transaction_fwd.hpp b/utils/sqlite/transaction_fwd.hpp new file mode 100644 index 000000000000..7773d8380458 --- /dev/null +++ b/utils/sqlite/transaction_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/sqlite/transaction_fwd.hpp +/// Forward declarations for utils/sqlite/transaction.hpp + +#if !defined(UTILS_SQLITE_TRANSACTION_FWD_HPP) +#define UTILS_SQLITE_TRANSACTION_FWD_HPP + +namespace utils { +namespace sqlite { + + +class transaction; + + +} // namespace sqlite +} // namespace utils + +#endif // !defined(UTILS_SQLITE_TRANSACTION_FWD_HPP) diff --git a/utils/sqlite/transaction_test.cpp b/utils/sqlite/transaction_test.cpp new file mode 100644 index 000000000000..d53e6fba4378 --- /dev/null +++ b/utils/sqlite/transaction_test.cpp @@ -0,0 +1,135 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/sqlite/transaction.hpp" + +#include + +#include "utils/format/macros.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.ipp" + +namespace sqlite = utils::sqlite; + + +namespace { + + +/// Ensures that a table has a single specific value in a column. +/// +/// \param db The SQLite database. +/// \param table_name The table to be checked. +/// \param column_name The column to be checked. +/// \param exp_value The value expected to be found in the column. +/// +/// \return True if the column contains a single value and it matches exp_value; +/// false if not. If the query fails, the calling test is marked as bad. +static bool +check_in_table(sqlite::database& db, const char* table_name, + const char* column_name, int exp_value) +{ + sqlite::statement stmt = db.create_statement( + F("SELECT * FROM %s WHERE %s == %s") % table_name % column_name % + exp_value); + if (!stmt.step()) + return false; + if (stmt.step()) + ATF_FAIL("More than one value found in table"); + return true; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(automatic_rollback); +ATF_TEST_CASE_BODY(automatic_rollback) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE t (col INTEGER PRIMARY KEY)"); + db.exec("INSERT INTO t VALUES (3)"); + { + sqlite::transaction tx = db.begin_transaction(); + db.exec("INSERT INTO t VALUES (5)"); + } + ATF_REQUIRE( check_in_table(db, "t", "col", 3)); + ATF_REQUIRE(!check_in_table(db, "t", "col", 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(explicit_commit); +ATF_TEST_CASE_BODY(explicit_commit) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE t (col INTEGER PRIMARY KEY)"); + db.exec("INSERT INTO t VALUES (3)"); + { + sqlite::transaction tx = db.begin_transaction(); + db.exec("INSERT INTO t VALUES (5)"); + tx.commit(); + } + ATF_REQUIRE(check_in_table(db, "t", "col", 3)); + ATF_REQUIRE(check_in_table(db, "t", "col", 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(explicit_rollback); +ATF_TEST_CASE_BODY(explicit_rollback) +{ + sqlite::database db = sqlite::database::in_memory(); + db.exec("CREATE TABLE t (col INTEGER PRIMARY KEY)"); + db.exec("INSERT INTO t VALUES (3)"); + { + sqlite::transaction tx = db.begin_transaction(); + db.exec("INSERT INTO t VALUES (5)"); + tx.rollback(); + } + ATF_REQUIRE( check_in_table(db, "t", "col", 3)); + ATF_REQUIRE(!check_in_table(db, "t", "col", 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(nested_fail); +ATF_TEST_CASE_BODY(nested_fail) +{ + sqlite::database db = sqlite::database::in_memory(); + { + sqlite::transaction tx = db.begin_transaction(); + ATF_REQUIRE_THROW(sqlite::error, db.begin_transaction()); + } +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, automatic_rollback); + ATF_ADD_TEST_CASE(tcs, explicit_commit); + ATF_ADD_TEST_CASE(tcs, explicit_rollback); + ATF_ADD_TEST_CASE(tcs, nested_fail); +} diff --git a/utils/stacktrace.cpp b/utils/stacktrace.cpp new file mode 100644 index 000000000000..11636b31959f --- /dev/null +++ b/utils/stacktrace.cpp @@ -0,0 +1,370 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/stacktrace.hpp" + +extern "C" { +#include +#include + +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" +#include "utils/process/executor.ipp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" + +namespace datetime = utils::datetime; +namespace executor = utils::process::executor; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::none; +using utils::optional; + + +/// Built-in path to GDB. +/// +/// This is the value that should be passed to the find_gdb() function. If this +/// is an absolute path, then we use the binary specified by the variable; if it +/// is a relative path, we look for the binary in the path. +/// +/// Test cases can override the value of this built-in constant to unit-test the +/// behavior of the functions below. +const char* utils::builtin_gdb = GDB; + + +/// Maximum time the external GDB process is allowed to run for. +datetime::delta utils::gdb_timeout(60, 0); + + +namespace { + + +/// Maximum length of the core file name, if known. +/// +/// Some operating systems impose a maximum length on the basename of the core +/// file. If MAXCOMLEN is defined, then we need to truncate the program name to +/// this length before searching for the core file. If no such limit is known, +/// this is infinite. +static const std::string::size_type max_core_name_length = +#if defined(MAXCOMLEN) + MAXCOMLEN +#else + std::string::npos +#endif + ; + + +/// Functor to execute GDB in a subprocess. +class run_gdb { + /// Path to the GDB binary to use. + const fs::path& _gdb; + + /// Path to the program being debugged. + const fs::path& _program; + + /// Path to the dumped core. + const fs::path& _core_name; + +public: + /// Constructs the functor. + /// + /// \param gdb_ Path to the GDB binary to use. + /// \param program_ Path to the program being debugged. Can be relative to + /// the given work directory. + /// \param core_name_ Path to the dumped core. Use find_core() to deduce + /// a valid candidate. Can be relative to the given work directory. + run_gdb(const fs::path& gdb_, const fs::path& program_, + const fs::path& core_name_) : + _gdb(gdb_), _program(program_), _core_name(core_name_) + { + } + + /// Executes GDB. + /// + /// \param control_directory Directory where we can store control files to + /// not clobber any files created by the program being debugged. + void + operator()(const fs::path& control_directory) + { + const fs::path gdb_script_path = control_directory / "gdb.script"; + + // Old versions of GDB, such as the one shipped by FreeBSD as of + // 11.0-CURRENT on 2014-11-26, do not support scripts on the command + // line via the '-ex' flag. Instead, we have to create a script file + // and use that instead. + std::ofstream gdb_script(gdb_script_path.c_str()); + if (!gdb_script) { + std::cerr << "Cannot create GDB script\n"; + ::_exit(EXIT_FAILURE); + } + gdb_script << "backtrace\n"; + gdb_script.close(); + + utils::unsetenv("TERM"); + + std::vector< std::string > args; + args.push_back("-batch"); + args.push_back("-q"); + args.push_back("-x"); + args.push_back(gdb_script_path.str()); + args.push_back(_program.str()); + args.push_back(_core_name.str()); + + // Force all GDB output to go to stderr. We print messages to stderr + // when grabbing the stacktrace and we do not want GDB's output to end + // up split in two different files. + if (::dup2(STDERR_FILENO, STDOUT_FILENO) == -1) { + std::cerr << "Cannot redirect stdout to stderr\n"; + ::_exit(EXIT_FAILURE); + } + + process::exec(_gdb, args); + } +}; + + +} // anonymous namespace + + +/// Looks for the path to the GDB binary. +/// +/// \return The absolute path to the GDB binary if any, otherwise none. Note +/// that the returned path may or may not be valid: there is no guarantee that +/// the path exists and is executable. +optional< fs::path > +utils::find_gdb(void) +{ + if (std::strlen(builtin_gdb) == 0) { + LW("The builtin path to GDB is bogus, which probably indicates a bug " + "in the build system; cannot gather stack traces"); + return none; + } + + const fs::path gdb(builtin_gdb); + if (gdb.is_absolute()) + return utils::make_optional(gdb); + else + return fs::find_in_path(gdb.c_str()); +} + + +/// Looks for a core file for the given program. +/// +/// \param program The name of the binary that generated the core file. Can be +/// either absolute or relative. +/// \param status The exit status of the program. This is necessary to gather +/// the PID. +/// \param work_directory The directory from which the program was run. +/// +/// \return The path to the core file, if found; otherwise none. +optional< fs::path > +utils::find_core(const fs::path& program, const process::status& status, + const fs::path& work_directory) +{ + std::vector< fs::path > candidates; + + candidates.push_back(work_directory / + (program.leaf_name().substr(0, max_core_name_length) + ".core")); + if (program.is_absolute()) { + candidates.push_back(program.branch_path() / + (program.leaf_name().substr(0, max_core_name_length) + ".core")); + } + candidates.push_back(work_directory / (F("core.%s") % status.dead_pid())); + candidates.push_back(fs::path("/cores") / + (F("core.%s") % status.dead_pid())); + + for (std::vector< fs::path >::const_iterator iter = candidates.begin(); + iter != candidates.end(); ++iter) { + if (fs::exists(*iter)) { + LD(F("Attempting core file candidate %s: found") % *iter); + return utils::make_optional(*iter); + } else { + LD(F("Attempting core file candidate %s: not found") % *iter); + } + } + return none; +} + + +/// Raises core size limit to its possible maximum. +/// +/// This is a best-effort operation. There is no guarantee that the operation +/// will yield a large-enough limit to generate any possible core file. +/// +/// \return True if the core size could be unlimited; false otherwise. +bool +utils::unlimit_core_size(void) +{ + bool ok; + + struct ::rlimit rl; + if (::getrlimit(RLIMIT_CORE, &rl) == -1) { + const int original_errno = errno; + LW(F("getrlimit should not have failed but got: %s") % + std::strerror(original_errno)); + ok = false; + } else { + if (rl.rlim_max == 0) { + LW("getrlimit returned 0 for RLIMIT_CORE rlim_max; cannot raise " + "soft core limit"); + ok = false; + } else { + rl.rlim_cur = rl.rlim_max; + LD(F("Raising soft core size limit to %s (hard value)") % + rl.rlim_cur); + if (::setrlimit(RLIMIT_CORE, &rl) == -1) { + const int original_errno = errno; + LW(F("setrlimit should not have failed but got: %s") % + std::strerror(original_errno)); + ok = false; + } else { + ok = true; + } + } + } + + return ok; +} + + +/// Gathers a stacktrace of a crashed program. +/// +/// \param program The name of the binary that crashed and dumped a core file. +/// Can be either absolute or relative. +/// \param executor_handle The executor handler to get the status from and +/// gdb handler from. +/// \param exit_handle The exit handler to stream additional diagnostic +/// information from (stderr) and for redirecting to additional +/// information to gdb from. +/// +/// \post If anything goes wrong, the diagnostic messages are written to the +/// output. This function should not throw. +void +utils::dump_stacktrace(const fs::path& program, + executor::executor_handle& executor_handle, + const executor::exit_handle& exit_handle) +{ + PRE(exit_handle.status()); + const process::status& status = exit_handle.status().get(); + PRE(status.signaled() && status.coredump()); + + std::ofstream gdb_err(exit_handle.stderr_file().c_str(), std::ios::app); + if (!gdb_err) { + LW(F("Failed to open %s to append GDB's output") % + exit_handle.stderr_file()); + return; + } + + gdb_err << F("Process with PID %s exited with signal %s and dumped core; " + "attempting to gather stack trace\n") % + status.dead_pid() % status.termsig(); + + const optional< fs::path > gdb = utils::find_gdb(); + if (!gdb) { + gdb_err << F("Cannot find GDB binary; builtin was '%s'\n") % + builtin_gdb; + return; + } + + const optional< fs::path > core_file = find_core( + program, status, exit_handle.work_directory()); + if (!core_file) { + gdb_err << F("Cannot find any core file\n"); + return; + } + + gdb_err.flush(); + const executor::exec_handle exec_handle = + executor_handle.spawn_followup( + run_gdb(gdb.get(), program, core_file.get()), + exit_handle, gdb_timeout); + const executor::exit_handle gdb_exit_handle = + executor_handle.wait(exec_handle); + + const optional< process::status >& gdb_status = gdb_exit_handle.status(); + if (!gdb_status) { + gdb_err << "GDB timed out\n"; + } else { + if (gdb_status.get().exited() && + gdb_status.get().exitstatus() == EXIT_SUCCESS) { + gdb_err << "GDB exited successfully\n"; + } else { + gdb_err << "GDB failed; see output above for details\n"; + } + } +} + + +/// Gathers a stacktrace of a program if it crashed. +/// +/// This is just a convenience function to allow appending the stacktrace to an +/// existing file and to permit reusing the status as returned by auxiliary +/// process-spawning functions. +/// +/// \param program The name of the binary that crashed and dumped a core file. +/// Can be either absolute or relative. +/// \param executor_handle The executor handler to get the status from and +/// gdb handler from. +/// \param exit_handle The exit handler to stream additional diagnostic +/// information from (stderr) and for redirecting to additional +/// information to gdb from. +/// +/// \throw std::runtime_error If the output file cannot be opened. +/// +/// \post If anything goes wrong with the stack gatheringq, the diagnostic +/// messages are written to the output. +void +utils::dump_stacktrace_if_available(const fs::path& program, + executor::executor_handle& executor_handle, + const executor::exit_handle& exit_handle) +{ + const optional< process::status >& status = exit_handle.status(); + if (!status || !status.get().signaled() || !status.get().coredump()) + return; + + dump_stacktrace(program, executor_handle, exit_handle); +} diff --git a/utils/stacktrace.hpp b/utils/stacktrace.hpp new file mode 100644 index 000000000000..13648e2c71cb --- /dev/null +++ b/utils/stacktrace.hpp @@ -0,0 +1,68 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/stacktrace.hpp +/// Utilities to gather a stacktrace of a crashing binary. + +#if !defined(ENGINE_STACKTRACE_HPP) +#define ENGINE_STACKTRACE_HPP + +#include + +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" +#include "utils/process/executor_fwd.hpp" +#include "utils/process/status_fwd.hpp" + +namespace utils { + + +extern const char* builtin_gdb; +extern utils::datetime::delta gdb_timeout; + +utils::optional< utils::fs::path > find_gdb(void); + +utils::optional< utils::fs::path > find_core(const utils::fs::path&, + const utils::process::status&, + const utils::fs::path&); + +bool unlimit_core_size(void); + +void dump_stacktrace(const utils::fs::path&, + utils::process::executor::executor_handle&, + const utils::process::executor::exit_handle&); + +void dump_stacktrace_if_available(const utils::fs::path&, + utils::process::executor::executor_handle&, + const utils::process::executor::exit_handle&); + + +} // namespace utils + +#endif // !defined(ENGINE_STACKTRACE_HPP) diff --git a/utils/stacktrace_helper.cpp b/utils/stacktrace_helper.cpp new file mode 100644 index 000000000000..f01e8c809797 --- /dev/null +++ b/utils/stacktrace_helper.cpp @@ -0,0 +1,36 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include + + +int +main(void) +{ + std::abort(); +} diff --git a/utils/stacktrace_test.cpp b/utils/stacktrace_test.cpp new file mode 100644 index 000000000000..ca87e7087f5a --- /dev/null +++ b/utils/stacktrace_test.cpp @@ -0,0 +1,620 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/stacktrace.hpp" + +extern "C" { +#include +#include +#include + +#include +#include +} + +#include +#include + +#include + +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/process/executor.ipp" +#include "utils/process/child.ipp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" +#include "utils/sanity.hpp" +#include "utils/test_utils.ipp" + +namespace datetime = utils::datetime; +namespace executor = utils::process::executor; +namespace fs = utils::fs; +namespace process = utils::process; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Functor to execute a binary in a subprocess. +/// +/// The provided binary is copied to the current work directory before being +/// executed and the copy is given the name chosen by the user. The copy is +/// necessary so that we have a deterministic location for where core files may +/// be dumped (if they happen to be dumped in the current directory). +class crash_me { + /// Path to the binary to execute. + const fs::path _binary; + + /// Name of the binary after being copied. + const fs::path _copy_name; + +public: + /// Constructor. + /// + /// \param binary_ Path to binary to execute. + /// \param copy_name_ Name of the binary after being copied. If empty, + /// use the leaf name of binary_. + explicit crash_me(const fs::path& binary_, + const std::string& copy_name_ = "") : + _binary(binary_), + _copy_name(copy_name_.empty() ? binary_.leaf_name() : copy_name_) + { + } + + /// Runs the binary. + void + operator()(void) const UTILS_NORETURN + { + atf::utils::copy_file(_binary.str(), _copy_name.str()); + + const std::vector< std::string > args; + process::exec(_copy_name, args); + } + + /// Runs the binary. + /// + /// This interface is exposed to support passing crash_me to the executor. + void + operator()(const fs::path& /* control_directory */) const + UTILS_NORETURN + { + (*this)(); // Delegate to ensure the two entry points remain in sync. + } +}; + + +static void child_exit(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that exits cleanly. +static void +child_exit(const fs::path& /* control_directory */) +{ + ::_exit(EXIT_SUCCESS); +} + + +static void child_pause(const fs::path&) UTILS_NORETURN; + + +/// Subprocess that just blocks. +static void +child_pause(const fs::path& /* control_directory */) +{ + sigset_t mask; + sigemptyset(&mask); + for (;;) { + ::sigsuspend(&mask); + } + std::abort(); +} + + +/// Generates a core dump, if possible. +/// +/// \post If this fails to generate a core file, the test case is marked as +/// skipped. The caller can rely on this when attempting further checks on the +/// core dump by assuming that the core dump exists somewhere. +/// +/// \param test_case Pointer to the caller test case, needed to obtain the path +/// to the source directory. +/// \param base_name Name of the binary to execute, which will be a copy of a +/// helper binary that always crashes. This name should later be part of +/// the core filename. +/// +/// \return The status of the crashed binary. +static process::status +generate_core(const atf::tests::tc* test_case, const char* base_name) +{ + utils::prepare_coredump_test(test_case); + + const fs::path helper = fs::path(test_case->get_config_var("srcdir")) / + "stacktrace_helper"; + + const process::status status = process::child::fork_files( + crash_me(helper, base_name), + fs::path("unused.out"), fs::path("unused.err"))->wait(); + ATF_REQUIRE(status.signaled()); + if (!status.coredump()) + ATF_SKIP("Test failed to generate core dump"); + return status; +} + + +/// Generates a core dump, if possible. +/// +/// \post If this fails to generate a core file, the test case is marked as +/// skipped. The caller can rely on this when attempting further checks on the +/// core dump by assuming that the core dump exists somewhere. +/// +/// \param test_case Pointer to the caller test case, needed to obtain the path +/// to the source directory. +/// \param base_name Name of the binary to execute, which will be a copy of a +/// helper binary that always crashes. This name should later be part of +/// the core filename. +/// \param executor_handle Executor to use to generate the core dump. +/// +/// \return The exit handle of the subprocess so that a stacktrace can be +/// executed reusing this context later on. +static executor::exit_handle +generate_core(const atf::tests::tc* test_case, const char* base_name, + executor::executor_handle& executor_handle) +{ + utils::prepare_coredump_test(test_case); + + const fs::path helper = fs::path(test_case->get_config_var("srcdir")) / + "stacktrace_helper"; + + const executor::exec_handle exec_handle = executor_handle.spawn( + crash_me(helper, base_name), datetime::delta(60, 0), none, none, none); + const executor::exit_handle exit_handle = executor_handle.wait(exec_handle); + + if (!exit_handle.status()) + ATF_SKIP("Test failed to generate core dump (timed out)"); + const process::status& status = exit_handle.status().get(); + ATF_REQUIRE(status.signaled()); + if (!status.coredump()) + ATF_SKIP("Test failed to generate core dump"); + + return exit_handle; +} + + +/// Creates a script. +/// +/// \param script Path to the script to create. +/// \param contents Contents of the script. +static void +create_script(const char* script, const std::string& contents) +{ + atf::utils::create_file(script, "#! /bin/sh\n\n" + contents); + ATF_REQUIRE(::chmod(script, 0755) != -1); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(unlimit_core_size); +ATF_TEST_CASE_BODY(unlimit_core_size) +{ + utils::require_run_coredump_tests(this); + + struct rlimit rl; + rl.rlim_cur = 0; + rl.rlim_max = RLIM_INFINITY; + if (::setrlimit(RLIMIT_CORE, &rl) == -1) + skip("Failed to lower the core size limit"); + + ATF_REQUIRE(utils::unlimit_core_size()); + + const fs::path helper = fs::path(get_config_var("srcdir")) / + "stacktrace_helper"; + const process::status status = process::child::fork_files( + crash_me(helper), + fs::path("unused.out"), fs::path("unused.err"))->wait(); + ATF_REQUIRE(status.signaled()); + if (!status.coredump()) + fail("Core not dumped as expected"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(unlimit_core_size__hard_is_zero); +ATF_TEST_CASE_BODY(unlimit_core_size__hard_is_zero) +{ + utils::require_run_coredump_tests(this); + + struct rlimit rl; + rl.rlim_cur = 0; + rl.rlim_max = 0; + if (::setrlimit(RLIMIT_CORE, &rl) == -1) + skip("Failed to lower the core size limit"); + + ATF_REQUIRE(!utils::unlimit_core_size()); + + const fs::path helper = fs::path(get_config_var("srcdir")) / + "stacktrace_helper"; + const process::status status = process::child::fork_files( + crash_me(helper), + fs::path("unused.out"), fs::path("unused.err"))->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE(!status.coredump()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_gdb__use_builtin); +ATF_TEST_CASE_BODY(find_gdb__use_builtin) +{ + utils::builtin_gdb = "/path/to/gdb"; + optional< fs::path > gdb = utils::find_gdb(); + ATF_REQUIRE(gdb); + ATF_REQUIRE_EQ("/path/to/gdb", gdb.get().str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_gdb__search_builtin__ok); +ATF_TEST_CASE_BODY(find_gdb__search_builtin__ok) +{ + atf::utils::create_file("custom-name", ""); + ATF_REQUIRE(::chmod("custom-name", 0755) != -1); + const fs::path exp_gdb = fs::path("custom-name").to_absolute(); + + utils::setenv("PATH", "/non-existent/location:.:/bin"); + + utils::builtin_gdb = "custom-name"; + optional< fs::path > gdb = utils::find_gdb(); + ATF_REQUIRE(gdb); + ATF_REQUIRE_EQ(exp_gdb, gdb.get()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_gdb__search_builtin__fail); +ATF_TEST_CASE_BODY(find_gdb__search_builtin__fail) +{ + utils::setenv("PATH", "."); + utils::builtin_gdb = "foo"; + optional< fs::path > gdb = utils::find_gdb(); + ATF_REQUIRE(!gdb); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_gdb__bogus_value); +ATF_TEST_CASE_BODY(find_gdb__bogus_value) +{ + utils::builtin_gdb = ""; + optional< fs::path > gdb = utils::find_gdb(); + ATF_REQUIRE(!gdb); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_core__found__short); +ATF_TEST_CASE_BODY(find_core__found__short) +{ + const process::status status = generate_core(this, "short"); + INV(status.coredump()); + const optional< fs::path > core_name = utils::find_core( + fs::path("short"), status, fs::path(".")); + if (!core_name) + fail("Core dumped, but no candidates found"); + ATF_REQUIRE(core_name.get().str().find("core") != std::string::npos); + ATF_REQUIRE(fs::exists(core_name.get())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_core__found__long); +ATF_TEST_CASE_BODY(find_core__found__long) +{ + const process::status status = generate_core( + this, "long-name-that-may-be-truncated-in-some-systems"); + INV(status.coredump()); + const optional< fs::path > core_name = utils::find_core( + fs::path("long-name-that-may-be-truncated-in-some-systems"), + status, fs::path(".")); + if (!core_name) + fail("Core dumped, but no candidates found"); + ATF_REQUIRE(core_name.get().str().find("core") != std::string::npos); + ATF_REQUIRE(fs::exists(core_name.get())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(find_core__not_found); +ATF_TEST_CASE_BODY(find_core__not_found) +{ + const process::status status = process::status::fake_signaled(SIGILL, true); + const optional< fs::path > core_name = utils::find_core( + fs::path("missing"), status, fs::path(".")); + if (core_name) + fail("Core not dumped, but candidate found: " + core_name.get().str()); +} + + +ATF_TEST_CASE(dump_stacktrace__integration); +ATF_TEST_CASE_HEAD(dump_stacktrace__integration) +{ + set_md_var("require.progs", utils::builtin_gdb); +} +ATF_TEST_CASE_BODY(dump_stacktrace__integration) +{ + executor::executor_handle handle = executor::setup(); + + executor::exit_handle exit_handle = generate_core(this, "short", handle); + INV(exit_handle.status()); + INV(exit_handle.status().get().coredump()); + + std::ostringstream output; + utils::dump_stacktrace(fs::path("short"), handle, exit_handle); + + // It is hard to validate the execution of an arbitrary GDB of which we do + // not know anything. Just assume that the backtrace, at the very least, + // prints a couple of frame identifiers. + ATF_REQUIRE(!atf::utils::grep_file("#0", exit_handle.stdout_file().str())); + ATF_REQUIRE( atf::utils::grep_file("#0", exit_handle.stderr_file().str())); + ATF_REQUIRE(!atf::utils::grep_file("#1", exit_handle.stdout_file().str())); + ATF_REQUIRE( atf::utils::grep_file("#1", exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__ok); +ATF_TEST_CASE_BODY(dump_stacktrace__ok) +{ + utils::setenv("PATH", "."); + create_script("fake-gdb", "echo 'frame 1'; echo 'frame 2'; " + "echo 'some warning' 1>&2; exit 0"); + utils::builtin_gdb = "fake-gdb"; + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + INV(exit_handle.status()); + INV(exit_handle.status().get().coredump()); + + utils::dump_stacktrace(fs::path("short"), handle, exit_handle); + + // Note how all output is expected on stderr even for the messages that the + // script decided to send to stdout. + ATF_REQUIRE(atf::utils::grep_file("exited with signal [0-9]* and dumped", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("^frame 1$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("^frame 2$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("^some warning$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("GDB exited successfully", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__cannot_find_core); +ATF_TEST_CASE_BODY(dump_stacktrace__cannot_find_core) +{ + // Make sure we can find a GDB binary so that we don't fail the test for + // the wrong reason. + utils::setenv("PATH", "."); + utils::builtin_gdb = "fake-gdb"; + atf::utils::create_file("fake-gdb", "unused"); + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + const optional< fs::path > core_name = utils::find_core( + fs::path("short"), + exit_handle.status().get(), + exit_handle.work_directory()); + if (core_name) { + // This is needed even if we provide a different basename to + // dump_stacktrace below because the system policies may be generating + // core dumps by PID, not binary name. + std::cout << "Removing core dump: " << core_name << '\n'; + fs::unlink(core_name.get()); + } + + utils::dump_stacktrace(fs::path("fake"), handle, exit_handle); + + atf::utils::cat_file(exit_handle.stdout_file().str(), "stdout: "); + atf::utils::cat_file(exit_handle.stderr_file().str(), "stderr: "); + ATF_REQUIRE(atf::utils::grep_file("Cannot find any core file", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__cannot_find_gdb); +ATF_TEST_CASE_BODY(dump_stacktrace__cannot_find_gdb) +{ + utils::setenv("PATH", "."); + utils::builtin_gdb = "missing-gdb"; + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + utils::dump_stacktrace(fs::path("fake"), handle, exit_handle); + + ATF_REQUIRE(atf::utils::grep_file( + "Cannot find GDB binary; builtin was 'missing-gdb'", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__gdb_fail); +ATF_TEST_CASE_BODY(dump_stacktrace__gdb_fail) +{ + utils::setenv("PATH", "."); + create_script("fake-gdb", "echo 'foo'; echo 'bar' 1>&2; exit 1"); + const std::string gdb = (fs::current_path() / "fake-gdb").str(); + utils::builtin_gdb = gdb.c_str(); + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + atf::utils::create_file((exit_handle.work_directory() / "fake.core").str(), + "Invalid core file, but not read"); + utils::dump_stacktrace(fs::path("fake"), handle, exit_handle); + + ATF_REQUIRE(atf::utils::grep_file("^foo$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("^bar$", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("GDB failed; see output above", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace__gdb_timeout); +ATF_TEST_CASE_BODY(dump_stacktrace__gdb_timeout) +{ + utils::setenv("PATH", "."); + create_script("fake-gdb", "while :; do sleep 1; done"); + const std::string gdb = (fs::current_path() / "fake-gdb").str(); + utils::builtin_gdb = gdb.c_str(); + utils::gdb_timeout = datetime::delta(1, 0); + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + atf::utils::create_file((exit_handle.work_directory() / "fake.core").str(), + "Invalid core file, but not read"); + utils::dump_stacktrace(fs::path("fake"), handle, exit_handle); + + ATF_REQUIRE(atf::utils::grep_file("GDB timed out", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace_if_available__append); +ATF_TEST_CASE_BODY(dump_stacktrace_if_available__append) +{ + utils::setenv("PATH", "."); + create_script("fake-gdb", "echo 'frame 1'; exit 0"); + utils::builtin_gdb = "fake-gdb"; + + executor::executor_handle handle = executor::setup(); + executor::exit_handle exit_handle = generate_core(this, "short", handle); + + atf::utils::create_file(exit_handle.stdout_file().str(), "Pre-stdout"); + atf::utils::create_file(exit_handle.stderr_file().str(), "Pre-stderr"); + + utils::dump_stacktrace_if_available(fs::path("short"), handle, exit_handle); + + ATF_REQUIRE(atf::utils::grep_file("Pre-stdout", + exit_handle.stdout_file().str())); + ATF_REQUIRE(atf::utils::grep_file("Pre-stderr", + exit_handle.stderr_file().str())); + ATF_REQUIRE(atf::utils::grep_file("frame 1", + exit_handle.stderr_file().str())); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace_if_available__no_status); +ATF_TEST_CASE_BODY(dump_stacktrace_if_available__no_status) +{ + executor::executor_handle handle = executor::setup(); + const executor::exec_handle exec_handle = handle.spawn( + child_pause, datetime::delta(0, 100000), none, none, none); + executor::exit_handle exit_handle = handle.wait(exec_handle); + INV(!exit_handle.status()); + + utils::dump_stacktrace_if_available(fs::path("short"), handle, exit_handle); + ATF_REQUIRE(atf::utils::compare_file(exit_handle.stdout_file().str(), "")); + ATF_REQUIRE(atf::utils::compare_file(exit_handle.stderr_file().str(), "")); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(dump_stacktrace_if_available__no_coredump); +ATF_TEST_CASE_BODY(dump_stacktrace_if_available__no_coredump) +{ + executor::executor_handle handle = executor::setup(); + const executor::exec_handle exec_handle = handle.spawn( + child_exit, datetime::delta(60, 0), none, none, none); + executor::exit_handle exit_handle = handle.wait(exec_handle); + INV(exit_handle.status()); + INV(exit_handle.status().get().exited()); + INV(exit_handle.status().get().exitstatus() == EXIT_SUCCESS); + + utils::dump_stacktrace_if_available(fs::path("short"), handle, exit_handle); + ATF_REQUIRE(atf::utils::compare_file(exit_handle.stdout_file().str(), "")); + ATF_REQUIRE(atf::utils::compare_file(exit_handle.stderr_file().str(), "")); + + exit_handle.cleanup(); + handle.cleanup(); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, unlimit_core_size); + ATF_ADD_TEST_CASE(tcs, unlimit_core_size__hard_is_zero); + + ATF_ADD_TEST_CASE(tcs, find_gdb__use_builtin); + ATF_ADD_TEST_CASE(tcs, find_gdb__search_builtin__ok); + ATF_ADD_TEST_CASE(tcs, find_gdb__search_builtin__fail); + ATF_ADD_TEST_CASE(tcs, find_gdb__bogus_value); + + ATF_ADD_TEST_CASE(tcs, find_core__found__short); + ATF_ADD_TEST_CASE(tcs, find_core__found__long); + ATF_ADD_TEST_CASE(tcs, find_core__not_found); + + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__integration); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__ok); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__cannot_find_core); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__cannot_find_gdb); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__gdb_fail); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace__gdb_timeout); + + ATF_ADD_TEST_CASE(tcs, dump_stacktrace_if_available__append); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace_if_available__no_status); + ATF_ADD_TEST_CASE(tcs, dump_stacktrace_if_available__no_coredump); +} diff --git a/utils/stream.cpp b/utils/stream.cpp new file mode 100644 index 000000000000..ee3ab417f753 --- /dev/null +++ b/utils/stream.cpp @@ -0,0 +1,149 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/stream.hpp" + +#include +#include +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" + +namespace fs = utils::fs; + + +namespace { + + +/// Constant that represents the path to stdout. +static const fs::path stdout_path("/dev/stdout"); + + +/// Constant that represents the path to stderr. +static const fs::path stderr_path("/dev/stderr"); + + +} // anonymous namespace + + +/// Opens a new file for output, respecting the stdout and stderr streams. +/// +/// \param path The path to the output file to be created. +/// +/// \return A pointer to a new output stream. +std::auto_ptr< std::ostream > +utils::open_ostream(const fs::path& path) +{ + std::auto_ptr< std::ostream > out; + if (path == stdout_path) { + out.reset(new std::ofstream()); + out->copyfmt(std::cout); + out->clear(std::cout.rdstate()); + out->rdbuf(std::cout.rdbuf()); + } else if (path == stderr_path) { + out.reset(new std::ofstream()); + out->copyfmt(std::cerr); + out->clear(std::cerr.rdstate()); + out->rdbuf(std::cerr.rdbuf()); + } else { + out.reset(new std::ofstream(path.c_str())); + if (!(*out)) { + throw std::runtime_error(F("Cannot open output file %s") % path); + } + } + INV(out.get() != NULL); + return out; +} + + +/// Gets the length of a stream. +/// +/// \param is The input stream for which to calculate its length. +/// +/// \return The length of the stream. This is of size_t type instead of +/// directly std::streampos to simplify the caller. Some systems do not +/// support comparing a std::streampos directly to an integer (see +/// NetBSD 1.5.x), which is what we often want to do. +/// +/// \throw std::exception If calculating the length fails due to a stream error. +std::size_t +utils::stream_length(std::istream& is) +{ + const std::streampos current_pos = is.tellg(); + try { + is.seekg(0, std::ios::end); + const std::streampos length = is.tellg(); + is.seekg(current_pos, std::ios::beg); + return static_cast< std::size_t >(length); + } catch (...) { + is.seekg(current_pos, std::ios::beg); + throw; + } +} + + +/// Reads a whole file into memory. +/// +/// \param path The file to read. +/// +/// \return A plain string containing the raw contents of the file. +/// +/// \throw std::runtime_error If the file cannot be opened. +std::string +utils::read_file(const fs::path& path) +{ + std::ifstream input(path.c_str()); + if (!input) + throw std::runtime_error(F("Failed to open '%s' for read") % path); + return read_stream(input); +} + + +/// Reads the whole contents of a stream into memory. +/// +/// \param input The input stream from which to read. +/// +/// \return A plain string containing the raw contents of the file. +std::string +utils::read_stream(std::istream& input) +{ + std::ostringstream buffer; + + char tmp[1024]; + while (input.good()) { + input.read(tmp, sizeof(tmp)); + if (input.good() || input.eof()) { + buffer.write(tmp, input.gcount()); + } + } + + return buffer.str(); +} diff --git a/utils/stream.hpp b/utils/stream.hpp new file mode 100644 index 000000000000..5c9316e72810 --- /dev/null +++ b/utils/stream.hpp @@ -0,0 +1,57 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/stream.hpp +/// Stream manipulation utilities. +/// +/// Note that file-manipulation utilities live in utils::fs instead. The +/// utilities here deal with already-open streams. + +#if !defined(UTILS_STREAM_HPP) +#define UTILS_STREAM_HPP + +#include +#include +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace utils { + + +std::auto_ptr< std::ostream > open_ostream(const utils::fs::path&); +std::size_t stream_length(std::istream&); +std::string read_file(const utils::fs::path&); +std::string read_stream(std::istream&); + + +} // namespace utils + +#endif // !defined(UTILS_STREAM_HPP) diff --git a/utils/stream_test.cpp b/utils/stream_test.cpp new file mode 100644 index 000000000000..7c4f3b5c6b4a --- /dev/null +++ b/utils/stream_test.cpp @@ -0,0 +1,157 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/stream.hpp" + +#include +#include + +#include + +#include "utils/fs/path.hpp" + +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(open_ostream__stdout); +ATF_TEST_CASE_BODY(open_ostream__stdout) +{ + const pid_t pid = atf::utils::fork(); + if (pid == 0) { + std::auto_ptr< std::ostream > output = utils::open_ostream( + fs::path("/dev/stdout")); + (*output) << "Message to stdout\n"; + output.reset(); + std::exit(EXIT_SUCCESS); + } + atf::utils::wait(pid, EXIT_SUCCESS, "Message to stdout\n", ""); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open_ostream__stderr); +ATF_TEST_CASE_BODY(open_ostream__stderr) +{ + const pid_t pid = atf::utils::fork(); + if (pid == 0) { + std::auto_ptr< std::ostream > output = utils::open_ostream( + fs::path("/dev/stderr")); + (*output) << "Message to stderr\n"; + output.reset(); + std::exit(EXIT_SUCCESS); + } + atf::utils::wait(pid, EXIT_SUCCESS, "", "Message to stderr\n"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(open_ostream__other); +ATF_TEST_CASE_BODY(open_ostream__other) +{ + const pid_t pid = atf::utils::fork(); + if (pid == 0) { + std::auto_ptr< std::ostream > output = utils::open_ostream( + fs::path("some-file.txt")); + (*output) << "Message to other file\n"; + output.reset(); + std::exit(EXIT_SUCCESS); + } + atf::utils::wait(pid, EXIT_SUCCESS, "", ""); + atf::utils::compare_file("some-file.txt", "Message to other file\n"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(stream_length__empty); +ATF_TEST_CASE_BODY(stream_length__empty) +{ + std::istringstream input(""); + ATF_REQUIRE_EQ(0, utils::stream_length(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(stream_length__some); +ATF_TEST_CASE_BODY(stream_length__some) +{ + const std::string contents(8192, 'x'); + std::istringstream input(contents); + ATF_REQUIRE_EQ( + contents.length(), + static_cast< std::string::size_type >(utils::stream_length(input))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_file__ok); +ATF_TEST_CASE_BODY(read_file__ok) +{ + const char* contents = "These are\nsome file contents"; + atf::utils::create_file("input.txt", contents); + ATF_REQUIRE_EQ(contents, utils::read_file(fs::path("input.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_file__missing_file); +ATF_TEST_CASE_BODY(read_file__missing_file) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, + "Failed to open 'foo.txt' for read", + utils::read_file(fs::path("foo.txt"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_stream__empty); +ATF_TEST_CASE_BODY(read_stream__empty) +{ + std::istringstream input(""); + ATF_REQUIRE_EQ("", utils::read_stream(input)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(read_stream__some); +ATF_TEST_CASE_BODY(read_stream__some) +{ + std::string contents; + for (int i = 0; i < 1000; i++) + contents += "abcdef"; + std::istringstream input(contents); + ATF_REQUIRE_EQ(contents, utils::read_stream(input)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, open_ostream__stdout); + ATF_ADD_TEST_CASE(tcs, open_ostream__stderr); + ATF_ADD_TEST_CASE(tcs, open_ostream__other); + + ATF_ADD_TEST_CASE(tcs, stream_length__empty); + ATF_ADD_TEST_CASE(tcs, stream_length__some); + + ATF_ADD_TEST_CASE(tcs, read_file__ok); + ATF_ADD_TEST_CASE(tcs, read_file__missing_file); + + ATF_ADD_TEST_CASE(tcs, read_stream__empty); + ATF_ADD_TEST_CASE(tcs, read_stream__some); +} diff --git a/utils/test_utils.ipp b/utils/test_utils.ipp new file mode 100644 index 000000000000..f21d0f4cc172 --- /dev/null +++ b/utils/test_utils.ipp @@ -0,0 +1,113 @@ +// Copyright 2016 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/test_utils.ipp +/// Provides test-only convenience utilities. + +#if defined(UTILS_TEST_UTILS_IPP) +# error "utils/test_utils.hpp can only be included once" +#endif +#define UTILS_TEST_UTILS_IPP + +extern "C" { +#include +} + +#include +#include + +#include + +#include "utils/defs.hpp" +#include "utils/stacktrace.hpp" +#include "utils/text/operations.ipp" + +namespace utils { + + +/// Tries to prevent dumping core if we do not need one on a crash. +/// +/// This is a best-effort operation provided so that tests that will cause +/// a crash do not collect an unnecessary core dump, which can be slow on +/// some systems (e.g. on macOS). +inline void +avoid_coredump_on_crash(void) +{ + struct ::rlimit rl; + rl.rlim_cur = 0; + rl.rlim_max = 0; + if (::setrlimit(RLIMIT_CORE, &rl) == -1) { + std::cerr << "Failed to zero core size limit; may dump core\n"; + } +} + + +inline void abort_without_coredump(void) UTILS_NORETURN; + + +/// Aborts execution and tries to not dump core. +/// +/// The coredump avoidance is a best-effort operation provided so that tests +/// that will cause a crash do not collect an unnecessary core dump, which can +/// be slow on some systems (e.g. on macOS). +inline void +abort_without_coredump(void) +{ + avoid_coredump_on_crash(); + std::abort(); +} + + +/// Skips the test if coredump tests have been disabled by the user. +/// +/// \param tc The calling test. +inline void +require_run_coredump_tests(const atf::tests::tc* tc) +{ + if (tc->has_config_var("run_coredump_tests") && + !text::to_type< bool >(tc->get_config_var("run_coredump_tests"))) { + tc->skip("run_coredump_tests=false; not running test"); + } +} + + +/// Prepares the test so that it can dump core, or skips it otherwise. +/// +/// \param tc The calling test. +inline void +prepare_coredump_test(const atf::tests::tc* tc) +{ + require_run_coredump_tests(tc); + + if (!unlimit_core_size()) { + tc->skip("Cannot unlimit the core file size; check limits manually"); + } +} + + +} // namespace utils diff --git a/utils/text/Kyuafile b/utils/text/Kyuafile new file mode 100644 index 000000000000..e4e870e9c648 --- /dev/null +++ b/utils/text/Kyuafile @@ -0,0 +1,9 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="exceptions_test"} +atf_test_program{name="operations_test"} +atf_test_program{name="regex_test"} +atf_test_program{name="table_test"} +atf_test_program{name="templates_test"} diff --git a/utils/text/Makefile.am.inc b/utils/text/Makefile.am.inc new file mode 100644 index 000000000000..d474ae191bf5 --- /dev/null +++ b/utils/text/Makefile.am.inc @@ -0,0 +1,74 @@ +# Copyright 2012 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +libutils_a_SOURCES += utils/text/exceptions.cpp +libutils_a_SOURCES += utils/text/exceptions.hpp +libutils_a_SOURCES += utils/text/operations.cpp +libutils_a_SOURCES += utils/text/operations.hpp +libutils_a_SOURCES += utils/text/operations.ipp +libutils_a_SOURCES += utils/text/regex.cpp +libutils_a_SOURCES += utils/text/regex.hpp +libutils_a_SOURCES += utils/text/regex_fwd.hpp +libutils_a_SOURCES += utils/text/table.cpp +libutils_a_SOURCES += utils/text/table.hpp +libutils_a_SOURCES += utils/text/table_fwd.hpp +libutils_a_SOURCES += utils/text/templates.cpp +libutils_a_SOURCES += utils/text/templates.hpp +libutils_a_SOURCES += utils/text/templates_fwd.hpp + +if WITH_ATF +tests_utils_textdir = $(pkgtestsdir)/utils/text + +tests_utils_text_DATA = utils/text/Kyuafile +EXTRA_DIST += $(tests_utils_text_DATA) + +tests_utils_text_PROGRAMS = utils/text/exceptions_test +utils_text_exceptions_test_SOURCES = utils/text/exceptions_test.cpp +utils_text_exceptions_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_exceptions_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_text_PROGRAMS += utils/text/operations_test +utils_text_operations_test_SOURCES = utils/text/operations_test.cpp +utils_text_operations_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_operations_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_text_PROGRAMS += utils/text/regex_test +utils_text_regex_test_SOURCES = utils/text/regex_test.cpp +utils_text_regex_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_regex_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_text_PROGRAMS += utils/text/table_test +utils_text_table_test_SOURCES = utils/text/table_test.cpp +utils_text_table_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_table_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) + +tests_utils_text_PROGRAMS += utils/text/templates_test +utils_text_templates_test_SOURCES = utils/text/templates_test.cpp +utils_text_templates_test_CXXFLAGS = $(UTILS_CFLAGS) $(ATF_CXX_CFLAGS) +utils_text_templates_test_LDADD = $(UTILS_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/utils/text/exceptions.cpp b/utils/text/exceptions.cpp new file mode 100644 index 000000000000..1692cfea7edb --- /dev/null +++ b/utils/text/exceptions.cpp @@ -0,0 +1,91 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/exceptions.hpp" + +namespace text = utils::text; + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +text::error::error(const std::string& message) : + std::runtime_error(message) +{ +} + + +/// Destructor for the error. +text::error::~error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +text::regex_error::regex_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +text::regex_error::~regex_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +text::syntax_error::syntax_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +text::syntax_error::~syntax_error(void) throw() +{ +} + + +/// Constructs a new error with a plain-text message. +/// +/// \param message The plain-text error message. +text::value_error::value_error(const std::string& message) : + error(message) +{ +} + + +/// Destructor for the error. +text::value_error::~value_error(void) throw() +{ +} diff --git a/utils/text/exceptions.hpp b/utils/text/exceptions.hpp new file mode 100644 index 000000000000..da0cfd98fb88 --- /dev/null +++ b/utils/text/exceptions.hpp @@ -0,0 +1,77 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/text/exceptions.hpp +/// Exception types raised by the text module. + +#if !defined(UTILS_TEXT_EXCEPTIONS_HPP) +#define UTILS_TEXT_EXCEPTIONS_HPP + +#include + +namespace utils { +namespace text { + + +/// Base exceptions for text errors. +class error : public std::runtime_error { +public: + explicit error(const std::string&); + ~error(void) throw(); +}; + + +/// Exception denoting an error in a regular expression. +class regex_error : public error { +public: + explicit regex_error(const std::string&); + ~regex_error(void) throw(); +}; + + +/// Exception denoting an error while parsing templates. +class syntax_error : public error { +public: + explicit syntax_error(const std::string&); + ~syntax_error(void) throw(); +}; + + +/// Exception denoting an error in a text value format. +class value_error : public error { +public: + explicit value_error(const std::string&); + ~value_error(void) throw(); +}; + + +} // namespace text +} // namespace utils + + +#endif // !defined(UTILS_TEXT_EXCEPTIONS_HPP) diff --git a/utils/text/exceptions_test.cpp b/utils/text/exceptions_test.cpp new file mode 100644 index 000000000000..1d3c3910900a --- /dev/null +++ b/utils/text/exceptions_test.cpp @@ -0,0 +1,76 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/exceptions.hpp" + +#include + +#include + +namespace text = utils::text; + + +ATF_TEST_CASE_WITHOUT_HEAD(error); +ATF_TEST_CASE_BODY(error) +{ + const text::error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(regex_error); +ATF_TEST_CASE_BODY(regex_error) +{ + const text::regex_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(syntax_error); +ATF_TEST_CASE_BODY(syntax_error) +{ + const text::syntax_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(value_error); +ATF_TEST_CASE_BODY(value_error) +{ + const text::value_error e("Some text"); + ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, error); + ATF_ADD_TEST_CASE(tcs, regex_error); + ATF_ADD_TEST_CASE(tcs, syntax_error); + ATF_ADD_TEST_CASE(tcs, value_error); +} diff --git a/utils/text/operations.cpp b/utils/text/operations.cpp new file mode 100644 index 000000000000..5a4345d979c7 --- /dev/null +++ b/utils/text/operations.cpp @@ -0,0 +1,261 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/operations.ipp" + +#include + +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" + +namespace text = utils::text; + + +/// Replaces XML special characters from an input string. +/// +/// The list of XML special characters is specified here: +/// http://www.w3.org/TR/xml11/#charsets +/// +/// \param in The input to quote. +/// +/// \return A quoted string without any XML special characters. +std::string +text::escape_xml(const std::string& in) +{ + std::ostringstream quoted; + + for (std::string::const_iterator it = in.begin(); + it != in.end(); ++it) { + unsigned char c = (unsigned char)*it; + if (c == '"') { + quoted << """; + } else if (c == '&') { + quoted << "&"; + } else if (c == '<') { + quoted << "<"; + } else if (c == '>') { + quoted << ">"; + } else if (c == '\'') { + quoted << "'"; + } else if ((c >= 0x01 && c <= 0x08) || + (c >= 0x0B && c <= 0x0C) || + (c >= 0x0E && c <= 0x1F) || + (c >= 0x7F && c <= 0x84) || + (c >= 0x86 && c <= 0x9F)) { + // for RestrictedChar characters, escape them + // as '&#[decimal ASCII value];' + // so that in the XML file we will see the escaped + // character. + quoted << "&#" << static_cast< std::string::size_type >(*it) + << ";"; + } else { + quoted << *it; + } + } + return quoted.str(); +} + + +/// Surrounds a string with quotes, escaping the quote itself if needed. +/// +/// \param text The string to quote. +/// \param quote The quote character to use. +/// +/// \return The quoted string. +std::string +text::quote(const std::string& text, const char quote) +{ + std::ostringstream quoted; + quoted << quote; + + std::string::size_type start_pos = 0; + std::string::size_type last_pos = text.find(quote); + while (last_pos != std::string::npos) { + quoted << text.substr(start_pos, last_pos - start_pos) << '\\'; + start_pos = last_pos; + last_pos = text.find(quote, start_pos + 1); + } + quoted << text.substr(start_pos); + + quoted << quote; + return quoted.str(); +} + + +/// Fills a paragraph to the specified length. +/// +/// This preserves any sequence of spaces in the input and any possible +/// newlines. Sequences of spaces may be split in half (and thus one space is +/// lost), but the rest of the spaces will be preserved as either trailing or +/// leading spaces. +/// +/// \param input The string to refill. +/// \param target_width The width to refill the paragraph to. +/// +/// \return The refilled paragraph as a sequence of independent lines. +std::vector< std::string > +text::refill(const std::string& input, const std::size_t target_width) +{ + std::vector< std::string > output; + + std::string::size_type start = 0; + while (start < input.length()) { + std::string::size_type width; + if (start + target_width >= input.length()) + width = input.length() - start; + else { + if (input[start + target_width] == ' ') { + width = target_width; + } else { + const std::string::size_type pos = input.find_last_of( + " ", start + target_width - 1); + if (pos == std::string::npos || pos < start + 1) { + width = input.find_first_of(" ", start + target_width); + if (width == std::string::npos) + width = input.length() - start; + else + width -= start; + } else { + width = pos - start; + } + } + } + INV(width != std::string::npos); + INV(start + width <= input.length()); + INV(input[start + width] == ' ' || input[start + width] == '\0'); + output.push_back(input.substr(start, width)); + + start += width + 1; + } + + if (input.empty()) { + INV(output.empty()); + output.push_back(""); + } + + return output; +} + + +/// Fills a paragraph to the specified length. +/// +/// See the documentation for refill() for additional details. +/// +/// \param input The string to refill. +/// \param target_width The width to refill the paragraph to. +/// +/// \return The refilled paragraph as a string with embedded newlines. +std::string +text::refill_as_string(const std::string& input, const std::size_t target_width) +{ + return join(refill(input, target_width), "\n"); +} + + +/// Replaces all occurrences of a substring in a string. +/// +/// \param input The string in which to perform the replacement. +/// \param search The pattern to be replaced. +/// \param replacement The substring to replace search with. +/// +/// \return A copy of input with the replacements performed. +std::string +text::replace_all(const std::string& input, const std::string& search, + const std::string& replacement) +{ + std::string output; + + std::string::size_type pos, lastpos = 0; + while ((pos = input.find(search, lastpos)) != std::string::npos) { + output += input.substr(lastpos, pos - lastpos); + output += replacement; + lastpos = pos + search.length(); + } + output += input.substr(lastpos); + + return output; +} + + +/// Splits a string into different components. +/// +/// \param str The string to split. +/// \param delimiter The separator to use to split the words. +/// +/// \return The different words in the input string as split by the provided +/// delimiter. +std::vector< std::string > +text::split(const std::string& str, const char delimiter) +{ + std::vector< std::string > words; + if (!str.empty()) { + std::string::size_type pos = str.find(delimiter); + words.push_back(str.substr(0, pos)); + while (pos != std::string::npos) { + ++pos; + const std::string::size_type next = str.find(delimiter, pos); + words.push_back(str.substr(pos, next - pos)); + pos = next; + } + } + return words; +} + + +/// Converts a string to a boolean. +/// +/// \param str The string to convert. +/// +/// \return The converted string, if the input string was valid. +/// +/// \throw std::value_error If the input string does not represent a valid +/// boolean value. +template<> +bool +text::to_type(const std::string& str) +{ + if (str == "true") + return true; + else if (str == "false") + return false; + else + throw value_error(F("Invalid boolean value '%s'") % str); +} + + +/// Identity function for to_type, for genericity purposes. +/// +/// \param str The string to convert. +/// +/// \return The input string. +template<> +std::string +text::to_type(const std::string& str) +{ + return str; +} diff --git a/utils/text/operations.hpp b/utils/text/operations.hpp new file mode 100644 index 000000000000..6d15be553b06 --- /dev/null +++ b/utils/text/operations.hpp @@ -0,0 +1,68 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/text/operations.hpp +/// Utilities to manipulate strings. + +#if !defined(UTILS_TEXT_OPERATIONS_HPP) +#define UTILS_TEXT_OPERATIONS_HPP + +#include +#include +#include + +namespace utils { +namespace text { + + +std::string escape_xml(const std::string&); +std::string quote(const std::string&, const char); + + +std::vector< std::string > refill(const std::string&, const std::size_t); +std::string refill_as_string(const std::string&, const std::size_t); + +std::string replace_all(const std::string&, const std::string&, + const std::string&); + +template< typename Collection > +std::string join(const Collection&, const std::string&); +std::vector< std::string > split(const std::string&, const char); + +template< typename Type > +Type to_type(const std::string&); +template<> +bool to_type(const std::string&); +template<> +std::string to_type(const std::string&); + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_OPERATIONS_HPP) diff --git a/utils/text/operations.ipp b/utils/text/operations.ipp new file mode 100644 index 000000000000..511cd6840a08 --- /dev/null +++ b/utils/text/operations.ipp @@ -0,0 +1,91 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#if !defined(UTILS_TEXT_OPERATIONS_IPP) +#define UTILS_TEXT_OPERATIONS_IPP + +#include "utils/text/operations.hpp" + +#include + +#include "utils/text/exceptions.hpp" + + +/// Concatenates a collection of strings into a single string. +/// +/// \param strings The collection of strings to concatenate. If the collection +/// is unordered, the ordering in the output is undefined. +/// \param delimiter The delimiter to use to separate the strings. +/// +/// \return The concatenated strings. +template< typename Collection > +std::string +utils::text::join(const Collection& strings, const std::string& delimiter) +{ + std::ostringstream output; + if (strings.size() > 1) { + for (typename Collection::const_iterator iter = strings.begin(); + iter != --strings.end(); ++iter) + output << (*iter) << delimiter; + } + if (strings.size() > 0) + output << *(--strings.end()); + return output.str(); +} + + +/// Converts a string to a native type. +/// +/// \tparam Type The type to convert the string to. An input stream operator +/// must exist to extract such a type from an std::istream. +/// \param str The string to convert. +/// +/// \return The converted string, if the input string was valid. +/// +/// \throw std::value_error If the input string does not represent a valid +/// target type. This exception does not include any details, so the caller +/// must take care to re-raise it with appropriate details. +template< typename Type > +Type +utils::text::to_type(const std::string& str) +{ + if (str.empty()) + throw text::value_error("Empty string"); + if (str[0] == ' ') + throw text::value_error("Invalid value"); + + std::istringstream input(str); + Type value; + input >> value; + if (!input.eof() || input.bad() || input.fail()) + throw text::value_error("Invalid value"); + return value; +} + + +#endif // !defined(UTILS_TEXT_OPERATIONS_IPP) diff --git a/utils/text/operations_test.cpp b/utils/text/operations_test.cpp new file mode 100644 index 000000000000..2d5ab36c9090 --- /dev/null +++ b/utils/text/operations_test.cpp @@ -0,0 +1,435 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/operations.ipp" + +#include +#include +#include +#include + +#include + +#include "utils/text/exceptions.hpp" + +namespace text = utils::text; + + +namespace { + + +/// Tests text::refill() on an input string with a range of widths. +/// +/// \param expected The expected refilled paragraph. +/// \param input The input paragraph to be refilled. +/// \param first_width The first width to validate. +/// \param last_width The last width to validate (inclusive). +static void +refill_test(const char* expected, const char* input, + const std::size_t first_width, const std::size_t last_width) +{ + for (std::size_t width = first_width; width <= last_width; ++width) { + const std::vector< std::string > lines = text::split(expected, '\n'); + std::cout << "Breaking at width " << width << '\n'; + ATF_REQUIRE_EQ(expected, text::refill_as_string(input, width)); + ATF_REQUIRE(lines == text::refill(input, width)); + } +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(escape_xml__empty); +ATF_TEST_CASE_BODY(escape_xml__empty) +{ + ATF_REQUIRE_EQ("", text::escape_xml("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(escape_xml__no_escaping); +ATF_TEST_CASE_BODY(escape_xml__no_escaping) +{ + ATF_REQUIRE_EQ("a", text::escape_xml("a")); + ATF_REQUIRE_EQ("Some text!", text::escape_xml("Some text!")); + ATF_REQUIRE_EQ("\n\t\r", text::escape_xml("\n\t\r")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(escape_xml__some_escaping); +ATF_TEST_CASE_BODY(escape_xml__some_escaping) +{ + ATF_REQUIRE_EQ("'", text::escape_xml("'")); + + ATF_REQUIRE_EQ("foo "bar& <tag> yay' baz", + text::escape_xml("foo \"bar& yay' baz")); + + ATF_REQUIRE_EQ(""&<>'", text::escape_xml("\"&<>'")); + ATF_REQUIRE_EQ("&&&", text::escape_xml("&&&")); + ATF_REQUIRE_EQ("&#8;&#11;", text::escape_xml("\b\v")); + ATF_REQUIRE_EQ("\t&#127;BAR&", text::escape_xml("\t\x7f""BAR&")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(quote__empty); +ATF_TEST_CASE_BODY(quote__empty) +{ + ATF_REQUIRE_EQ("''", text::quote("", '\'')); + ATF_REQUIRE_EQ("##", text::quote("", '#')); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(quote__no_escaping); +ATF_TEST_CASE_BODY(quote__no_escaping) +{ + ATF_REQUIRE_EQ("'Some text\"'", text::quote("Some text\"", '\'')); + ATF_REQUIRE_EQ("#Another'string#", text::quote("Another'string", '#')); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(quote__some_escaping); +ATF_TEST_CASE_BODY(quote__some_escaping) +{ + ATF_REQUIRE_EQ("'Some\\'text'", text::quote("Some'text", '\'')); + ATF_REQUIRE_EQ("#Some\\#text#", text::quote("Some#text", '#')); + + ATF_REQUIRE_EQ("'More than one\\' quote\\''", + text::quote("More than one' quote'", '\'')); + ATF_REQUIRE_EQ("'Multiple quotes \\'\\'\\' together'", + text::quote("Multiple quotes ''' together", '\'')); + + ATF_REQUIRE_EQ("'\\'escape at the beginning'", + text::quote("'escape at the beginning", '\'')); + ATF_REQUIRE_EQ("'escape at the end\\''", + text::quote("escape at the end'", '\'')); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__empty); +ATF_TEST_CASE_BODY(refill__empty) +{ + ATF_REQUIRE_EQ(1, text::refill("", 0).size()); + ATF_REQUIRE(text::refill("", 0)[0].empty()); + ATF_REQUIRE_EQ("", text::refill_as_string("", 0)); + + ATF_REQUIRE_EQ(1, text::refill("", 10).size()); + ATF_REQUIRE(text::refill("", 10)[0].empty()); + ATF_REQUIRE_EQ("", text::refill_as_string("", 10)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__no_changes); +ATF_TEST_CASE_BODY(refill__no_changes) +{ + std::vector< std::string > exp_lines; + exp_lines.push_back("foo bar\nbaz"); + + ATF_REQUIRE(exp_lines == text::refill("foo bar\nbaz", 12)); + ATF_REQUIRE_EQ("foo bar\nbaz", text::refill_as_string("foo bar\nbaz", 12)); + + ATF_REQUIRE(exp_lines == text::refill("foo bar\nbaz", 18)); + ATF_REQUIRE_EQ("foo bar\nbaz", text::refill_as_string("foo bar\nbaz", 80)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__break_one); +ATF_TEST_CASE_BODY(refill__break_one) +{ + refill_test("only break the\nfirst line", "only break the first line", + 14, 19); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__break_one__not_first_word); +ATF_TEST_CASE_BODY(refill__break_one__not_first_word) +{ + refill_test("first-long-word\nother\nwords", "first-long-word other words", + 6, 10); + refill_test("first-long-word\nother words", "first-long-word other words", + 11, 20); + refill_test("first-long-word other\nwords", "first-long-word other words", + 21, 26); + refill_test("first-long-word other words", "first-long-word other words", + 27, 28); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__break_many); +ATF_TEST_CASE_BODY(refill__break_many) +{ + refill_test("this is a long\nparagraph to be\nsplit into\npieces", + "this is a long paragraph to be split into pieces", + 15, 15); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__cannot_break); +ATF_TEST_CASE_BODY(refill__cannot_break) +{ + refill_test("this-is-a-long-string", "this-is-a-long-string", 5, 5); + + refill_test("this is\na-string-with-long-words", + "this is a-string-with-long-words", 10, 10); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(refill__preserve_whitespace); +ATF_TEST_CASE_BODY(refill__preserve_whitespace) +{ + refill_test("foo bar baz ", "foo bar baz ", 80, 80); + refill_test("foo \n bar", "foo bar", 5, 5); + + std::vector< std::string > exp_lines; + exp_lines.push_back("foo \n"); + exp_lines.push_back(" bar"); + ATF_REQUIRE(exp_lines == text::refill("foo \n bar", 5)); + ATF_REQUIRE_EQ("foo \n\n bar", text::refill_as_string("foo \n bar", 5)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__empty); +ATF_TEST_CASE_BODY(join__empty) +{ + std::vector< std::string > lines; + ATF_REQUIRE_EQ("", text::join(lines, " ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__one); +ATF_TEST_CASE_BODY(join__one) +{ + std::vector< std::string > lines; + lines.push_back("first line"); + ATF_REQUIRE_EQ("first line", text::join(lines, "*")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__several); +ATF_TEST_CASE_BODY(join__several) +{ + std::vector< std::string > lines; + lines.push_back("first abc"); + lines.push_back("second"); + lines.push_back("and last line"); + ATF_REQUIRE_EQ("first abc second and last line", text::join(lines, " ")); + ATF_REQUIRE_EQ("first abc***second***and last line", + text::join(lines, "***")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(join__unordered); +ATF_TEST_CASE_BODY(join__unordered) +{ + std::set< std::string > lines; + lines.insert("first"); + lines.insert("second"); + const std::string joined = text::join(lines, " "); + ATF_REQUIRE(joined == "first second" || joined == "second first"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(split__empty); +ATF_TEST_CASE_BODY(split__empty) +{ + std::vector< std::string > words = text::split("", ' '); + std::vector< std::string > exp_words; + ATF_REQUIRE(exp_words == words); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(split__one); +ATF_TEST_CASE_BODY(split__one) +{ + std::vector< std::string > words = text::split("foo", ' '); + std::vector< std::string > exp_words; + exp_words.push_back("foo"); + ATF_REQUIRE(exp_words == words); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(split__several__simple); +ATF_TEST_CASE_BODY(split__several__simple) +{ + std::vector< std::string > words = text::split("foo bar baz", ' '); + std::vector< std::string > exp_words; + exp_words.push_back("foo"); + exp_words.push_back("bar"); + exp_words.push_back("baz"); + ATF_REQUIRE(exp_words == words); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(split__several__delimiters); +ATF_TEST_CASE_BODY(split__several__delimiters) +{ + std::vector< std::string > words = text::split("XfooXXbarXXXbazXX", 'X'); + std::vector< std::string > exp_words; + exp_words.push_back(""); + exp_words.push_back("foo"); + exp_words.push_back(""); + exp_words.push_back("bar"); + exp_words.push_back(""); + exp_words.push_back(""); + exp_words.push_back("baz"); + exp_words.push_back(""); + exp_words.push_back(""); + ATF_REQUIRE(exp_words == words); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(replace_all__empty); +ATF_TEST_CASE_BODY(replace_all__empty) +{ + ATF_REQUIRE_EQ("", text::replace_all("", "search", "replacement")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(replace_all__none); +ATF_TEST_CASE_BODY(replace_all__none) +{ + ATF_REQUIRE_EQ("string without matches", + text::replace_all("string without matches", + "WITHOUT", "replacement")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(replace_all__one); +ATF_TEST_CASE_BODY(replace_all__one) +{ + ATF_REQUIRE_EQ("string replacement matches", + text::replace_all("string without matches", + "without", "replacement")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(replace_all__several); +ATF_TEST_CASE_BODY(replace_all__several) +{ + ATF_REQUIRE_EQ("OO fOO bar OOf baz OO", + text::replace_all("oo foo bar oof baz oo", + "oo", "OO")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__ok__bool); +ATF_TEST_CASE_BODY(to_type__ok__bool) +{ + ATF_REQUIRE( text::to_type< bool >("true")); + ATF_REQUIRE(!text::to_type< bool >("false")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__ok__numerical); +ATF_TEST_CASE_BODY(to_type__ok__numerical) +{ + ATF_REQUIRE_EQ(12, text::to_type< int >("12")); + ATF_REQUIRE_EQ(18745, text::to_type< int >("18745")); + ATF_REQUIRE_EQ(-12345, text::to_type< int >("-12345")); + + ATF_REQUIRE_EQ(12.0, text::to_type< double >("12")); + ATF_REQUIRE_EQ(12.5, text::to_type< double >("12.5")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__ok__string); +ATF_TEST_CASE_BODY(to_type__ok__string) +{ + // While this seems redundant, having this particular specialization that + // does nothing allows callers to delegate work to to_type without worrying + // about the particular type being converted. + ATF_REQUIRE_EQ("", text::to_type< std::string >("")); + ATF_REQUIRE_EQ(" abcd ", text::to_type< std::string >(" abcd ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__empty); +ATF_TEST_CASE_BODY(to_type__empty) +{ + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >("")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__invalid__bool); +ATF_TEST_CASE_BODY(to_type__invalid__bool) +{ + ATF_REQUIRE_THROW(text::value_error, text::to_type< bool >("")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< bool >("true ")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< bool >("foo")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(to_type__invalid__numerical); +ATF_TEST_CASE_BODY(to_type__invalid__numerical) +{ + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >(" 3")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >("3 ")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >("3a")); + ATF_REQUIRE_THROW(text::value_error, text::to_type< int >("a3")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, escape_xml__empty); + ATF_ADD_TEST_CASE(tcs, escape_xml__no_escaping); + ATF_ADD_TEST_CASE(tcs, escape_xml__some_escaping); + + ATF_ADD_TEST_CASE(tcs, quote__empty); + ATF_ADD_TEST_CASE(tcs, quote__no_escaping); + ATF_ADD_TEST_CASE(tcs, quote__some_escaping); + + ATF_ADD_TEST_CASE(tcs, refill__empty); + ATF_ADD_TEST_CASE(tcs, refill__no_changes); + ATF_ADD_TEST_CASE(tcs, refill__break_one); + ATF_ADD_TEST_CASE(tcs, refill__break_one__not_first_word); + ATF_ADD_TEST_CASE(tcs, refill__break_many); + ATF_ADD_TEST_CASE(tcs, refill__cannot_break); + ATF_ADD_TEST_CASE(tcs, refill__preserve_whitespace); + + ATF_ADD_TEST_CASE(tcs, join__empty); + ATF_ADD_TEST_CASE(tcs, join__one); + ATF_ADD_TEST_CASE(tcs, join__several); + ATF_ADD_TEST_CASE(tcs, join__unordered); + + ATF_ADD_TEST_CASE(tcs, split__empty); + ATF_ADD_TEST_CASE(tcs, split__one); + ATF_ADD_TEST_CASE(tcs, split__several__simple); + ATF_ADD_TEST_CASE(tcs, split__several__delimiters); + + ATF_ADD_TEST_CASE(tcs, replace_all__empty); + ATF_ADD_TEST_CASE(tcs, replace_all__none); + ATF_ADD_TEST_CASE(tcs, replace_all__one); + ATF_ADD_TEST_CASE(tcs, replace_all__several); + + ATF_ADD_TEST_CASE(tcs, to_type__ok__bool); + ATF_ADD_TEST_CASE(tcs, to_type__ok__numerical); + ATF_ADD_TEST_CASE(tcs, to_type__ok__string); + ATF_ADD_TEST_CASE(tcs, to_type__empty); + ATF_ADD_TEST_CASE(tcs, to_type__invalid__bool); + ATF_ADD_TEST_CASE(tcs, to_type__invalid__numerical); +} diff --git a/utils/text/regex.cpp b/utils/text/regex.cpp new file mode 100644 index 000000000000..b078ba88f6b4 --- /dev/null +++ b/utils/text/regex.cpp @@ -0,0 +1,302 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/regex.hpp" + +extern "C" { +#include + +#include +} + +#include "utils/auto_array.ipp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" + +namespace text = utils::text; + + +namespace { + + +static void throw_regex_error(const int, const ::regex_t*, const std::string&) + UTILS_NORETURN; + + +/// Constructs and raises a regex_error. +/// +/// \param error The error code returned by regcomp(3) or regexec(3). +/// \param preg The native regex object that caused this error. +/// \param prefix Error message prefix string. +/// +/// \throw regex_error The constructed exception. +static void +throw_regex_error(const int error, const ::regex_t* preg, + const std::string& prefix) +{ + char buffer[1024]; + + // TODO(jmmv): Would be nice to handle the case where the message does + // not fit in the temporary buffer. + (void)::regerror(error, preg, buffer, sizeof(buffer)); + + throw text::regex_error(F("%s: %s") % prefix % buffer); +} + + +} // anonymous namespace + + +/// Internal implementation for regex_matches. +struct utils::text::regex_matches::impl : utils::noncopyable { + /// String on which we are matching. + /// + /// In theory, we could take a reference here instead of a copy, and make + /// it a requirement for the caller to ensure that the lifecycle of the + /// input string outlasts the lifecycle of the regex_matches. However, that + /// contract is very easy to break with hardcoded strings (as we do in + /// tests). Just go for the safer case here. + const std::string _string; + + /// Maximum number of matching groups we expect, including the full match. + /// + /// In other words, this is the size of the _matches array. + const std::size_t _nmatches; + + /// Native regular expression match representation. + utils::auto_array< ::regmatch_t > _matches; + + /// Constructor. + /// + /// This executes the regex on the given string and sets up the internal + /// class state based on the results. + /// + /// \param preg The native regex object. + /// \param str The string on which to execute the regex. + /// \param ngroups Number of capture groups in the regex. This is an upper + /// bound and may be greater than the actual matches. + /// + /// \throw regex_error If the call to regexec(3) fails. + impl(const ::regex_t* preg, const std::string& str, + const std::size_t ngroups) : + _string(str), + _nmatches(ngroups + 1), + _matches(new ::regmatch_t[_nmatches]) + { + const int error = ::regexec(preg, _string.c_str(), _nmatches, + _matches.get(), 0); + if (error == REG_NOMATCH) { + _matches.reset(NULL); + } else if (error != 0) { + throw_regex_error(error, preg, + F("regexec on '%s' failed") % _string); + } + } + + /// Destructor. + ~impl(void) + { + } +}; + + +/// Constructor. +/// +/// \param pimpl Constructed implementation of the object. +text::regex_matches::regex_matches(std::shared_ptr< impl > pimpl) : + _pimpl(pimpl) +{ +} + + +/// Destructor. +text::regex_matches::~regex_matches(void) +{ +} + + +/// Returns the number of matches in this object. +/// +/// Note that this does not correspond to the number of groups provided at +/// construction time. The returned value here accounts for only the returned +/// valid matches. +/// +/// \return Number of matches, including the full match. +std::size_t +text::regex_matches::count(void) const +{ + std::size_t total = 0; + if (_pimpl->_matches.get() != NULL) { + for (std::size_t i = 0; i < _pimpl->_nmatches; ++i) { + if (_pimpl->_matches[i].rm_so != -1) + ++total; + } + INV(total <= _pimpl->_nmatches); + } + return total; +} + + +/// Gets a match. +/// +/// \param index Number of the match to get. Index 0 always contains the match +/// of the whole regex. +/// +/// \pre There regex must have matched the input string. +/// \pre index must be lower than count(). +/// +/// \return The textual match. +std::string +text::regex_matches::get(const std::size_t index) const +{ + PRE(*this); + PRE(index < count()); + + const ::regmatch_t* match = &_pimpl->_matches[index]; + + return std::string(_pimpl->_string.c_str() + match->rm_so, + match->rm_eo - match->rm_so); +} + + +/// Checks if there are any matches. +/// +/// \return True if the object contains one or more matches; false otherwise. +text::regex_matches::operator bool(void) const +{ + return _pimpl->_matches.get() != NULL; +} + + +/// Internal implementation for regex. +struct utils::text::regex::impl : utils::noncopyable { + /// Native regular expression representation. + ::regex_t _preg; + + /// Number of capture groups in the regular expression. This is an upper + /// bound and does NOT include the default full string match. + std::size_t _ngroups; + + /// Constructor. + /// + /// This compiles the given regular expression. + /// + /// \param regex_ The regular expression to compile. + /// \param ngroups Number of capture groups in the regular expression. This + /// is an upper bound and does NOT include the default full string + /// match. + /// \param ignore_case Whether to ignore case during matching. + /// + /// \throw regex_error If the call to regcomp(3) fails. + impl(const std::string& regex_, const std::size_t ngroups, + const bool ignore_case) : + _ngroups(ngroups) + { + const int flags = REG_EXTENDED | (ignore_case ? REG_ICASE : 0); + const int error = ::regcomp(&_preg, regex_.c_str(), flags); + if (error != 0) + throw_regex_error(error, &_preg, F("regcomp on '%s' failed") + % regex_); + } + + /// Destructor. + ~impl(void) + { + ::regfree(&_preg); + } +}; + + +/// Constructor. +/// +/// \param pimpl Constructed implementation of the object. +text::regex::regex(std::shared_ptr< impl > pimpl) : _pimpl(pimpl) +{ +} + + +/// Destructor. +text::regex::~regex(void) +{ +} + + +/// Compiles a new regular expression. +/// +/// \param regex_ The regular expression to compile. +/// \param ngroups Number of capture groups in the regular expression. This is +/// an upper bound and does NOT include the default full string match. +/// \param ignore_case Whether to ignore case during matching. +/// +/// \return A new regular expression, ready to match strings. +/// +/// \throw regex_error If the regular expression is invalid and cannot be +/// compiled. +text::regex +text::regex::compile(const std::string& regex_, const std::size_t ngroups, + const bool ignore_case) +{ + return regex(std::shared_ptr< impl >(new impl(regex_, ngroups, + ignore_case))); +} + + +/// Matches the regular expression against a string. +/// +/// \param str String to match the regular expression against. +/// +/// \return A new regex_matches object with the results of the match. +text::regex_matches +text::regex::match(const std::string& str) const +{ + std::shared_ptr< regex_matches::impl > pimpl(new regex_matches::impl( + &_pimpl->_preg, str, _pimpl->_ngroups)); + return regex_matches(pimpl); +} + + +/// Compiles and matches a regular expression once. +/// +/// This is syntactic sugar to simplify the instantiation of a new regex object +/// and its subsequent match on a string. +/// +/// \param regex_ The regular expression to compile and match. +/// \param str String to match the regular expression against. +/// \param ngroups Number of capture groups in the regular expression. +/// \param ignore_case Whether to ignore case during matching. +/// +/// \return A new regex_matches object with the results of the match. +text::regex_matches +text::match_regex(const std::string& regex_, const std::string& str, + const std::size_t ngroups, const bool ignore_case) +{ + return regex::compile(regex_, ngroups, ignore_case).match(str); +} diff --git a/utils/text/regex.hpp b/utils/text/regex.hpp new file mode 100644 index 000000000000..b3d20c246735 --- /dev/null +++ b/utils/text/regex.hpp @@ -0,0 +1,92 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/text/regex.hpp +/// Utilities to build and match regular expressions. + +#if !defined(UTILS_TEXT_REGEX_HPP) +#define UTILS_TEXT_REGEX_HPP + +#include "utils/text/regex_fwd.hpp" + +#include +#include + + +namespace utils { +namespace text { + + +/// Container for regex match results. +class regex_matches { + struct impl; + + /// Pointer to shared implementation. + std::shared_ptr< impl > _pimpl; + + friend class regex; + regex_matches(std::shared_ptr< impl >); + +public: + ~regex_matches(void); + + std::size_t count(void) const; + std::string get(const std::size_t) const; + + operator bool(void) const; +}; + + +/// Regular expression compiler and executor. +/// +/// All regular expressions handled by this class are "extended". +class regex { + struct impl; + + /// Pointer to shared implementation. + std::shared_ptr< impl > _pimpl; + + regex(std::shared_ptr< impl >); + +public: + ~regex(void); + + static regex compile(const std::string&, const std::size_t, + const bool = false); + regex_matches match(const std::string&) const; +}; + + +regex_matches match_regex(const std::string&, const std::string&, + const std::size_t, const bool = false); + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_REGEX_HPP) diff --git a/utils/text/regex_fwd.hpp b/utils/text/regex_fwd.hpp new file mode 100644 index 000000000000..e9010324c10d --- /dev/null +++ b/utils/text/regex_fwd.hpp @@ -0,0 +1,46 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/text/regex_fwd.hpp +/// Forward declarations for utils/text/regex.hpp + +#if !defined(UTILS_TEXT_REGEX_FWD_HPP) +#define UTILS_TEXT_REGEX_FWD_HPP + +namespace utils { +namespace text { + + +class regex_matches; +class regex; + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_REGEX_FWD_HPP) diff --git a/utils/text/regex_test.cpp b/utils/text/regex_test.cpp new file mode 100644 index 000000000000..7ea5ee485aad --- /dev/null +++ b/utils/text/regex_test.cpp @@ -0,0 +1,177 @@ +// Copyright 2014 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/regex.hpp" + +#include + +#include "utils/text/exceptions.hpp" + +namespace text = utils::text; + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__no_matches); +ATF_TEST_CASE_BODY(integration__no_matches) +{ + const text::regex_matches matches = text::match_regex( + "foo.*bar", "this is a string without the searched text", 0); + ATF_REQUIRE(!matches); + ATF_REQUIRE_EQ(0, matches.count()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__no_capture_groups); +ATF_TEST_CASE_BODY(integration__no_capture_groups) +{ + const text::regex_matches matches = text::match_regex( + "foo.*bar", "this is a string with foo and bar embedded in it", 0); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(1, matches.count()); + ATF_REQUIRE_EQ("foo and bar", matches.get(0)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__one_capture_group); +ATF_TEST_CASE_BODY(integration__one_capture_group) +{ + const text::regex_matches matches = text::match_regex( + "^([^ ]*) ", "the string", 1); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(2, matches.count()); + ATF_REQUIRE_EQ("the ", matches.get(0)); + ATF_REQUIRE_EQ("the", matches.get(1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__many_capture_groups); +ATF_TEST_CASE_BODY(integration__many_capture_groups) +{ + const text::regex_matches matches = text::match_regex( + "is ([^ ]*) ([a-z]*) to", "this is another string to parse", 2); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(3, matches.count()); + ATF_REQUIRE_EQ("is another string to", matches.get(0)); + ATF_REQUIRE_EQ("another", matches.get(1)); + ATF_REQUIRE_EQ("string", matches.get(2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__capture_groups_underspecified); +ATF_TEST_CASE_BODY(integration__capture_groups_underspecified) +{ + const text::regex_matches matches = text::match_regex( + "is ([^ ]*) ([a-z]*) to", "this is another string to parse", 1); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(2, matches.count()); + ATF_REQUIRE_EQ("is another string to", matches.get(0)); + ATF_REQUIRE_EQ("another", matches.get(1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__capture_groups_overspecified); +ATF_TEST_CASE_BODY(integration__capture_groups_overspecified) +{ + const text::regex_matches matches = text::match_regex( + "is ([^ ]*) ([a-z]*) to", "this is another string to parse", 10); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(3, matches.count()); + ATF_REQUIRE_EQ("is another string to", matches.get(0)); + ATF_REQUIRE_EQ("another", matches.get(1)); + ATF_REQUIRE_EQ("string", matches.get(2)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__reuse_regex_in_multiple_matches); +ATF_TEST_CASE_BODY(integration__reuse_regex_in_multiple_matches) +{ + const text::regex regex = text::regex::compile("number is ([0-9]+)", 1); + + { + const text::regex_matches matches = regex.match("my number is 581."); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(2, matches.count()); + ATF_REQUIRE_EQ("number is 581", matches.get(0)); + ATF_REQUIRE_EQ("581", matches.get(1)); + } + + { + const text::regex_matches matches = regex.match("your number is 6"); + ATF_REQUIRE(matches); + ATF_REQUIRE_EQ(2, matches.count()); + ATF_REQUIRE_EQ("number is 6", matches.get(0)); + ATF_REQUIRE_EQ("6", matches.get(1)); + } +} + + +ATF_TEST_CASE_WITHOUT_HEAD(integration__ignore_case); +ATF_TEST_CASE_BODY(integration__ignore_case) +{ + const text::regex regex1 = text::regex::compile("foo", 0, false); + ATF_REQUIRE(!regex1.match("bar Foo bar")); + ATF_REQUIRE(!regex1.match("bar foO bar")); + ATF_REQUIRE(!regex1.match("bar FOO bar")); + + ATF_REQUIRE(!text::match_regex("foo", "bar Foo bar", 0, false)); + ATF_REQUIRE(!text::match_regex("foo", "bar foO bar", 0, false)); + ATF_REQUIRE(!text::match_regex("foo", "bar FOO bar", 0, false)); + + const text::regex regex2 = text::regex::compile("foo", 0, true); + ATF_REQUIRE( regex2.match("bar foo bar")); + ATF_REQUIRE( regex2.match("bar Foo bar")); + ATF_REQUIRE( regex2.match("bar foO bar")); + ATF_REQUIRE( regex2.match("bar FOO bar")); + + ATF_REQUIRE( text::match_regex("foo", "bar foo bar", 0, true)); + ATF_REQUIRE( text::match_regex("foo", "bar Foo bar", 0, true)); + ATF_REQUIRE( text::match_regex("foo", "bar foO bar", 0, true)); + ATF_REQUIRE( text::match_regex("foo", "bar FOO bar", 0, true)); +} + +ATF_TEST_CASE_WITHOUT_HEAD(integration__invalid_regex); +ATF_TEST_CASE_BODY(integration__invalid_regex) +{ + ATF_REQUIRE_THROW(text::regex_error, + text::regex::compile("this is (unbalanced", 0)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + // regex and regex_matches are so coupled that it makes no sense to test + // them independently. Just validate their integration. + ATF_ADD_TEST_CASE(tcs, integration__no_matches); + ATF_ADD_TEST_CASE(tcs, integration__no_capture_groups); + ATF_ADD_TEST_CASE(tcs, integration__one_capture_group); + ATF_ADD_TEST_CASE(tcs, integration__many_capture_groups); + ATF_ADD_TEST_CASE(tcs, integration__capture_groups_underspecified); + ATF_ADD_TEST_CASE(tcs, integration__capture_groups_overspecified); + ATF_ADD_TEST_CASE(tcs, integration__reuse_regex_in_multiple_matches); + ATF_ADD_TEST_CASE(tcs, integration__ignore_case); + ATF_ADD_TEST_CASE(tcs, integration__invalid_regex); +} diff --git a/utils/text/table.cpp b/utils/text/table.cpp new file mode 100644 index 000000000000..4a2c72f8053f --- /dev/null +++ b/utils/text/table.cpp @@ -0,0 +1,428 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/table.hpp" + +#include +#include +#include +#include + +#include "utils/sanity.hpp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +namespace { + + +/// Applies user overrides to the column widths of a table. +/// +/// \param table The table from which to calculate the column widths. +/// \param user_widths The column widths provided by the user. This vector must +/// have less or the same number of elements as the columns of the table. +/// Values of width_auto are ignored; any other explicit values are copied +/// to the output widths vector, including width_refill. +/// +/// \return A vector with the widths of the columns of the input table with any +/// user overrides applied. +static text::widths_vector +override_column_widths(const text::table& table, + const text::widths_vector& user_widths) +{ + PRE(user_widths.size() <= table.ncolumns()); + text::widths_vector widths = table.column_widths(); + + // Override the actual width of the columns based on user-specified widths. + for (text::widths_vector::size_type i = 0; i < user_widths.size(); ++i) { + const text::widths_vector::value_type& user_width = user_widths[i]; + if (user_width != text::table_formatter::width_auto) { + PRE_MSG(user_width == text::table_formatter::width_refill || + user_width >= widths[i], + "User-provided column widths must be larger than the " + "column contents (except for the width_refill column)"); + widths[i] = user_width; + } + } + + return widths; +} + + +/// Locates the refill column, if any. +/// +/// \param widths The widths of the columns as returned by +/// override_column_widths(). Note that one of the columns may or may not +/// be width_refill, which is the column we are looking for. +/// +/// \return The index of the refill column with a width_refill width if any, or +/// otherwise the index of the last column (which is the default refill column). +static text::widths_vector::size_type +find_refill_column(const text::widths_vector& widths) +{ + text::widths_vector::size_type i = 0; + for (; i < widths.size(); ++i) { + if (widths[i] == text::table_formatter::width_refill) + return i; + } + return i - 1; +} + + +/// Pads the widths of the table to fit within a maximum width. +/// +/// On output, a column of the widths vector is truncated to a shorter length +/// than its current value, if the total width of the table would exceed the +/// maximum table width. +/// +/// \param [in,out] widths The widths of the columns as returned by +/// override_column_widths(). One of these columns should have a value of +/// width_refill; if not, a default column is refilled. +/// \param user_max_width The target width of the table; must not be zero. +/// \param column_padding The padding between the cells, if any. The target +/// width should be larger than the padding times the number of columns; if +/// that is not the case, we attempt a readjustment here. +static void +refill_widths(text::widths_vector& widths, + const text::widths_vector::value_type user_max_width, + const std::size_t column_padding) +{ + PRE(user_max_width != 0); + + // widths.size() is a proxy for the number of columns of the table. + const std::size_t total_padding = column_padding * (widths.size() - 1); + const text::widths_vector::value_type max_width = std::max( + user_max_width, total_padding) - total_padding; + + const text::widths_vector::size_type refill_column = + find_refill_column(widths); + INV(refill_column < widths.size()); + + text::widths_vector::value_type width = 0; + for (text::widths_vector::size_type i = 0; i < widths.size(); ++i) { + if (i != refill_column) + width += widths[i]; + } + widths[refill_column] = max_width - width; +} + + +/// Pads an input text to a specified width with spaces. +/// +/// \param input The text to add padding to (may be empty). +/// \param length The desired length of the output. +/// \param is_last Whether the text being processed belongs to the last column +/// of a row or not. Values in the last column should not be padded to +/// prevent trailing whitespace on the screen (which affects copy/pasting +/// for example). +/// +/// \return The padded cell. If the input string is longer than the desired +/// length, the input string is returned verbatim. The padded table won't be +/// correct, but we don't expect this to be a common case to worry about. +static std::string +pad_cell(const std::string& input, const std::size_t length, const bool is_last) +{ + if (is_last) + return input; + else { + if (input.length() < length) + return input + std::string(length - input.length(), ' '); + else + return input; + } +} + + +/// Refills a cell and adds it to the output lines. +/// +/// \param row The row containing the cell to be refilled. +/// \param widths The widths of the row. +/// \param column The column being refilled. +/// \param [in,out] textual_rows The output lines as processed so far. This is +/// updated to accomodate for the contents of the refilled cell, extending +/// the rows as necessary. +static void +refill_cell(const text::table_row& row, const text::widths_vector& widths, + const text::table_row::size_type column, + std::vector< text::table_row >& textual_rows) +{ + const std::vector< std::string > rows = text::refill(row[column], + widths[column]); + + if (textual_rows.size() < rows.size()) + textual_rows.resize(rows.size(), text::table_row(row.size())); + + for (std::vector< std::string >::size_type i = 0; i < rows.size(); ++i) { + for (text::table_row::size_type j = 0; j < row.size(); ++j) { + const bool is_last = j == row.size() - 1; + if (j == column) + textual_rows[i][j] = pad_cell(rows[i], widths[j], is_last); + else { + if (textual_rows[i][j].empty()) + textual_rows[i][j] = pad_cell("", widths[j], is_last); + } + } + } +} + + +/// Formats a single table row. +/// +/// \param row The row to format. +/// \param widths The widths of the columns to apply during formatting. Cells +/// wider than the specified width are refilled to attempt to fit in the +/// cell. Cells narrower than the width are right-padded with spaces. +/// \param separator The column separator to use. +/// +/// \return The textual lines that contain the formatted row. +static std::vector< std::string > +format_row(const text::table_row& row, const text::widths_vector& widths, + const std::string& separator) +{ + PRE(row.size() == widths.size()); + + std::vector< text::table_row > textual_rows(1, text::table_row(row.size())); + + for (text::table_row::size_type column = 0; column < row.size(); ++column) { + if (widths[column] > row[column].length()) + textual_rows[0][column] = pad_cell(row[column], widths[column], + column == row.size() - 1); + else + refill_cell(row, widths, column, textual_rows); + } + + std::vector< std::string > lines; + for (std::vector< text::table_row >::const_iterator + iter = textual_rows.begin(); iter != textual_rows.end(); ++iter) { + lines.push_back(text::join(*iter, separator)); + } + return lines; +} + + +} // anonymous namespace + + +/// Constructs a new table. +/// +/// \param ncolumns_ The number of columns that the table will have. +text::table::table(const table_row::size_type ncolumns_) +{ + _column_widths.resize(ncolumns_, 0); +} + + +/// Gets the number of columns in the table. +/// +/// \return The number of columns in the table. This value remains constant +/// during the existence of the table. +text::widths_vector::size_type +text::table::ncolumns(void) const +{ + return _column_widths.size(); +} + + +/// Gets the width of a column. +/// +/// The returned value is not valid if add_row() is called again, as the column +/// may have grown in width. +/// +/// \param column The index of the column of which to get the width. Must be +/// less than the total number of columns. +/// +/// \return The width of a column. +text::widths_vector::value_type +text::table::column_width(const widths_vector::size_type column) const +{ + PRE(column < _column_widths.size()); + return _column_widths[column]; +} + + +/// Gets the widths of all columns. +/// +/// The returned value is not valid if add_row() is called again, as the columns +/// may have grown in width. +/// +/// \return A vector with the width of all columns. +const text::widths_vector& +text::table::column_widths(void) const +{ + return _column_widths; +} + + +/// Checks whether the table is empty or not. +/// +/// \return True if the table is empty; false otherwise. +bool +text::table::empty(void) const +{ + return _rows.empty(); +} + + +/// Adds a row to the table. +/// +/// \param row The row to be added. This row must have the same amount of +/// columns as defined during the construction of the table. +void +text::table::add_row(const table_row& row) +{ + PRE(row.size() == _column_widths.size()); + _rows.push_back(row); + + for (table_row::size_type i = 0; i < row.size(); ++i) + if (_column_widths[i] < row[i].length()) + _column_widths[i] = row[i].length(); +} + + +/// Gets an iterator pointing to the beginning of the rows of the table. +/// +/// \return An iterator on the rows. +text::table::const_iterator +text::table::begin(void) const +{ + return _rows.begin(); +} + + +/// Gets an iterator pointing to the end of the rows of the table. +/// +/// \return An iterator on the rows. +text::table::const_iterator +text::table::end(void) const +{ + return _rows.end(); +} + + +/// Column width to denote that the column has to fit all of its cells. +const std::size_t text::table_formatter::width_auto = 0; + + +/// Column width to denote that the column can be refilled to fit the table. +const std::size_t text::table_formatter::width_refill = + std::numeric_limits< std::size_t >::max(); + + +/// Constructs a new table formatter. +text::table_formatter::table_formatter(void) : + _separator(""), + _table_width(0) +{ +} + + +/// Sets the width of a column. +/// +/// All columns except one must have a width that is, at least, as wide as the +/// widest cell in the column. One of the columns can have a width of +/// width_refill, which indicates that the column will be refilled if the table +/// does not fit in its maximum width. +/// +/// \param column The index of the column to set the width for. +/// \param width The width to set the column to. +/// +/// \return A reference to this formatter to allow using the builder pattern. +text::table_formatter& +text::table_formatter::set_column_width(const table_row::size_type column, + const std::size_t width) +{ +#if !defined(NDEBUG) + if (width == width_refill) { + for (widths_vector::size_type i = 0; i < _column_widths.size(); i++) { + if (i != column) + PRE_MSG(_column_widths[i] != width_refill, + "Only one column width can be set to width_refill"); + } + } +#endif + + if (_column_widths.size() < column + 1) + _column_widths.resize(column + 1, width_auto); + _column_widths[column] = width; + return *this; +} + + +/// Sets the separator to use between the cells. +/// +/// \param separator The separator to use. +/// +/// \return A reference to this formatter to allow using the builder pattern. +text::table_formatter& +text::table_formatter::set_separator(const char* separator) +{ + _separator = separator; + return *this; +} + + +/// Sets the maximum width of the table. +/// +/// \param table_width The maximum width of the table; cannot be zero. +/// +/// \return A reference to this formatter to allow using the builder pattern. +text::table_formatter& +text::table_formatter::set_table_width(const std::size_t table_width) +{ + PRE(table_width > 0); + _table_width = table_width; + return *this; +} + + +/// Formats a table into a collection of textual lines. +/// +/// \param t Table to format. +/// +/// \return A collection of textual lines. +std::vector< std::string > +text::table_formatter::format(const table& t) const +{ + std::vector< std::string > lines; + + if (!t.empty()) { + widths_vector widths = override_column_widths(t, _column_widths); + if (_table_width != 0) + refill_widths(widths, _table_width, _separator.length()); + + for (table::const_iterator iter = t.begin(); iter != t.end(); ++iter) { + const std::vector< std::string > sublines = + format_row(*iter, widths, _separator); + std::copy(sublines.begin(), sublines.end(), + std::back_inserter(lines)); + } + } + + return lines; +} diff --git a/utils/text/table.hpp b/utils/text/table.hpp new file mode 100644 index 000000000000..5fd7c50c991c --- /dev/null +++ b/utils/text/table.hpp @@ -0,0 +1,125 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/text/table.hpp +/// Table construction and formatting. + +#if !defined(UTILS_TEXT_TABLE_HPP) +#define UTILS_TEXT_TABLE_HPP + +#include "utils/text/table_fwd.hpp" + +#include +#include +#include + +namespace utils { +namespace text { + + +/// Representation of a table. +/// +/// A table is nothing more than a matrix of rows by columns. The number of +/// columns is hardcoded at construction times, and the rows can be accumulated +/// at a later stage. +/// +/// The only value of this class is a simpler and more natural mechanism of the +/// construction of a table, with additional sanity checks. We could as well +/// just expose the internal data representation to our users. +class table { + /// Widths of the table columns so far. + widths_vector _column_widths; + + /// Type defining the collection of rows in the table. + typedef std::vector< table_row > rows_vector; + + /// The rows of the table. + /// + /// This is actually the matrix representing the table. Every element of + /// this vector (which are vectors themselves) must have _ncolumns items. + rows_vector _rows; + +public: + table(const table_row::size_type); + + widths_vector::size_type ncolumns(void) const; + widths_vector::value_type column_width(const widths_vector::size_type) + const; + const widths_vector& column_widths(void) const; + + void add_row(const table_row&); + + bool empty(void) const; + + /// Constant iterator on the rows of the table. + typedef rows_vector::const_iterator const_iterator; + + const_iterator begin(void) const; + const_iterator end(void) const; +}; + + +/// Settings to format a table. +/// +/// This class implements a builder pattern to construct an object that contains +/// all the knowledge to format a table. Once all the settings have been set, +/// the format() method provides the algorithm to apply such formatting settings +/// to any input table. +class table_formatter { + /// Text to use as the separator between cells. + std::string _separator; + + /// Colletion of widths of the columns of a table. + std::size_t _table_width; + + /// Widths of the table columns. + /// + /// Note that this only includes widths for the column widths explicitly + /// overriden by the caller. In other words, this vector can be shorter + /// than the table passed to the format() method, which is just fine. Any + /// non-specified column widths are assumed to be width_auto. + widths_vector _column_widths; + +public: + table_formatter(void); + + static const std::size_t width_auto; + static const std::size_t width_refill; + table_formatter& set_column_width(const table_row::size_type, + const std::size_t); + table_formatter& set_separator(const char*); + table_formatter& set_table_width(const std::size_t); + + std::vector< std::string > format(const table&) const; +}; + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_TABLE_HPP) diff --git a/utils/text/table_fwd.hpp b/utils/text/table_fwd.hpp new file mode 100644 index 000000000000..77c6b1fa8c78 --- /dev/null +++ b/utils/text/table_fwd.hpp @@ -0,0 +1,58 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/text/table_fwd.hpp +/// Forward declarations for utils/text/table.hpp + +#if !defined(UTILS_TEXT_TABLE_FWD_HPP) +#define UTILS_TEXT_TABLE_FWD_HPP + +#include +#include +#include + +namespace utils { +namespace text { + + +/// Values of the cells of a particular table row. +typedef std::vector< std::string > table_row; + + +/// Vector of column widths. +typedef std::vector< std::size_t > widths_vector; + + +class table; +class table_formatter; + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_TABLE_FWD_HPP) diff --git a/utils/text/table_test.cpp b/utils/text/table_test.cpp new file mode 100644 index 000000000000..45928dae89c4 --- /dev/null +++ b/utils/text/table_test.cpp @@ -0,0 +1,413 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/table.hpp" + +#include + +#include + +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +/// Performs a check on text::table_formatter. +/// +/// This is provided for test simplicity's sake. Having to match the result of +/// the formatting on a line by line basis would result in too verbose tests +/// (maybe not with C++11, but not using this yet). +/// +/// Because of the flattening of the formatted table into a string, we risk +/// misdetecting problems when the algorithm bundles newlines into the lines of +/// a table. This should not happen, and not accounting for this little detail +/// makes testing so much easier. +/// +/// \param expected Textual representation of the table, as a collection of +/// lines separated by newline characters. +/// \param formatter The formatter to use. +/// \param table The table to format. +static void +table_formatter_check(const std::string& expected, + const text::table_formatter& formatter, + const text::table& table) +{ + ATF_REQUIRE_EQ(expected, text::join(formatter.format(table), "\n") + "\n"); +} + + + +ATF_TEST_CASE_WITHOUT_HEAD(table__ncolumns); +ATF_TEST_CASE_BODY(table__ncolumns) +{ + ATF_REQUIRE_EQ(5, text::table(5).ncolumns()); + ATF_REQUIRE_EQ(10, text::table(10).ncolumns()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table__column_width); +ATF_TEST_CASE_BODY(table__column_width) +{ + text::table_row row1; + row1.push_back("1234"); + row1.push_back("123456"); + text::table_row row2; + row2.push_back("12"); + row2.push_back("12345678"); + + text::table table(2); + table.add_row(row1); + table.add_row(row2); + + ATF_REQUIRE_EQ(4, table.column_width(0)); + ATF_REQUIRE_EQ(8, table.column_width(1)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table__column_widths); +ATF_TEST_CASE_BODY(table__column_widths) +{ + text::table_row row1; + row1.push_back("1234"); + row1.push_back("123456"); + text::table_row row2; + row2.push_back("12"); + row2.push_back("12345678"); + + text::table table(2); + table.add_row(row1); + table.add_row(row2); + + ATF_REQUIRE_EQ(4, table.column_widths()[0]); + ATF_REQUIRE_EQ(8, table.column_widths()[1]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table__empty); +ATF_TEST_CASE_BODY(table__empty) +{ + text::table table(2); + ATF_REQUIRE(table.empty()); + table.add_row(text::table_row(2)); + ATF_REQUIRE(!table.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table__iterate); +ATF_TEST_CASE_BODY(table__iterate) +{ + text::table_row row1; + row1.push_back("foo"); + text::table_row row2; + row2.push_back("bar"); + + text::table table(1); + table.add_row(row1); + table.add_row(row2); + + text::table::const_iterator iter = table.begin(); + ATF_REQUIRE(iter != table.end()); + ATF_REQUIRE(row1 == *iter); + ++iter; + ATF_REQUIRE(iter != table.end()); + ATF_REQUIRE(row2 == *iter); + ++iter; + ATF_REQUIRE(iter == table.end()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__empty); +ATF_TEST_CASE_BODY(table_formatter__empty) +{ + ATF_REQUIRE(text::table_formatter().set_separator(" ") + .format(text::table(1)).empty()); + ATF_REQUIRE(text::table_formatter().set_separator(" ") + .format(text::table(10)).empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__defaults); +ATF_TEST_CASE_BODY(table_formatter__defaults) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + table_formatter_check( + "First Second Third\n" + "Fourth with some textFifth with some more textSixth foo\n", + text::table_formatter(), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__one_column__no_max_width); +ATF_TEST_CASE_BODY(table_formatter__one_column__no_max_width) +{ + text::table table(1); + { + text::table_row row; + row.push_back("First row with some words"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Second row with some words"); + table.add_row(row); + } + + table_formatter_check( + "First row with some words\n" + "Second row with some words\n", + text::table_formatter().set_separator(" | "), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__one_column__explicit_width); +ATF_TEST_CASE_BODY(table_formatter__one_column__explicit_width) +{ + text::table table(1); + { + text::table_row row; + row.push_back("First row with some words"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Second row with some words"); + table.add_row(row); + } + + table_formatter_check( + "First row with some words\n" + "Second row with some words\n", + text::table_formatter().set_separator(" | ").set_column_width(0, 1024), + table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__one_column__max_width); +ATF_TEST_CASE_BODY(table_formatter__one_column__max_width) +{ + text::table table(1); + { + text::table_row row; + row.push_back("First row with some words"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Second row with some words"); + table.add_row(row); + } + + table_formatter_check( + "First row\nwith some\nwords\n" + "Second row\nwith some\nwords\n", + text::table_formatter().set_separator(" | ").set_table_width(11), + table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__many_columns__no_max_width); +ATF_TEST_CASE_BODY(table_formatter__many_columns__no_max_width) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + table_formatter_check( + "First | Second | Third\n" + "Fourth with some text | Fifth with some more text | Sixth foo\n", + text::table_formatter().set_separator(" | "), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__many_columns__explicit_width); +ATF_TEST_CASE_BODY(table_formatter__many_columns__explicit_width) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + table_formatter_check( + "First | Second | Third\n" + "Fourth with some text | Fifth with some more text | Sixth foo\n", + text::table_formatter().set_separator(" | ").set_column_width(0, 23) + .set_column_width(1, 28), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__many_columns__max_width); +ATF_TEST_CASE_BODY(table_formatter__many_columns__max_width) +{ + text::table table(3); + { + text::table_row row; + row.push_back("First"); + row.push_back("Second"); + row.push_back("Third"); + table.add_row(row); + } + { + text::table_row row; + row.push_back("Fourth with some text"); + row.push_back("Fifth with some more text"); + row.push_back("Sixth foo"); + table.add_row(row); + } + + table_formatter_check( + "First | Second | Third\n" + "Fourth with some text | Fifth with | Sixth foo\n" + " | some more | \n" + " | text | \n", + text::table_formatter().set_separator(" | ").set_table_width(46) + .set_column_width(1, text::table_formatter::width_refill) + .set_column_width(0, text::table_formatter::width_auto), table); + + table_formatter_check( + "First | Second | Third\n" + "Fourth with some text | Fifth with | Sixth foo\n" + " | some more | \n" + " | text | \n", + text::table_formatter().set_separator(" | ").set_table_width(48) + .set_column_width(1, text::table_formatter::width_refill) + .set_column_width(0, 23), table); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(table_formatter__use_case__cli_help); +ATF_TEST_CASE_BODY(table_formatter__use_case__cli_help) +{ + text::table options_table(2); + { + text::table_row row; + row.push_back("-a a_value"); + row.push_back("This is the description of the first flag"); + options_table.add_row(row); + } + { + text::table_row row; + row.push_back("-b"); + row.push_back("And this is the text for the second flag"); + options_table.add_row(row); + } + + text::table commands_table(2); + { + text::table_row row; + row.push_back("first"); + row.push_back("This is the first command"); + commands_table.add_row(row); + } + { + text::table_row row; + row.push_back("second"); + row.push_back("And this is the second command"); + commands_table.add_row(row); + } + + const text::widths_vector::value_type first_width = + std::max(options_table.column_width(0), commands_table.column_width(0)); + + table_formatter_check( + "-a a_value This is the description\n" + " of the first flag\n" + "-b And this is the text for\n" + " the second flag\n", + text::table_formatter().set_separator(" ").set_table_width(36) + .set_column_width(0, first_width) + .set_column_width(1, text::table_formatter::width_refill), + options_table); + + table_formatter_check( + "first This is the first\n" + " command\n" + "second And this is the second\n" + " command\n", + text::table_formatter().set_separator(" ").set_table_width(36) + .set_column_width(0, first_width) + .set_column_width(1, text::table_formatter::width_refill), + commands_table); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, table__ncolumns); + ATF_ADD_TEST_CASE(tcs, table__column_width); + ATF_ADD_TEST_CASE(tcs, table__column_widths); + ATF_ADD_TEST_CASE(tcs, table__empty); + ATF_ADD_TEST_CASE(tcs, table__iterate); + + ATF_ADD_TEST_CASE(tcs, table_formatter__empty); + ATF_ADD_TEST_CASE(tcs, table_formatter__defaults); + ATF_ADD_TEST_CASE(tcs, table_formatter__one_column__no_max_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__one_column__explicit_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__one_column__max_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__many_columns__no_max_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__many_columns__explicit_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__many_columns__max_width); + ATF_ADD_TEST_CASE(tcs, table_formatter__use_case__cli_help); +} diff --git a/utils/text/templates.cpp b/utils/text/templates.cpp new file mode 100644 index 000000000000..13cb27b1cce2 --- /dev/null +++ b/utils/text/templates.cpp @@ -0,0 +1,764 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/templates.hpp" + +#include +#include +#include +#include + +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/noncopyable.hpp" +#include "utils/sanity.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace text = utils::text; + + +namespace { + + +/// Definition of a template statement. +/// +/// A template statement is a particular line in the input file that is +/// preceeded by a template marker. This class provides a high-level +/// representation of the contents of such statement and a mechanism to parse +/// the textual line into this high-level representation. +class statement_def { +public: + /// Types of the known statements. + enum statement_type { + /// Alternative clause of a conditional. + /// + /// Takes no arguments. + type_else, + + /// End of conditional marker. + /// + /// Takes no arguments. + type_endif, + + /// End of loop marker. + /// + /// Takes no arguments. + type_endloop, + + /// Beginning of a conditional. + /// + /// Takes a single argument, which denotes the name of the variable or + /// vector to check for existence. This is the only expression + /// supported. + type_if, + + /// Beginning of a loop over all the elements of a vector. + /// + /// Takes two arguments: the name of the vector over which to iterate + /// and the name of the iterator to later index this vector. + type_loop, + }; + +private: + /// Internal data describing the structure of a particular statement type. + struct type_descriptor { + /// The native type of the statement. + statement_type type; + + /// The expected number of arguments. + unsigned int n_arguments; + + /// Constructs a new type descriptor. + /// + /// \param type_ The native type of the statement. + /// \param n_arguments_ The expected number of arguments. + type_descriptor(const statement_type type_, + const unsigned int n_arguments_) + : type(type_), n_arguments(n_arguments_) + { + } + }; + + /// Mapping of statement type names to their definitions. + typedef std::map< std::string, type_descriptor > types_map; + + /// Description of the different statement types. + /// + /// This static map is initialized once and reused later for any statement + /// lookup. Unfortunately, we cannot perform this initialization in a + /// static manner without C++11. + static types_map _types; + + /// Generates a new types definition map. + /// + /// \return A new types definition map, to be assigned to _types. + static types_map + generate_types_map(void) + { + // If you change this, please edit the comments in the enum above. + types_map types; + types.insert(types_map::value_type( + "else", type_descriptor(type_else, 0))); + types.insert(types_map::value_type( + "endif", type_descriptor(type_endif, 0))); + types.insert(types_map::value_type( + "endloop", type_descriptor(type_endloop, 0))); + types.insert(types_map::value_type( + "if", type_descriptor(type_if, 1))); + types.insert(types_map::value_type( + "loop", type_descriptor(type_loop, 2))); + return types; + } + +public: + /// The type of the statement. + statement_type type; + + /// The arguments to the statement, in textual form. + const std::vector< std::string > arguments; + + /// Creates a new statement. + /// + /// \param type_ The type of the statement. + /// \param arguments_ The arguments to the statement. + statement_def(const statement_type& type_, + const std::vector< std::string >& arguments_) : + type(type_), arguments(arguments_) + { +#if !defined(NDEBUG) + for (types_map::const_iterator iter = _types.begin(); + iter != _types.end(); ++iter) { + const type_descriptor& descriptor = (*iter).second; + if (descriptor.type == type_) { + PRE(descriptor.n_arguments == arguments_.size()); + return; + } + } + UNREACHABLE; +#endif + } + + /// Parses a statement. + /// + /// \param line The textual representation of the statement without any + /// prefix. + /// + /// \return The parsed statement. + /// + /// \throw text::syntax_error If the statement is not correctly defined. + static statement_def + parse(const std::string& line) + { + if (_types.empty()) + _types = generate_types_map(); + + const std::vector< std::string > words = text::split(line, ' '); + if (words.empty()) + throw text::syntax_error("Empty statement"); + + const types_map::const_iterator iter = _types.find(words[0]); + if (iter == _types.end()) + throw text::syntax_error(F("Unknown statement '%s'") % words[0]); + const type_descriptor& descriptor = (*iter).second; + + if (words.size() - 1 != descriptor.n_arguments) + throw text::syntax_error(F("Invalid number of arguments for " + "statement '%s'") % words[0]); + + std::vector< std::string > new_arguments; + new_arguments.resize(words.size() - 1); + std::copy(words.begin() + 1, words.end(), new_arguments.begin()); + + return statement_def(descriptor.type, new_arguments); + } +}; + + +statement_def::types_map statement_def::_types; + + +/// Definition of a loop. +/// +/// This simple structure is used to keep track of the parameters of a loop. +struct loop_def { + /// The name of the vector over which this loop is iterating. + std::string vector; + + /// The name of the iterator defined by this loop. + std::string iterator; + + /// Position in the input to which to rewind to on looping. + /// + /// This position points to the line after the loop statement, not the loop + /// itself. This is one of the reasons why we have this structure, so that + /// we can maintain the data about the loop without having to re-process it. + std::istream::pos_type position; + + /// Constructs a new loop definition. + /// + /// \param vector_ The name of the vector (first argument). + /// \param iterator_ The name of the iterator (second argumnet). + /// \param position_ Position of the next line after the loop statement. + loop_def(const std::string& vector_, const std::string& iterator_, + const std::istream::pos_type position_) : + vector(vector_), iterator(iterator_), position(position_) + { + } +}; + + +/// Stateful class to instantiate the templates in an input stream. +/// +/// The goal of this parser is to scan the input once and not buffer anything in +/// memory. The only exception are loops: loops are reinterpreted on every +/// iteration from the same input file by rewidining the stream to the +/// appropriate position. +class templates_parser : utils::noncopyable { + /// The templates to apply. + /// + /// Note that this is not const because the parser has to have write access + /// to the templates. In particular, it needs to be able to define the + /// iterators as regular variables. + text::templates_def _templates; + + /// Prefix that marks a line as a statement. + const std::string _prefix; + + /// Delimiter to surround an expression instantiation. + const std::string _delimiter; + + /// Whether to skip incoming lines or not. + /// + /// The top of the stack is true whenever we encounter a conditional that + /// evaluates to false or a loop that does not have any iterations left. + /// Under these circumstances, we need to continue scanning the input stream + /// until we find the matching closing endif or endloop construct. + /// + /// This is a stack rather than a plain boolean to allow us deal with + /// if-else clauses. + std::stack< bool > _skip; + + /// Current count of nested conditionals. + unsigned int _if_level; + + /// Level of the top-most conditional that evaluated to false. + unsigned int _exit_if_level; + + /// Current count of nested loops. + unsigned int _loop_level; + + /// Level of the top-most loop that does not have any iterations left. + unsigned int _exit_loop_level; + + /// Information about all the nested loops up to the current point. + std::stack< loop_def > _loops; + + /// Checks if a line is a statement or not. + /// + /// \param line The line to validate. + /// + /// \return True if the line looks like a statement, which is determined by + /// checking if the line starts by the predefined prefix. + bool + is_statement(const std::string& line) + { + return ((line.length() >= _prefix.length() && + line.substr(0, _prefix.length()) == _prefix) && + (line.length() < _delimiter.length() || + line.substr(0, _delimiter.length()) != _delimiter)); + } + + /// Parses a given statement line into a statement definition. + /// + /// \param line The line to validate; it must be a valid statement. + /// + /// \return The parsed statement. + /// + /// \throw text::syntax_error If the input is not a valid statement. + statement_def + parse_statement(const std::string& line) + { + PRE(is_statement(line)); + return statement_def::parse(line.substr(_prefix.length())); + } + + /// Processes a line from the input when not in skip mode. + /// + /// \param line The line to be processed. + /// \param input The input stream from which the line was read. The current + /// position in the stream must be after the line being processed. + /// \param output The output stream into which to write the results. + /// + /// \throw text::syntax_error If the input is not valid. + void + handle_normal(const std::string& line, std::istream& input, + std::ostream& output) + { + if (!is_statement(line)) { + // Fast path. Mostly to avoid an indentation level for the big + // chunk of code below. + output << line << '\n'; + return; + } + + const statement_def statement = parse_statement(line); + + switch (statement.type) { + case statement_def::type_else: + _skip.top() = !_skip.top(); + break; + + case statement_def::type_endif: + _if_level--; + break; + + case statement_def::type_endloop: { + PRE(_loops.size() == _loop_level); + loop_def& loop = _loops.top(); + + const std::size_t next_index = 1 + text::to_type< std::size_t >( + _templates.get_variable(loop.iterator)); + + if (next_index < _templates.get_vector(loop.vector).size()) { + _templates.add_variable(loop.iterator, F("%s") % next_index); + input.seekg(loop.position); + } else { + _loop_level--; + _loops.pop(); + _templates.remove_variable(loop.iterator); + } + } break; + + case statement_def::type_if: { + _if_level++; + const std::string value = _templates.evaluate( + statement.arguments[0]); + if (value.empty() || value == "0" || value == "false") { + _exit_if_level = _if_level; + _skip.push(true); + } else { + _skip.push(false); + } + } break; + + case statement_def::type_loop: { + _loop_level++; + + const loop_def loop(statement.arguments[0], statement.arguments[1], + input.tellg()); + if (_templates.get_vector(loop.vector).empty()) { + _exit_loop_level = _loop_level; + _skip.push(true); + } else { + _templates.add_variable(loop.iterator, "0"); + _loops.push(loop); + _skip.push(false); + } + } break; + } + } + + /// Processes a line from the input when in skip mode. + /// + /// \param line The line to be processed. + /// + /// \throw text::syntax_error If the input is not valid. + void + handle_skip(const std::string& line) + { + PRE(_skip.top()); + + if (!is_statement(line)) + return; + + const statement_def statement = parse_statement(line); + switch (statement.type) { + case statement_def::type_else: + if (_exit_if_level == _if_level) + _skip.top() = !_skip.top(); + break; + + case statement_def::type_endif: + INV(_if_level >= _exit_if_level); + if (_if_level == _exit_if_level) + _skip.top() = false; + _if_level--; + _skip.pop(); + break; + + case statement_def::type_endloop: + INV(_loop_level >= _exit_loop_level); + if (_loop_level == _exit_loop_level) + _skip.top() = false; + _loop_level--; + _skip.pop(); + break; + + case statement_def::type_if: + _if_level++; + _skip.push(true); + break; + + case statement_def::type_loop: + _loop_level++; + _skip.push(true); + break; + + default: + break; + } + } + + /// Evaluates expressions on a given input line. + /// + /// An expression is surrounded by _delimiter on both sides. We scan the + /// string from left to right finding any expressions that may appear, yank + /// them out and call templates_def::evaluate() to get their value. + /// + /// Lonely or unbalanced appearances of _delimiter on the input line are + /// not considered an error, given that the user may actually want to supply + /// that character sequence without being interpreted as a template. + /// + /// \param in_line The input line from which to evaluate expressions. + /// + /// \return The evaluated line. + /// + /// \throw text::syntax_error If the expressions in the line are malformed. + std::string + evaluate(const std::string& in_line) + { + std::string out_line; + + std::string::size_type last_pos = 0; + while (last_pos != std::string::npos) { + const std::string::size_type open_pos = in_line.find( + _delimiter, last_pos); + if (open_pos == std::string::npos) { + out_line += in_line.substr(last_pos); + last_pos = std::string::npos; + } else { + const std::string::size_type close_pos = in_line.find( + _delimiter, open_pos + _delimiter.length()); + if (close_pos == std::string::npos) { + out_line += in_line.substr(last_pos); + last_pos = std::string::npos; + } else { + out_line += in_line.substr(last_pos, open_pos - last_pos); + out_line += _templates.evaluate(in_line.substr( + open_pos + _delimiter.length(), + close_pos - open_pos - _delimiter.length())); + last_pos = close_pos + _delimiter.length(); + } + } + } + + return out_line; + } + +public: + /// Constructs a new template parser. + /// + /// \param templates_ The templates to apply to the processed file. + /// \param prefix_ The prefix that identifies lines as statements. + /// \param delimiter_ Delimiter to surround a variable instantiation. + templates_parser(const text::templates_def& templates_, + const std::string& prefix_, + const std::string& delimiter_) : + _templates(templates_), + _prefix(prefix_), + _delimiter(delimiter_), + _if_level(0), + _exit_if_level(0), + _loop_level(0), + _exit_loop_level(0) + { + } + + /// Applies the templates to a given input. + /// + /// \param input The stream to which to apply the templates. + /// \param output The stream into which to write the results. + /// + /// \throw text::syntax_error If the input is not valid. Note that the + /// is not guaranteed to be unmodified on exit if an error is + /// encountered. + void + instantiate(std::istream& input, std::ostream& output) + { + std::string line; + while (std::getline(input, line).good()) { + if (!_skip.empty() && _skip.top()) + handle_skip(line); + else + handle_normal(evaluate(line), input, output); + } + } +}; + + +} // anonymous namespace + + +/// Constructs an empty templates definition. +text::templates_def::templates_def(void) +{ +} + + +/// Sets a string variable in the templates. +/// +/// If the variable already exists, its value is replaced. This behavior is +/// required to implement iterators, but client code should really not be +/// redefining variables. +/// +/// \pre The variable must not already exist as a vector. +/// +/// \param name The name of the variable to set. +/// \param value The value to set the given variable to. +void +text::templates_def::add_variable(const std::string& name, + const std::string& value) +{ + PRE(_vectors.find(name) == _vectors.end()); + _variables[name] = value; +} + + +/// Unsets a string variable from the templates. +/// +/// Client code has no reason to use this. This is only required to implement +/// proper scoping of loop iterators. +/// +/// \pre The variable must exist. +/// +/// \param name The name of the variable to remove from the templates. +void +text::templates_def::remove_variable(const std::string& name) +{ + PRE(_variables.find(name) != _variables.end()); + _variables.erase(_variables.find(name)); +} + + +/// Creates a new vector in the templates. +/// +/// If the vector already exists, it is cleared. Client code should really not +/// be redefining variables. +/// +/// \pre The vector must not already exist as a variable. +/// +/// \param name The name of the vector to set. +void +text::templates_def::add_vector(const std::string& name) +{ + PRE(_variables.find(name) == _variables.end()); + _vectors[name] = strings_vector(); +} + + +/// Adds a value to an existing vector in the templates. +/// +/// \pre name The vector must exist. +/// +/// \param name The name of the vector to append the value to. +/// \param value The textual value to append to the vector. +void +text::templates_def::add_to_vector(const std::string& name, + const std::string& value) +{ + PRE(_variables.find(name) == _variables.end()); + PRE(_vectors.find(name) != _vectors.end()); + _vectors[name].push_back(value); +} + + +/// Checks whether a given identifier exists as a variable or a vector. +/// +/// This is used to implement the evaluation of conditions in if clauses. +/// +/// \param name The name of the variable or vector. +/// +/// \return True if the given name exists as a variable or a vector; false +/// otherwise. +bool +text::templates_def::exists(const std::string& name) const +{ + return (_variables.find(name) != _variables.end() || + _vectors.find(name) != _vectors.end()); +} + + +/// Gets the value of a variable. +/// +/// \param name The name of the variable. +/// +/// \return The value of the requested variable. +/// +/// \throw text::syntax_error If the variable does not exist. +const std::string& +text::templates_def::get_variable(const std::string& name) const +{ + const variables_map::const_iterator iter = _variables.find(name); + if (iter == _variables.end()) + throw text::syntax_error(F("Unknown variable '%s'") % name); + return (*iter).second; +} + + +/// Gets a vector. +/// +/// \param name The name of the vector. +/// +/// \return A reference to the requested vector. +/// +/// \throw text::syntax_error If the vector does not exist. +const text::templates_def::strings_vector& +text::templates_def::get_vector(const std::string& name) const +{ + const vectors_map::const_iterator iter = _vectors.find(name); + if (iter == _vectors.end()) + throw text::syntax_error(F("Unknown vector '%s'") % name); + return (*iter).second; +} + + +/// Indexes a vector and gets the value. +/// +/// \param name The name of the vector to index. +/// \param index_name The name of a variable representing the index to use. +/// This must be convertible to a natural. +/// +/// \return The value of the vector at the given index. +/// +/// \throw text::syntax_error If the vector does not existor if the index is out +/// of range. +const std::string& +text::templates_def::get_vector(const std::string& name, + const std::string& index_name) const +{ + const strings_vector& vector = get_vector(name); + const std::string& index_str = get_variable(index_name); + + std::size_t index; + try { + index = text::to_type< std::size_t >(index_str); + } catch (const text::syntax_error& e) { + throw text::syntax_error(F("Index '%s' not an integer, value '%s'") % + index_name % index_str); + } + if (index >= vector.size()) + throw text::syntax_error(F("Index '%s' out of range at position '%s'") % + index_name % index); + + return vector[index]; +} + + +/// Evaluates a expression using these templates. +/// +/// An expression is a query on the current templates to fetch a particular +/// value. The value is always returned as a string, as this is how templates +/// are internally stored. +/// +/// \param expression The expression to evaluate. This should not include any +/// of the delimiters used in the user input, as otherwise the expression +/// will not be evaluated properly. +/// +/// \return The result of the expression evaluation as a string. +/// +/// \throw text::syntax_error If there is any problem while evaluating the +/// expression. +std::string +text::templates_def::evaluate(const std::string& expression) const +{ + const std::string::size_type paren_open = expression.find('('); + if (paren_open == std::string::npos) { + return get_variable(expression); + } else { + const std::string::size_type paren_close = expression.find( + ')', paren_open); + if (paren_close == std::string::npos) + throw text::syntax_error(F("Expected ')' in expression '%s')") % + expression); + if (paren_close != expression.length() - 1) + throw text::syntax_error(F("Unexpected text found after ')' in " + "expression '%s'") % expression); + + const std::string arg0 = expression.substr(0, paren_open); + const std::string arg1 = expression.substr( + paren_open + 1, paren_close - paren_open - 1); + if (arg0 == "defined") { + return exists(arg1) ? "true" : "false"; + } else if (arg0 == "length") { + return F("%s") % get_vector(arg1).size(); + } else { + return get_vector(arg0, arg1); + } + } +} + + +/// Applies a set of templates to an input stream. +/// +/// \param templates The templates to use. +/// \param input The input to process. +/// \param output The stream to which to write the processed text. +/// +/// \throw text::syntax_error If there is any problem processing the input. +void +text::instantiate(const templates_def& templates, + std::istream& input, std::ostream& output) +{ + templates_parser parser(templates, "%", "%%"); + parser.instantiate(input, output); +} + + +/// Applies a set of templates to an input file and writes an output file. +/// +/// \param templates The templates to use. +/// \param input_file The path to the input to process. +/// \param output_file The path to the file into which to write the output. +/// +/// \throw text::error If the input or output files cannot be opened. +/// \throw text::syntax_error If there is any problem processing the input. +void +text::instantiate(const templates_def& templates, + const fs::path& input_file, const fs::path& output_file) +{ + std::ifstream input(input_file.c_str()); + if (!input) + throw text::error(F("Failed to open %s for read") % input_file); + + std::ofstream output(output_file.c_str()); + if (!output) + throw text::error(F("Failed to open %s for write") % output_file); + + instantiate(templates, input, output); +} diff --git a/utils/text/templates.hpp b/utils/text/templates.hpp new file mode 100644 index 000000000000..ffbf28512d0d --- /dev/null +++ b/utils/text/templates.hpp @@ -0,0 +1,122 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/text/templates.hpp +/// Custom templating engine for text documents. +/// +/// This module provides a simple mechanism to generate text documents based on +/// templates. The templates are just text files that contain template +/// statements that instruct this processor to perform transformations on the +/// input. +/// +/// While this was originally written to handle HTML templates, it is actually +/// generic enough to handle any kind of text document, hence why it lives +/// within the utils::text library. +/// +/// An example of how the templates look like: +/// +/// %if names +/// List of names +/// ------------- +/// Amount of names: %%length(names)%% +/// Most preferred name: %%preferred_name%% +/// Full list: +/// %loop names iter +/// * %%last_names(iter)%%, %%names(iter)%% +/// %endloop +/// %endif names + +#if !defined(UTILS_TEXT_TEMPLATES_HPP) +#define UTILS_TEXT_TEMPLATES_HPP + +#include "utils/text/templates_fwd.hpp" + +#include +#include +#include +#include +#include + +#include "utils/fs/path_fwd.hpp" + +namespace utils { +namespace text { + + +/// Definitions of the templates to apply to a file. +/// +/// This class provides the environment (e.g. the list of variables) that the +/// templating system has to use when generating the output files. This +/// definition is static in the sense that this is what the caller program +/// specifies. +class templates_def { + /// Mapping of variable names to their values. + typedef std::map< std::string, std::string > variables_map; + + /// Collection of global variables available to the templates. + variables_map _variables; + + /// Convenience name for a vector of strings. + typedef std::vector< std::string > strings_vector; + + /// Mapping of vector names to their contents. + /// + /// Ideally, these would be represented as part of the _variables, but we + /// would need a complex mechanism to identify whether a variable is a + /// string or a vector. + typedef std::map< std::string, strings_vector > vectors_map; + + /// Collection of vectors available to the templates. + vectors_map _vectors; + + const std::string& get_vector(const std::string&, const std::string&) const; + +public: + templates_def(void); + + void add_variable(const std::string&, const std::string&); + void remove_variable(const std::string&); + void add_vector(const std::string&); + void add_to_vector(const std::string&, const std::string&); + + bool exists(const std::string&) const; + const std::string& get_variable(const std::string&) const; + const strings_vector& get_vector(const std::string&) const; + + std::string evaluate(const std::string&) const; +}; + + +void instantiate(const templates_def&, std::istream&, std::ostream&); +void instantiate(const templates_def&, const fs::path&, const fs::path&); + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_TEMPLATES_HPP) diff --git a/utils/text/templates_fwd.hpp b/utils/text/templates_fwd.hpp new file mode 100644 index 000000000000..c806be0cf497 --- /dev/null +++ b/utils/text/templates_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/text/templates_fwd.hpp +/// Forward declarations for utils/text/templates.hpp + +#if !defined(UTILS_TEXT_TEMPLATES_FWD_HPP) +#define UTILS_TEXT_TEMPLATES_FWD_HPP + +namespace utils { +namespace text { + + +class templates_def; + + +} // namespace text +} // namespace utils + +#endif // !defined(UTILS_TEXT_TEMPLATES_FWD_HPP) diff --git a/utils/text/templates_test.cpp b/utils/text/templates_test.cpp new file mode 100644 index 000000000000..4524dc61a416 --- /dev/null +++ b/utils/text/templates_test.cpp @@ -0,0 +1,1001 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/text/templates.hpp" + +#include +#include + +#include + +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/text/exceptions.hpp" + +namespace fs = utils::fs; +namespace text = utils::text; + + +namespace { + + +/// Applies a set of templates to an input string and validates the output. +/// +/// This fails the test case if exp_output does not match the document generated +/// by the application of the templates. +/// +/// \param templates The templates to apply. +/// \param input_str The input document to which to apply the templates. +/// \param exp_output The expected output document. +static void +do_test_ok(const text::templates_def& templates, const std::string& input_str, + const std::string& exp_output) +{ + std::istringstream input(input_str); + std::ostringstream output; + + text::instantiate(templates, input, output); + ATF_REQUIRE_EQ(exp_output, output.str()); +} + + +/// Applies a set of templates to an input string and checks for an error. +/// +/// This fails the test case if the exception raised by the template processing +/// does not match the expected message. +/// +/// \param templates The templates to apply. +/// \param input_str The input document to which to apply the templates. +/// \param exp_message The expected error message in the raised exception. +static void +do_test_fail(const text::templates_def& templates, const std::string& input_str, + const std::string& exp_message) +{ + std::istringstream input(input_str); + std::ostringstream output; + + ATF_REQUIRE_THROW_RE(text::syntax_error, exp_message, + text::instantiate(templates, input, output)); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_variable__first); +ATF_TEST_CASE_BODY(templates_def__add_variable__first) +{ + text::templates_def templates; + templates.add_variable("the-name", "first-value"); + ATF_REQUIRE_EQ("first-value", templates.get_variable("the-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_variable__replace); +ATF_TEST_CASE_BODY(templates_def__add_variable__replace) +{ + text::templates_def templates; + templates.add_variable("the-name", "first-value"); + templates.add_variable("the-name", "second-value"); + ATF_REQUIRE_EQ("second-value", templates.get_variable("the-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__remove_variable); +ATF_TEST_CASE_BODY(templates_def__remove_variable) +{ + text::templates_def templates; + templates.add_variable("the-name", "the-value"); + templates.get_variable("the-name"); // Should not throw. + templates.remove_variable("the-name"); + ATF_REQUIRE_THROW(text::syntax_error, templates.get_variable("the-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_vector__first); +ATF_TEST_CASE_BODY(templates_def__add_vector__first) +{ + text::templates_def templates; + templates.add_vector("the-name"); + ATF_REQUIRE(templates.get_vector("the-name").empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_vector__replace); +ATF_TEST_CASE_BODY(templates_def__add_vector__replace) +{ + text::templates_def templates; + templates.add_vector("the-name"); + templates.add_to_vector("the-name", "foo"); + ATF_REQUIRE(!templates.get_vector("the-name").empty()); + templates.add_vector("the-name"); + ATF_REQUIRE(templates.get_vector("the-name").empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__add_to_vector); +ATF_TEST_CASE_BODY(templates_def__add_to_vector) +{ + text::templates_def templates; + templates.add_vector("the-name"); + ATF_REQUIRE_EQ(0, templates.get_vector("the-name").size()); + templates.add_to_vector("the-name", "first"); + ATF_REQUIRE_EQ(1, templates.get_vector("the-name").size()); + templates.add_to_vector("the-name", "second"); + ATF_REQUIRE_EQ(2, templates.get_vector("the-name").size()); + templates.add_to_vector("the-name", "third"); + ATF_REQUIRE_EQ(3, templates.get_vector("the-name").size()); + + std::vector< std::string > expected; + expected.push_back("first"); + expected.push_back("second"); + expected.push_back("third"); + ATF_REQUIRE(expected == templates.get_vector("the-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__exists__variable); +ATF_TEST_CASE_BODY(templates_def__exists__variable) +{ + text::templates_def templates; + ATF_REQUIRE(!templates.exists("some-name")); + templates.add_variable("some-name ", "foo"); + ATF_REQUIRE(!templates.exists("some-name")); + templates.add_variable("some-name", "foo"); + ATF_REQUIRE(templates.exists("some-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__exists__vector); +ATF_TEST_CASE_BODY(templates_def__exists__vector) +{ + text::templates_def templates; + ATF_REQUIRE(!templates.exists("some-name")); + templates.add_vector("some-name "); + ATF_REQUIRE(!templates.exists("some-name")); + templates.add_vector("some-name"); + ATF_REQUIRE(templates.exists("some-name")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__get_variable__ok); +ATF_TEST_CASE_BODY(templates_def__get_variable__ok) +{ + text::templates_def templates; + templates.add_variable("foo", ""); + templates.add_variable("bar", " baz "); + ATF_REQUIRE_EQ("", templates.get_variable("foo")); + ATF_REQUIRE_EQ(" baz ", templates.get_variable("bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__get_variable__unknown); +ATF_TEST_CASE_BODY(templates_def__get_variable__unknown) +{ + text::templates_def templates; + templates.add_variable("foo", ""); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown variable 'foo '", + templates.get_variable("foo ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__get_vector__ok); +ATF_TEST_CASE_BODY(templates_def__get_vector__ok) +{ + text::templates_def templates; + templates.add_vector("foo"); + templates.add_vector("bar"); + templates.add_to_vector("bar", "baz"); + ATF_REQUIRE_EQ(0, templates.get_vector("foo").size()); + ATF_REQUIRE_EQ(1, templates.get_vector("bar").size()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__get_vector__unknown); +ATF_TEST_CASE_BODY(templates_def__get_vector__unknown) +{ + text::templates_def templates; + templates.add_vector("foo"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown vector 'foo '", + templates.get_vector("foo ")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__variable__ok); +ATF_TEST_CASE_BODY(templates_def__evaluate__variable__ok) +{ + text::templates_def templates; + templates.add_variable("foo", ""); + templates.add_variable("bar", " baz "); + ATF_REQUIRE_EQ("", templates.evaluate("foo")); + ATF_REQUIRE_EQ(" baz ", templates.evaluate("bar")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__variable__unknown); +ATF_TEST_CASE_BODY(templates_def__evaluate__variable__unknown) +{ + text::templates_def templates; + templates.add_variable("foo", ""); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown variable 'foo1'", + templates.evaluate("foo1")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__vector__ok); +ATF_TEST_CASE_BODY(templates_def__evaluate__vector__ok) +{ + text::templates_def templates; + templates.add_vector("v"); + templates.add_to_vector("v", "foo"); + templates.add_to_vector("v", "bar"); + templates.add_to_vector("v", "baz"); + + templates.add_variable("index", "0"); + ATF_REQUIRE_EQ("foo", templates.evaluate("v(index)")); + templates.add_variable("index", "1"); + ATF_REQUIRE_EQ("bar", templates.evaluate("v(index)")); + templates.add_variable("index", "2"); + ATF_REQUIRE_EQ("baz", templates.evaluate("v(index)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__vector__unknown_vector); +ATF_TEST_CASE_BODY(templates_def__evaluate__vector__unknown_vector) +{ + text::templates_def templates; + templates.add_vector("v"); + templates.add_to_vector("v", "foo"); + templates.add_variable("index", "0"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown vector 'fooz'", + templates.evaluate("fooz(index)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__vector__unknown_index); +ATF_TEST_CASE_BODY(templates_def__evaluate__vector__unknown_index) +{ + text::templates_def templates; + templates.add_vector("v"); + templates.add_to_vector("v", "foo"); + templates.add_variable("index", "0"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown variable 'indexz'", + templates.evaluate("v(indexz)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__vector__out_of_range); +ATF_TEST_CASE_BODY(templates_def__evaluate__vector__out_of_range) +{ + text::templates_def templates; + templates.add_vector("v"); + templates.add_to_vector("v", "foo"); + templates.add_variable("index", "1"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Index 'index' out of range " + "at position '1'", templates.evaluate("v(index)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__defined); +ATF_TEST_CASE_BODY(templates_def__evaluate__defined) +{ + text::templates_def templates; + templates.add_vector("the-variable"); + templates.add_vector("the-vector"); + ATF_REQUIRE_EQ("false", templates.evaluate("defined(the-variabl)")); + ATF_REQUIRE_EQ("false", templates.evaluate("defined(the-vecto)")); + ATF_REQUIRE_EQ("true", templates.evaluate("defined(the-variable)")); + ATF_REQUIRE_EQ("true", templates.evaluate("defined(the-vector)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__length__ok); +ATF_TEST_CASE_BODY(templates_def__evaluate__length__ok) +{ + text::templates_def templates; + templates.add_vector("v"); + ATF_REQUIRE_EQ("0", templates.evaluate("length(v)")); + templates.add_to_vector("v", "foo"); + ATF_REQUIRE_EQ("1", templates.evaluate("length(v)")); + templates.add_to_vector("v", "bar"); + ATF_REQUIRE_EQ("2", templates.evaluate("length(v)")); + templates.add_to_vector("v", "baz"); + ATF_REQUIRE_EQ("3", templates.evaluate("length(v)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__length__unknown_vector); +ATF_TEST_CASE_BODY(templates_def__evaluate__length__unknown_vector) +{ + text::templates_def templates; + templates.add_vector("foo1"); + ATF_REQUIRE_THROW_RE(text::syntax_error, "Unknown vector 'foo'", + templates.evaluate("length(foo)")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(templates_def__evaluate__parenthesis_error); +ATF_TEST_CASE_BODY(templates_def__evaluate__parenthesis_error) +{ + text::templates_def templates; + ATF_REQUIRE_THROW_RE(text::syntax_error, + "Expected '\\)' in.*'foo\\(abc'", + templates.evaluate("foo(abc")); + ATF_REQUIRE_THROW_RE(text::syntax_error, + "Unexpected text.*'\\)' in.*'a\\(b\\)c'", + templates.evaluate("a(b)c")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__empty_input); +ATF_TEST_CASE_BODY(instantiate__empty_input) +{ + const text::templates_def templates; + do_test_ok(templates, "", ""); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__value__ok); +ATF_TEST_CASE_BODY(instantiate__value__ok) +{ + const std::string input = + "first line\n" + "%%testvar1%%\n" + "third line\n" + "%%testvar2%% %%testvar3%%%%testvar4%%\n" + "fifth line\n"; + + const std::string exp_output = + "first line\n" + "second line\n" + "third line\n" + "fourth line.\n" + "fifth line\n"; + + text::templates_def templates; + templates.add_variable("testvar1", "second line"); + templates.add_variable("testvar2", "fourth"); + templates.add_variable("testvar3", "line"); + templates.add_variable("testvar4", "."); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__value__unknown_variable); +ATF_TEST_CASE_BODY(instantiate__value__unknown_variable) +{ + const std::string input = + "%%testvar1%%\n"; + + text::templates_def templates; + templates.add_variable("testvar2", "fourth line"); + + do_test_fail(templates, input, "Unknown variable 'testvar1'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_length__ok); +ATF_TEST_CASE_BODY(instantiate__vector_length__ok) +{ + const std::string input = + "%%length(testvector1)%%\n" + "%%length(testvector2)%% - %%length(testvector3)%%\n"; + + const std::string exp_output = + "4\n" + "0 - 1\n"; + + text::templates_def templates; + templates.add_vector("testvector1"); + templates.add_to_vector("testvector1", "000"); + templates.add_to_vector("testvector1", "111"); + templates.add_to_vector("testvector1", "543"); + templates.add_to_vector("testvector1", "999"); + templates.add_vector("testvector2"); + templates.add_vector("testvector3"); + templates.add_to_vector("testvector3", "123"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_length__unknown_vector); +ATF_TEST_CASE_BODY(instantiate__vector_length__unknown_vector) +{ + const std::string input = + "%%length(testvector)%%\n"; + + text::templates_def templates; + templates.add_vector("testvector2"); + + do_test_fail(templates, input, "Unknown vector 'testvector'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_value__ok); +ATF_TEST_CASE_BODY(instantiate__vector_value__ok) +{ + const std::string input = + "first line\n" + "%%testvector1(i)%%\n" + "third line\n" + "%%testvector2(j)%%\n" + "fifth line\n"; + + const std::string exp_output = + "first line\n" + "543\n" + "third line\n" + "123\n" + "fifth line\n"; + + text::templates_def templates; + templates.add_variable("i", "2"); + templates.add_variable("j", "0"); + templates.add_vector("testvector1"); + templates.add_to_vector("testvector1", "000"); + templates.add_to_vector("testvector1", "111"); + templates.add_to_vector("testvector1", "543"); + templates.add_to_vector("testvector1", "999"); + templates.add_vector("testvector2"); + templates.add_to_vector("testvector2", "123"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_value__unknown_vector); +ATF_TEST_CASE_BODY(instantiate__vector_value__unknown_vector) +{ + const std::string input = + "%%testvector(j)%%\n"; + + text::templates_def templates; + templates.add_vector("testvector2"); + + do_test_fail(templates, input, "Unknown vector 'testvector'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_value__out_of_range__empty); +ATF_TEST_CASE_BODY(instantiate__vector_value__out_of_range__empty) +{ + const std::string input = + "%%testvector(j)%%\n"; + + text::templates_def templates; + templates.add_vector("testvector"); + templates.add_variable("j", "0"); + + do_test_fail(templates, input, "Index 'j' out of range at position '0'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__vector_value__out_of_range__not_empty); +ATF_TEST_CASE_BODY(instantiate__vector_value__out_of_range__not_empty) +{ + const std::string input = + "%%testvector(j)%%\n"; + + text::templates_def templates; + templates.add_vector("testvector"); + templates.add_to_vector("testvector", "a"); + templates.add_to_vector("testvector", "b"); + templates.add_variable("j", "2"); + + do_test_fail(templates, input, "Index 'j' out of range at position '2'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__if__one_level__taken); +ATF_TEST_CASE_BODY(instantiate__if__one_level__taken) +{ + const std::string input = + "first line\n" + "%if defined(some_var)\n" + "hello from within the variable conditional\n" + "%endif\n" + "%if defined(some_vector)\n" + "hello from within the vector conditional\n" + "%else\n" + "bye from within the vector conditional\n" + "%endif\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "hello from within the variable conditional\n" + "hello from within the vector conditional\n" + "some more\n"; + + text::templates_def templates; + templates.add_variable("some_var", "zzz"); + templates.add_vector("some_vector"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__if__one_level__not_taken); +ATF_TEST_CASE_BODY(instantiate__if__one_level__not_taken) +{ + const std::string input = + "first line\n" + "%if defined(some_var)\n" + "hello from within the variable conditional\n" + "%endif\n" + "%if defined(some_vector)\n" + "hello from within the vector conditional\n" + "%else\n" + "bye from within the vector conditional\n" + "%endif\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "bye from within the vector conditional\n" + "some more\n"; + + text::templates_def templates; + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__if__multiple_levels__taken); +ATF_TEST_CASE_BODY(instantiate__if__multiple_levels__taken) +{ + const std::string input = + "first line\n" + "%if defined(var1)\n" + "first before\n" + "%if length(var2)\n" + "second before\n" + "%if defined(var3)\n" + "third before\n" + "hello from within the conditional\n" + "third after\n" + "%endif\n" + "second after\n" + "%else\n" + "second after not shown\n" + "%endif\n" + "first after\n" + "%endif\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "first before\n" + "second before\n" + "third before\n" + "hello from within the conditional\n" + "third after\n" + "second after\n" + "first after\n" + "some more\n"; + + text::templates_def templates; + templates.add_variable("var1", "false"); + templates.add_vector("var2"); + templates.add_to_vector("var2", "not-empty"); + templates.add_variable("var3", "foobar"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__if__multiple_levels__not_taken); +ATF_TEST_CASE_BODY(instantiate__if__multiple_levels__not_taken) +{ + const std::string input = + "first line\n" + "%if defined(var1)\n" + "first before\n" + "%if length(var2)\n" + "second before\n" + "%if defined(var3)\n" + "third before\n" + "hello from within the conditional\n" + "third after\n" + "%else\n" + "will not be shown either\n" + "%endif\n" + "second after\n" + "%else\n" + "second after shown\n" + "%endif\n" + "first after\n" + "%endif\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "first before\n" + "second after shown\n" + "first after\n" + "some more\n"; + + text::templates_def templates; + templates.add_variable("var1", "false"); + templates.add_vector("var2"); + templates.add_vector("var3"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__no_iterations); +ATF_TEST_CASE_BODY(instantiate__loop__no_iterations) +{ + const std::string input = + "first line\n" + "%loop table1 i\n" + "hello\n" + "value in vector: %%table1(i)%%\n" + "%if defined(var1)\n" "some other text\n" "%endif\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "some more\n"; + + text::templates_def templates; + templates.add_variable("var1", "defined"); + templates.add_vector("table1"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__multiple_iterations); +ATF_TEST_CASE_BODY(instantiate__loop__multiple_iterations) +{ + const std::string input = + "first line\n" + "%loop table1 i\n" + "hello %%table1(i)%% %%table2(i)%%\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "hello foo1 foo2\n" + "hello bar1 bar2\n" + "some more\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "foo1"); + templates.add_to_vector("table1", "bar1"); + templates.add_vector("table2"); + templates.add_to_vector("table2", "foo2"); + templates.add_to_vector("table2", "bar2"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__nested__no_iterations); +ATF_TEST_CASE_BODY(instantiate__loop__nested__no_iterations) +{ + const std::string input = + "first line\n" + "%loop table1 i\n" + "before: %%table1(i)%%\n" + "%loop table2 j\n" + "before: %%table2(j)%%\n" + "%loop table3 k\n" + "%%table3(k)%%\n" + "%endloop\n" + "after: %%table2(i)%%\n" + "%endloop\n" + "after: %%table1(i)%%\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "before: a\n" + "after: a\n" + "before: b\n" + "after: b\n" + "some more\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "a"); + templates.add_to_vector("table1", "b"); + templates.add_vector("table2"); + templates.add_vector("table3"); + templates.add_to_vector("table3", "1"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__nested__multiple_iterations); +ATF_TEST_CASE_BODY(instantiate__loop__nested__multiple_iterations) +{ + const std::string input = + "first line\n" + "%loop table1 i\n" + "%loop table2 j\n" + "%%table1(i)%% %%table2(j)%%\n" + "%endloop\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "a 1\n" + "a 2\n" + "a 3\n" + "b 1\n" + "b 2\n" + "b 3\n" + "some more\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "a"); + templates.add_to_vector("table1", "b"); + templates.add_vector("table2"); + templates.add_to_vector("table2", "1"); + templates.add_to_vector("table2", "2"); + templates.add_to_vector("table2", "3"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__sequential); +ATF_TEST_CASE_BODY(instantiate__loop__sequential) +{ + const std::string input = + "first line\n" + "%loop table1 iter\n" + "1: %%table1(iter)%%\n" + "%endloop\n" + "divider\n" + "%loop table2 iter\n" + "2: %%table2(iter)%%\n" + "%endloop\n" + "divider\n" + "%loop table3 iter\n" + "3: %%table3(iter)%%\n" + "%endloop\n" + "divider\n" + "%loop table4 iter\n" + "4: %%table4(iter)%%\n" + "%endloop\n" + "some more\n"; + + const std::string exp_output = + "first line\n" + "1: a\n" + "1: b\n" + "divider\n" + "divider\n" + "divider\n" + "4: 1\n" + "4: 2\n" + "4: 3\n" + "some more\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "a"); + templates.add_to_vector("table1", "b"); + templates.add_vector("table2"); + templates.add_vector("table3"); + templates.add_vector("table4"); + templates.add_to_vector("table4", "1"); + templates.add_to_vector("table4", "2"); + templates.add_to_vector("table4", "3"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__loop__scoping); +ATF_TEST_CASE_BODY(instantiate__loop__scoping) +{ + const std::string input = + "%loop table1 i\n" + "%if defined(i)\n" "i defined inside scope 1\n" "%endif\n" + "%loop table2 j\n" + "%if defined(i)\n" "i defined inside scope 2\n" "%endif\n" + "%if defined(j)\n" "j defined inside scope 2\n" "%endif\n" + "%endloop\n" + "%if defined(j)\n" "j defined inside scope 1\n" "%endif\n" + "%endloop\n" + "%if defined(i)\n" "i defined outside\n" "%endif\n" + "%if defined(j)\n" "j defined outside\n" "%endif\n"; + + const std::string exp_output = + "i defined inside scope 1\n" + "i defined inside scope 2\n" + "j defined inside scope 2\n" + "i defined inside scope 1\n" + "i defined inside scope 2\n" + "j defined inside scope 2\n"; + + text::templates_def templates; + templates.add_vector("table1"); + templates.add_to_vector("table1", "first"); + templates.add_to_vector("table1", "second"); + templates.add_vector("table2"); + templates.add_to_vector("table2", "first"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__mismatched_delimiters); +ATF_TEST_CASE_BODY(instantiate__mismatched_delimiters) +{ + const std::string input = + "this is some %% text\n" + "and this is %%var%% text%%\n"; + + const std::string exp_output = + "this is some %% text\n" + "and this is some more text%%\n"; + + text::templates_def templates; + templates.add_variable("var", "some more"); + + do_test_ok(templates, input, exp_output); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__empty_statement); +ATF_TEST_CASE_BODY(instantiate__empty_statement) +{ + do_test_fail(text::templates_def(), "%\n", "Empty statement"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__unknown_statement); +ATF_TEST_CASE_BODY(instantiate__unknown_statement) +{ + do_test_fail(text::templates_def(), "%if2\n", "Unknown statement 'if2'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__invalid_narguments); +ATF_TEST_CASE_BODY(instantiate__invalid_narguments) +{ + do_test_fail(text::templates_def(), "%if a b\n", + "Invalid number of arguments for statement 'if'"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__files__ok); +ATF_TEST_CASE_BODY(instantiate__files__ok) +{ + text::templates_def templates; + templates.add_variable("string", "Hello, world!"); + + atf::utils::create_file("input.txt", "The string is: %%string%%\n"); + + text::instantiate(templates, fs::path("input.txt"), fs::path("output.txt")); + + std::ifstream output("output.txt"); + std::string line; + ATF_REQUIRE(std::getline(output, line).good()); + ATF_REQUIRE_EQ(line, "The string is: Hello, world!"); + ATF_REQUIRE(std::getline(output, line).eof()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(instantiate__files__input_error); +ATF_TEST_CASE_BODY(instantiate__files__input_error) +{ + text::templates_def templates; + ATF_REQUIRE_THROW_RE(text::error, "Failed to open input.txt for read", + text::instantiate(templates, fs::path("input.txt"), + fs::path("output.txt"))); +} + + +ATF_TEST_CASE(instantiate__files__output_error); +ATF_TEST_CASE_HEAD(instantiate__files__output_error) +{ + set_md_var("require.user", "unprivileged"); +} +ATF_TEST_CASE_BODY(instantiate__files__output_error) +{ + text::templates_def templates; + + atf::utils::create_file("input.txt", ""); + + fs::mkdir(fs::path("dir"), 0444); + + ATF_REQUIRE_THROW_RE(text::error, "Failed to open dir/output.txt for write", + text::instantiate(templates, fs::path("input.txt"), + fs::path("dir/output.txt"))); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, templates_def__add_variable__first); + ATF_ADD_TEST_CASE(tcs, templates_def__add_variable__replace); + ATF_ADD_TEST_CASE(tcs, templates_def__remove_variable); + ATF_ADD_TEST_CASE(tcs, templates_def__add_vector__first); + ATF_ADD_TEST_CASE(tcs, templates_def__add_vector__replace); + ATF_ADD_TEST_CASE(tcs, templates_def__add_to_vector); + ATF_ADD_TEST_CASE(tcs, templates_def__exists__variable); + ATF_ADD_TEST_CASE(tcs, templates_def__exists__vector); + ATF_ADD_TEST_CASE(tcs, templates_def__get_variable__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__get_variable__unknown); + ATF_ADD_TEST_CASE(tcs, templates_def__get_vector__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__get_vector__unknown); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__variable__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__variable__unknown); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__vector__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__vector__unknown_vector); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__vector__unknown_index); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__vector__out_of_range); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__defined); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__length__ok); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__length__unknown_vector); + ATF_ADD_TEST_CASE(tcs, templates_def__evaluate__parenthesis_error); + + ATF_ADD_TEST_CASE(tcs, instantiate__empty_input); + ATF_ADD_TEST_CASE(tcs, instantiate__value__ok); + ATF_ADD_TEST_CASE(tcs, instantiate__value__unknown_variable); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_length__ok); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_length__unknown_vector); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_value__ok); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_value__unknown_vector); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_value__out_of_range__empty); + ATF_ADD_TEST_CASE(tcs, instantiate__vector_value__out_of_range__not_empty); + ATF_ADD_TEST_CASE(tcs, instantiate__if__one_level__taken); + ATF_ADD_TEST_CASE(tcs, instantiate__if__one_level__not_taken); + ATF_ADD_TEST_CASE(tcs, instantiate__if__multiple_levels__taken); + ATF_ADD_TEST_CASE(tcs, instantiate__if__multiple_levels__not_taken); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__no_iterations); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__multiple_iterations); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__nested__no_iterations); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__nested__multiple_iterations); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__sequential); + ATF_ADD_TEST_CASE(tcs, instantiate__loop__scoping); + ATF_ADD_TEST_CASE(tcs, instantiate__mismatched_delimiters); + ATF_ADD_TEST_CASE(tcs, instantiate__empty_statement); + ATF_ADD_TEST_CASE(tcs, instantiate__unknown_statement); + ATF_ADD_TEST_CASE(tcs, instantiate__invalid_narguments); + + ATF_ADD_TEST_CASE(tcs, instantiate__files__ok); + ATF_ADD_TEST_CASE(tcs, instantiate__files__input_error); + ATF_ADD_TEST_CASE(tcs, instantiate__files__output_error); +} diff --git a/utils/units.cpp b/utils/units.cpp new file mode 100644 index 000000000000..bfb488fa2ed6 --- /dev/null +++ b/utils/units.cpp @@ -0,0 +1,172 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/units.hpp" + +extern "C" { +#include +} + +#include + +#include "utils/format/macros.hpp" +#include "utils/text/exceptions.hpp" +#include "utils/text/operations.ipp" + +namespace units = utils::units; + + +/// Constructs a zero bytes quantity. +units::bytes::bytes(void) : + _count(0) +{ +} + + +/// Constructs an arbitrary bytes quantity. +/// +/// \param count_ The amount of bytes in the quantity. +units::bytes::bytes(const uint64_t count_) : + _count(count_) +{ +} + + +/// Parses a string into a bytes quantity. +/// +/// \param in_str The user-provided string to be converted. +/// +/// \return The converted bytes quantity. +/// +/// \throw std::runtime_error If the input string is empty or invalid. +units::bytes +units::bytes::parse(const std::string& in_str) +{ + if (in_str.empty()) + throw std::runtime_error("Bytes quantity cannot be empty"); + + uint64_t multiplier; + std::string str = in_str; + { + const char unit = str[str.length() - 1]; + switch (unit) { + case 'T': case 't': multiplier = TB; break; + case 'G': case 'g': multiplier = GB; break; + case 'M': case 'm': multiplier = MB; break; + case 'K': case 'k': multiplier = KB; break; + default: multiplier = 1; + } + if (multiplier != 1) + str.erase(str.length() - 1); + } + + if (str.empty()) + throw std::runtime_error("Bytes quantity cannot be empty"); + if (str[0] == '.' || str[str.length() - 1] == '.') { + // The standard parser for float values accepts things like ".3" and + // "3.", which means that we would interpret ".3K" and "3.K" as valid + // quantities. I think this is ugly and should not be allowed, so + // special-case this condition and just error out. + throw std::runtime_error(F("Invalid bytes quantity '%s'") % in_str); + } + + double count; + try { + count = text::to_type< double >(str); + } catch (const text::value_error& e) { + throw std::runtime_error(F("Invalid bytes quantity '%s'") % in_str); + } + + return bytes(uint64_t(count * multiplier)); +} + + +/// Formats a bytes quantity for user consumption. +/// +/// \return A textual representation of the bytes quantiy. +std::string +units::bytes::format(void) const +{ + if (_count >= TB) { + return F("%.2sT") % (static_cast< float >(_count) / TB); + } else if (_count >= GB) { + return F("%.2sG") % (static_cast< float >(_count) / GB); + } else if (_count >= MB) { + return F("%.2sM") % (static_cast< float >(_count) / MB); + } else if (_count >= KB) { + return F("%.2sK") % (static_cast< float >(_count) / KB); + } else { + return F("%s") % _count; + } +} + + +/// Implicit conversion to an integral representation. +units::bytes::operator uint64_t(void) const +{ + return _count; +} + + +/// Extracts a bytes quantity from a stream. +/// +/// \param input The stream from which to read a single word representing the +/// bytes quantity. +/// \param rhs The variable into which to store the parsed value. +/// +/// \return The input stream. +/// +/// \post The bad bit of input is set to 1 if the parsing failed. +std::istream& +units::operator>>(std::istream& input, bytes& rhs) +{ + std::string word; + input >> word; + if (input.good() || input.eof()) { + try { + rhs = bytes::parse(word); + } catch (const std::runtime_error& e) { + input.setstate(std::ios::badbit); + } + } + return input; +} + + +/// Injects a bytes quantity into a stream. +/// +/// \param output The stream into which to inject the bytes quantity as a +/// user-readable string. +/// \param rhs The bytes quantity to format. +/// +/// \return The output stream. +std::ostream& +units::operator<<(std::ostream& output, const bytes& rhs) +{ + return (output << rhs.format()); +} diff --git a/utils/units.hpp b/utils/units.hpp new file mode 100644 index 000000000000..281788c3199f --- /dev/null +++ b/utils/units.hpp @@ -0,0 +1,96 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/units.hpp +/// Formatters and parsers of user-friendly units. + +#if !defined(UTILS_UNITS_HPP) +#define UTILS_UNITS_HPP + +#include "utils/units_fwd.hpp" + +extern "C" { +#include +} + +#include +#include +#include + +namespace utils { +namespace units { + + +namespace { + +/// Constant representing 1 kilobyte. +const uint64_t KB = int64_t(1) << 10; + +/// Constant representing 1 megabyte. +const uint64_t MB = int64_t(1) << 20; + +/// Constant representing 1 gigabyte. +const uint64_t GB = int64_t(1) << 30; + +/// Constant representing 1 terabyte. +const uint64_t TB = int64_t(1) << 40; + +} // anonymous namespace + + +/// Representation of a bytes quantity. +/// +/// The purpose of this class is to represent an amount of bytes in a semantic +/// manner, and to provide functions to format such numbers for nice user +/// presentation and to parse back such numbers. +/// +/// The input follows this regular expression: [0-9]+(|\.[0-9]+)[GgKkMmTt]? +/// The output follows this regular expression: [0-9]+\.[0-9]{3}[GKMT]? +class bytes { + /// Raw representation, in bytes, of the quantity. + uint64_t _count; + +public: + bytes(void); + explicit bytes(const uint64_t); + + static bytes parse(const std::string&); + std::string format(void) const; + + operator uint64_t(void) const; +}; + + +std::istream& operator>>(std::istream&, bytes&); +std::ostream& operator<<(std::ostream&, const bytes&); + + +} // namespace units +} // namespace utils + +#endif // !defined(UTILS_UNITS_HPP) diff --git a/utils/units_fwd.hpp b/utils/units_fwd.hpp new file mode 100644 index 000000000000..3653d9727a2d --- /dev/null +++ b/utils/units_fwd.hpp @@ -0,0 +1,45 @@ +// Copyright 2015 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/units_fwd.hpp +/// Forward declarations for utils/units.hpp + +#if !defined(UTILS_UNITS_FWD_HPP) +#define UTILS_UNITS_FWD_HPP + +namespace utils { +namespace units { + + +class bytes; + + +} // namespace units +} // namespace utils + +#endif // !defined(UTILS_UNITS_FWD_HPP) diff --git a/utils/units_test.cpp b/utils/units_test.cpp new file mode 100644 index 000000000000..601265c95b49 --- /dev/null +++ b/utils/units_test.cpp @@ -0,0 +1,248 @@ +// Copyright 2012 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/units.hpp" + +#include +#include + +#include + +namespace units = utils::units; + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__tb); +ATF_TEST_CASE_BODY(bytes__format__tb) +{ + using units::TB; + using units::GB; + + ATF_REQUIRE_EQ("2.00T", units::bytes(2 * TB).format()); + ATF_REQUIRE_EQ("45.12T", units::bytes(45 * TB + 120 * GB).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__gb); +ATF_TEST_CASE_BODY(bytes__format__gb) +{ + using units::GB; + using units::MB; + + ATF_REQUIRE_EQ("5.00G", units::bytes(5 * GB).format()); + ATF_REQUIRE_EQ("745.96G", units::bytes(745 * GB + 980 * MB).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__mb); +ATF_TEST_CASE_BODY(bytes__format__mb) +{ + using units::MB; + using units::KB; + + ATF_REQUIRE_EQ("1.00M", units::bytes(1 * MB).format()); + ATF_REQUIRE_EQ("1023.50M", units::bytes(1023 * MB + 512 * KB).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__kb); +ATF_TEST_CASE_BODY(bytes__format__kb) +{ + using units::KB; + + ATF_REQUIRE_EQ("3.00K", units::bytes(3 * KB).format()); + ATF_REQUIRE_EQ("1.33K", units::bytes(1 * KB + 340).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__format__b); +ATF_TEST_CASE_BODY(bytes__format__b) +{ + ATF_REQUIRE_EQ("0", units::bytes().format()); + ATF_REQUIRE_EQ("0", units::bytes(0).format()); + ATF_REQUIRE_EQ("1023", units::bytes(1023).format()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__tb); +ATF_TEST_CASE_BODY(bytes__parse__tb) +{ + using units::TB; + using units::GB; + + ATF_REQUIRE_EQ(0, units::bytes::parse("0T")); + ATF_REQUIRE_EQ(units::bytes(TB), units::bytes::parse("1T")); + ATF_REQUIRE_EQ(units::bytes(TB), units::bytes::parse("1t")); + ATF_REQUIRE_EQ(13567973486755LL, units::bytes::parse("12.340000T")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__gb); +ATF_TEST_CASE_BODY(bytes__parse__gb) +{ + using units::GB; + using units::MB; + + ATF_REQUIRE_EQ(0, units::bytes::parse("0G")); + ATF_REQUIRE_EQ(units::bytes(GB), units::bytes::parse("1G")); + ATF_REQUIRE_EQ(units::bytes(GB), units::bytes::parse("1g")); + ATF_REQUIRE_EQ(13249974108LL, units::bytes::parse("12.340G")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__mb); +ATF_TEST_CASE_BODY(bytes__parse__mb) +{ + using units::MB; + using units::KB; + + ATF_REQUIRE_EQ(0, units::bytes::parse("0M")); + ATF_REQUIRE_EQ(units::bytes(MB), units::bytes::parse("1M")); + ATF_REQUIRE_EQ(units::bytes(MB), units::bytes::parse("1m")); + ATF_REQUIRE_EQ(12939427, units::bytes::parse("12.34000M")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__kb); +ATF_TEST_CASE_BODY(bytes__parse__kb) +{ + using units::KB; + + ATF_REQUIRE_EQ(0, units::bytes::parse("0K")); + ATF_REQUIRE_EQ(units::bytes(KB), units::bytes::parse("1K")); + ATF_REQUIRE_EQ(units::bytes(KB), units::bytes::parse("1k")); + ATF_REQUIRE_EQ(12636, units::bytes::parse("12.34K")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__b); +ATF_TEST_CASE_BODY(bytes__parse__b) +{ + ATF_REQUIRE_EQ(0, units::bytes::parse("0")); + ATF_REQUIRE_EQ(89, units::bytes::parse("89")); + ATF_REQUIRE_EQ(1234, units::bytes::parse("1234")); + ATF_REQUIRE_EQ(1234567890, units::bytes::parse("1234567890")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__parse__error); +ATF_TEST_CASE_BODY(bytes__parse__error) +{ + ATF_REQUIRE_THROW_RE(std::runtime_error, "empty", units::bytes::parse("")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "empty", units::bytes::parse("k")); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'.'", + units::bytes::parse(".")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'3.'", + units::bytes::parse("3.")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'.3'", + units::bytes::parse(".3")); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*' t'", + units::bytes::parse(" t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'.t'", + units::bytes::parse(".t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'12 t'", + units::bytes::parse("12 t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'12.t'", + units::bytes::parse("12.t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'.12t'", + units::bytes::parse(".12t")); + ATF_REQUIRE_THROW_RE(std::runtime_error, "Invalid.*'abt'", + units::bytes::parse("abt")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__istream__one_word); +ATF_TEST_CASE_BODY(bytes__istream__one_word) +{ + std::istringstream input("12M"); + + units::bytes bytes; + input >> bytes; + ATF_REQUIRE(input.eof()); + ATF_REQUIRE_EQ(units::bytes(12 * units::MB), bytes); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__istream__many_words); +ATF_TEST_CASE_BODY(bytes__istream__many_words) +{ + std::istringstream input("12M more"); + + units::bytes bytes; + input >> bytes; + ATF_REQUIRE(input.good()); + ATF_REQUIRE_EQ(units::bytes(12 * units::MB), bytes); + + std::string word; + input >> word; + ATF_REQUIRE_EQ("more", word); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__istream__error); +ATF_TEST_CASE_BODY(bytes__istream__error) +{ + std::istringstream input("12.M more"); + + units::bytes bytes(123456789); + input >> bytes; + ATF_REQUIRE(input.bad()); + ATF_REQUIRE_EQ(units::bytes(123456789), bytes); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(bytes__ostream); +ATF_TEST_CASE_BODY(bytes__ostream) +{ + std::ostringstream output; + output << "foo " << units::bytes(5 * units::KB) << " bar"; + ATF_REQUIRE_EQ("foo 5.00K bar", output.str()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, bytes__format__tb); + ATF_ADD_TEST_CASE(tcs, bytes__format__gb); + ATF_ADD_TEST_CASE(tcs, bytes__format__mb); + ATF_ADD_TEST_CASE(tcs, bytes__format__kb); + ATF_ADD_TEST_CASE(tcs, bytes__format__b); + + ATF_ADD_TEST_CASE(tcs, bytes__parse__tb); + ATF_ADD_TEST_CASE(tcs, bytes__parse__gb); + ATF_ADD_TEST_CASE(tcs, bytes__parse__mb); + ATF_ADD_TEST_CASE(tcs, bytes__parse__kb); + ATF_ADD_TEST_CASE(tcs, bytes__parse__b); + ATF_ADD_TEST_CASE(tcs, bytes__parse__error); + + ATF_ADD_TEST_CASE(tcs, bytes__istream__one_word); + ATF_ADD_TEST_CASE(tcs, bytes__istream__many_words); + ATF_ADD_TEST_CASE(tcs, bytes__istream__error); + ATF_ADD_TEST_CASE(tcs, bytes__ostream); +}