summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrooks Davis <brooks@FreeBSD.org>2020-03-17 16:56:50 +0000
committerBrooks Davis <brooks@FreeBSD.org>2020-03-17 16:56:50 +0000
commit08334c51dbb99d9ecd2bb86a2d94ed06da9e167a (patch)
treec43eb24d59bd5c963583a5190caef80fc8387322
downloadsrc-test2-vendor/kyua.tar.gz
src-test2-vendor/kyua.zip
Import the kyua testing framework for infrastructure softwarevendor/kyua/0.13-a685f91vendor/kyua
Imported at 0.13 plus assumulated changes to git hash a685f91. Obtained from: https://github.com/jmmv/kyua Sponsored by: DARPA
Notes
Notes: svn path=/vendor/kyua/dist/; revision=359042 svn path=/vendor/kyua/0.13-a685f91/; revision=359043; tag=vendor/kyua/0.13-a685f91
-rw-r--r--.gitignore23
-rw-r--r--.travis.yml49
-rw-r--r--AUTHORS11
-rw-r--r--CONTRIBUTING.md173
-rw-r--r--CONTRIBUTORS20
-rw-r--r--Doxyfile.in59
-rw-r--r--INSTALL.md268
-rw-r--r--Kyuafile18
-rw-r--r--LICENSE27
-rw-r--r--Makefile.am186
-rw-r--r--NEWS.md622
-rw-r--r--README.md84
-rw-r--r--admin/.gitignore6
-rw-r--r--admin/Makefile.am.inc41
-rwxr-xr-xadmin/build-bintray-dist.sh131
-rw-r--r--admin/check-api-docs.awk72
-rw-r--r--admin/check-style-common.awk79
-rw-r--r--admin/check-style-cpp.awk87
-rw-r--r--admin/check-style-make.awk71
-rw-r--r--admin/check-style-man.awk71
-rw-r--r--admin/check-style-shell.awk95
-rwxr-xr-xadmin/check-style.sh170
-rwxr-xr-xadmin/clean-all.sh90
-rwxr-xr-xadmin/travis-build.sh98
-rwxr-xr-xadmin/travis-install-deps.sh83
-rw-r--r--bootstrap/.gitignore4
-rw-r--r--bootstrap/Kyuafile5
-rw-r--r--bootstrap/Makefile.am.inc90
-rw-r--r--bootstrap/atf_helpers.cpp71
-rw-r--r--bootstrap/plain_helpers.cpp141
-rw-r--r--bootstrap/testsuite.at200
-rw-r--r--cli/Kyuafile14
-rw-r--r--cli/Makefile.am.inc123
-rw-r--r--cli/cmd_about.cpp160
-rw-r--r--cli/cmd_about.hpp57
-rw-r--r--cli/cmd_about_test.cpp306
-rw-r--r--cli/cmd_config.cpp122
-rw-r--r--cli/cmd_config.hpp54
-rw-r--r--cli/cmd_config_test.cpp144
-rw-r--r--cli/cmd_db_exec.cpp200
-rw-r--r--cli/cmd_db_exec.hpp61
-rw-r--r--cli/cmd_db_exec_test.cpp165
-rw-r--r--cli/cmd_db_migrate.cpp82
-rw-r--r--cli/cmd_db_migrate.hpp54
-rw-r--r--cli/cmd_debug.cpp94
-rw-r--r--cli/cmd_debug.hpp54
-rw-r--r--cli/cmd_debug_test.cpp82
-rw-r--r--cli/cmd_help.cpp250
-rw-r--r--cli/cmd_help.hpp62
-rw-r--r--cli/cmd_help_test.cpp347
-rw-r--r--cli/cmd_list.cpp161
-rw-r--r--cli/cmd_list.hpp65
-rw-r--r--cli/cmd_list_test.cpp112
-rw-r--r--cli/cmd_report.cpp421
-rw-r--r--cli/cmd_report.hpp54
-rw-r--r--cli/cmd_report_html.cpp474
-rw-r--r--cli/cmd_report_html.hpp55
-rw-r--r--cli/cmd_report_junit.cpp89
-rw-r--r--cli/cmd_report_junit.hpp54
-rw-r--r--cli/cmd_test.cpp186
-rw-r--r--cli/cmd_test.hpp54
-rw-r--r--cli/cmd_test_test.cpp63
-rw-r--r--cli/common.cpp411
-rw-r--r--cli/common.hpp104
-rw-r--r--cli/common.ipp30
-rw-r--r--cli/common_test.cpp488
-rw-r--r--cli/config.cpp223
-rw-r--r--cli/config.hpp55
-rw-r--r--cli/config_test.cpp351
-rw-r--r--cli/main.cpp356
-rw-r--r--cli/main.hpp61
-rw-r--r--cli/main_test.cpp489
-rw-r--r--configure.ac173
-rw-r--r--doc/.gitignore14
-rw-r--r--doc/Kyuafile5
-rw-r--r--doc/Makefile.am.inc152
-rw-r--r--doc/build-root.mdoc104
-rw-r--r--doc/kyua-about.1.in95
-rw-r--r--doc/kyua-config.1.in59
-rw-r--r--doc/kyua-db-exec.1.in80
-rw-r--r--doc/kyua-db-migrate.1.in63
-rw-r--r--doc/kyua-debug.1.in145
-rw-r--r--doc/kyua-help.1.in64
-rw-r--r--doc/kyua-list.1.in90
-rw-r--r--doc/kyua-report-html.1.in103
-rw-r--r--doc/kyua-report-junit.1.in87
-rw-r--r--doc/kyua-report.1.in118
-rw-r--r--doc/kyua-test.1.in102
-rw-r--r--doc/kyua.1.in400
-rw-r--r--doc/kyua.conf.5.in141
-rw-r--r--doc/kyuafile.5.in407
-rwxr-xr-xdoc/manbuild.sh171
-rwxr-xr-xdoc/manbuild_test.sh235
-rw-r--r--doc/results-file-flag-read.mdoc53
-rw-r--r--doc/results-file-flag-write.mdoc46
-rw-r--r--doc/results-files-report-example.mdoc32
-rw-r--r--doc/results-files.mdoc68
-rw-r--r--doc/test-filters.mdoc40
-rw-r--r--doc/test-isolation.mdoc112
-rw-r--r--drivers/Kyuafile7
-rw-r--r--drivers/Makefile.am.inc72
-rw-r--r--drivers/debug_test.cpp109
-rw-r--r--drivers/debug_test.hpp79
-rw-r--r--drivers/list_tests.cpp84
-rw-r--r--drivers/list_tests.hpp92
-rw-r--r--drivers/list_tests_helpers.cpp98
-rw-r--r--drivers/list_tests_test.cpp287
-rw-r--r--drivers/report_junit.cpp258
-rw-r--r--drivers/report_junit.hpp75
-rw-r--r--drivers/report_junit_test.cpp415
-rw-r--r--drivers/run_tests.cpp344
-rw-r--r--drivers/run_tests.hpp106
-rw-r--r--drivers/scan_results.cpp107
-rw-r--r--drivers/scan_results.hpp105
-rw-r--r--drivers/scan_results_test.cpp258
-rw-r--r--engine/Kyuafile17
-rw-r--r--engine/Makefile.am.inc155
-rw-r--r--engine/atf.cpp242
-rw-r--r--engine/atf.hpp72
-rw-r--r--engine/atf_helpers.cpp414
-rw-r--r--engine/atf_list.cpp196
-rw-r--r--engine/atf_list.hpp51
-rw-r--r--engine/atf_list_test.cpp278
-rw-r--r--engine/atf_result.cpp642
-rw-r--r--engine/atf_result.hpp114
-rw-r--r--engine/atf_result_fwd.hpp43
-rw-r--r--engine/atf_result_test.cpp788
-rw-r--r--engine/atf_test.cpp450
-rw-r--r--engine/config.cpp254
-rw-r--r--engine/config.hpp65
-rw-r--r--engine/config_fwd.hpp43
-rw-r--r--engine/config_test.cpp203
-rw-r--r--engine/exceptions.cpp81
-rw-r--r--engine/exceptions.hpp75
-rw-r--r--engine/exceptions_test.cpp69
-rw-r--r--engine/filters.cpp389
-rw-r--r--engine/filters.hpp134
-rw-r--r--engine/filters_fwd.hpp45
-rw-r--r--engine/filters_test.cpp594
-rw-r--r--engine/kyuafile.cpp694
-rw-r--r--engine/kyuafile.hpp96
-rw-r--r--engine/kyuafile_fwd.hpp43
-rw-r--r--engine/kyuafile_test.cpp606
-rw-r--r--engine/plain.cpp143
-rw-r--r--engine/plain.hpp67
-rw-r--r--engine/plain_helpers.cpp238
-rw-r--r--engine/plain_test.cpp207
-rw-r--r--engine/requirements.cpp293
-rw-r--r--engine/requirements.hpp51
-rw-r--r--engine/requirements_test.cpp511
-rw-r--r--engine/scanner.cpp216
-rw-r--r--engine/scanner.hpp76
-rw-r--r--engine/scanner_fwd.hpp59
-rw-r--r--engine/scanner_test.cpp476
-rw-r--r--engine/scheduler.cpp1373
-rw-r--r--engine/scheduler.hpp282
-rw-r--r--engine/scheduler_fwd.hpp61
-rw-r--r--engine/scheduler_test.cpp1242
-rw-r--r--engine/tap.cpp191
-rw-r--r--engine/tap.hpp67
-rw-r--r--engine/tap_helpers.cpp202
-rw-r--r--engine/tap_parser.cpp438
-rw-r--r--engine/tap_parser.hpp99
-rw-r--r--engine/tap_parser_fwd.hpp50
-rw-r--r--engine/tap_parser_test.cpp465
-rw-r--r--engine/tap_test.cpp218
-rw-r--r--examples/Kyuafile5
-rw-r--r--examples/Kyuafile.top52
-rw-r--r--examples/Makefile.am.inc45
-rw-r--r--examples/kyua.conf69
-rw-r--r--examples/syntax_test.cpp210
-rw-r--r--integration/Kyuafile16
-rw-r--r--integration/Makefile.am.inc150
-rwxr-xr-xintegration/cmd_about_test.sh158
-rwxr-xr-xintegration/cmd_config_test.sh355
-rwxr-xr-xintegration/cmd_db_exec_test.sh165
-rwxr-xr-xintegration/cmd_db_migrate_test.sh167
-rwxr-xr-xintegration/cmd_debug_test.sh421
-rwxr-xr-xintegration/cmd_help_test.sh93
-rwxr-xr-xintegration/cmd_list_test.sh600
-rwxr-xr-xintegration/cmd_report_html_test.sh267
-rwxr-xr-xintegration/cmd_report_junit_test.sh300
-rwxr-xr-xintegration/cmd_report_test.sh381
-rwxr-xr-xintegration/cmd_test_test.sh1071
-rwxr-xr-xintegration/global_test.sh146
-rw-r--r--integration/helpers/.gitignore11
-rw-r--r--integration/helpers/Makefile.am.inc90
-rw-r--r--integration/helpers/bad_test_program.cpp50
-rw-r--r--integration/helpers/bogus_test_cases.cpp64
-rw-r--r--integration/helpers/config.cpp58
-rw-r--r--integration/helpers/dump_env.cpp74
-rw-r--r--integration/helpers/expect_all_pass.cpp92
-rw-r--r--integration/helpers/expect_some_fail.cpp94
-rw-r--r--integration/helpers/interrupts.cpp62
-rw-r--r--integration/helpers/metadata.cpp95
-rw-r--r--integration/helpers/race.cpp99
-rw-r--r--integration/helpers/simple_all_pass.cpp55
-rw-r--r--integration/helpers/simple_some_fail.cpp53
-rwxr-xr-xintegration/utils.sh177
-rw-r--r--m4/ax_cxx_compile_stdcxx.m4951
-rw-r--r--m4/compiler-features.m4122
-rw-r--r--m4/compiler-flags.m4169
-rw-r--r--m4/developer-mode.m4112
-rw-r--r--m4/doxygen.m462
-rw-r--r--m4/fs.m4125
-rw-r--r--m4/getopt.m4213
-rw-r--r--m4/memory.m4122
-rw-r--r--m4/signals.m492
-rw-r--r--m4/uname.m463
-rw-r--r--main.cpp50
-rw-r--r--misc/Makefile.am.inc32
-rw-r--r--misc/context.html55
-rw-r--r--misc/index.html187
-rw-r--r--misc/report.css78
-rw-r--r--misc/test_result.html76
-rw-r--r--model/Kyuafile10
-rw-r--r--model/Makefile.am.inc89
-rw-r--r--model/README11
-rw-r--r--model/context.cpp159
-rw-r--r--model/context.hpp76
-rw-r--r--model/context_fwd.hpp43
-rw-r--r--model/context_test.cpp106
-rw-r--r--model/exceptions.cpp76
-rw-r--r--model/exceptions.hpp71
-rw-r--r--model/exceptions_test.cpp65
-rw-r--r--model/metadata.cpp1068
-rw-r--r--model/metadata.hpp130
-rw-r--r--model/metadata_fwd.hpp44
-rw-r--r--model/metadata_test.cpp461
-rw-r--r--model/test_case.cpp339
-rw-r--r--model/test_case.hpp98
-rw-r--r--model/test_case_fwd.hpp51
-rw-r--r--model/test_case_test.cpp263
-rw-r--r--model/test_program.cpp452
-rw-r--r--model/test_program.hpp110
-rw-r--r--model/test_program_fwd.hpp55
-rw-r--r--model/test_program_test.cpp711
-rw-r--r--model/test_result.cpp142
-rw-r--r--model/test_result.hpp79
-rw-r--r--model/test_result_fwd.hpp53
-rw-r--r--model/test_result_test.cpp185
-rw-r--r--model/types.hpp61
-rw-r--r--store/Kyuafile15
-rw-r--r--store/Makefile.am.inc145
-rw-r--r--store/dbtypes.cpp255
-rw-r--r--store/dbtypes.hpp68
-rw-r--r--store/dbtypes_test.cpp234
-rw-r--r--store/exceptions.cpp88
-rw-r--r--store/exceptions.hpp72
-rw-r--r--store/exceptions_test.cpp65
-rw-r--r--store/layout.cpp264
-rw-r--r--store/layout.hpp84
-rw-r--r--store/layout_fwd.hpp54
-rw-r--r--store/layout_test.cpp350
-rw-r--r--store/metadata.cpp137
-rw-r--r--store/metadata.hpp68
-rw-r--r--store/metadata_fwd.hpp43
-rw-r--r--store/metadata_test.cpp154
-rw-r--r--store/migrate.cpp287
-rw-r--r--store/migrate.hpp55
-rw-r--r--store/migrate_test.cpp132
-rw-r--r--store/migrate_v1_v2.sql357
-rw-r--r--store/migrate_v2_v3.sql120
-rw-r--r--store/read_backend.cpp160
-rw-r--r--store/read_backend.hpp77
-rw-r--r--store/read_backend_fwd.hpp43
-rw-r--r--store/read_backend_test.cpp152
-rw-r--r--store/read_transaction.cpp532
-rw-r--r--store/read_transaction.hpp120
-rw-r--r--store/read_transaction_fwd.hpp44
-rw-r--r--store/read_transaction_test.cpp262
-rw-r--r--store/schema_inttest.cpp492
-rw-r--r--store/schema_v1.sql314
-rw-r--r--store/schema_v2.sql293
-rw-r--r--store/schema_v3.sql255
-rw-r--r--store/testdata_v1.sql330
-rw-r--r--store/testdata_v2.sql462
-rw-r--r--store/testdata_v3_1.sql42
-rw-r--r--store/testdata_v3_2.sql190
-rw-r--r--store/testdata_v3_3.sql171
-rw-r--r--store/testdata_v3_4.sql141
-rw-r--r--store/transaction_test.cpp170
-rw-r--r--store/write_backend.cpp208
-rw-r--r--store/write_backend.hpp81
-rw-r--r--store/write_backend_fwd.hpp52
-rw-r--r--store/write_backend_test.cpp204
-rw-r--r--store/write_transaction.cpp440
-rw-r--r--store/write_transaction.hpp89
-rw-r--r--store/write_transaction_fwd.hpp43
-rw-r--r--store/write_transaction_test.cpp416
-rw-r--r--utils/.gitignore2
-rw-r--r--utils/Kyuafile24
-rw-r--r--utils/Makefile.am.inc133
-rw-r--r--utils/auto_array.hpp102
-rw-r--r--utils/auto_array.ipp227
-rw-r--r--utils/auto_array_fwd.hpp43
-rw-r--r--utils/auto_array_test.cpp312
-rw-r--r--utils/cmdline/Kyuafile11
-rw-r--r--utils/cmdline/Makefile.am.inc96
-rw-r--r--utils/cmdline/base_command.cpp201
-rw-r--r--utils/cmdline/base_command.hpp162
-rw-r--r--utils/cmdline/base_command.ipp104
-rw-r--r--utils/cmdline/base_command_fwd.hpp47
-rw-r--r--utils/cmdline/base_command_test.cpp295
-rw-r--r--utils/cmdline/commands_map.hpp96
-rw-r--r--utils/cmdline/commands_map.ipp161
-rw-r--r--utils/cmdline/commands_map_fwd.hpp45
-rw-r--r--utils/cmdline/commands_map_test.cpp140
-rw-r--r--utils/cmdline/exceptions.cpp175
-rw-r--r--utils/cmdline/exceptions.hpp109
-rw-r--r--utils/cmdline/exceptions_test.cpp83
-rw-r--r--utils/cmdline/globals.cpp78
-rw-r--r--utils/cmdline/globals.hpp48
-rw-r--r--utils/cmdline/globals_test.cpp77
-rw-r--r--utils/cmdline/options.cpp605
-rw-r--r--utils/cmdline/options.hpp237
-rw-r--r--utils/cmdline/options_fwd.hpp51
-rw-r--r--utils/cmdline/options_test.cpp526
-rw-r--r--utils/cmdline/parser.cpp385
-rw-r--r--utils/cmdline/parser.hpp85
-rw-r--r--utils/cmdline/parser.ipp83
-rw-r--r--utils/cmdline/parser_fwd.hpp58
-rw-r--r--utils/cmdline/parser_test.cpp688
-rw-r--r--utils/cmdline/ui.cpp276
-rw-r--r--utils/cmdline/ui.hpp79
-rw-r--r--utils/cmdline/ui_fwd.hpp45
-rw-r--r--utils/cmdline/ui_mock.cpp114
-rw-r--r--utils/cmdline/ui_mock.hpp78
-rw-r--r--utils/cmdline/ui_test.cpp424
-rw-r--r--utils/config/Kyuafile10
-rw-r--r--utils/config/Makefile.am.inc87
-rw-r--r--utils/config/exceptions.cpp149
-rw-r--r--utils/config/exceptions.hpp106
-rw-r--r--utils/config/exceptions_test.cpp133
-rw-r--r--utils/config/keys.cpp70
-rw-r--r--utils/config/keys.hpp52
-rw-r--r--utils/config/keys_fwd.hpp51
-rw-r--r--utils/config/keys_test.cpp114
-rw-r--r--utils/config/lua_module.cpp282
-rw-r--r--utils/config/lua_module.hpp50
-rw-r--r--utils/config/lua_module_test.cpp474
-rw-r--r--utils/config/nodes.cpp589
-rw-r--r--utils/config/nodes.hpp272
-rw-r--r--utils/config/nodes.ipp408
-rw-r--r--utils/config/nodes_fwd.hpp70
-rw-r--r--utils/config/nodes_test.cpp695
-rw-r--r--utils/config/parser.cpp181
-rw-r--r--utils/config/parser.hpp95
-rw-r--r--utils/config/parser_fwd.hpp45
-rw-r--r--utils/config/parser_test.cpp252
-rw-r--r--utils/config/tree.cpp338
-rw-r--r--utils/config/tree.hpp128
-rw-r--r--utils/config/tree.ipp156
-rw-r--r--utils/config/tree_fwd.hpp52
-rw-r--r--utils/config/tree_test.cpp1086
-rw-r--r--utils/datetime.cpp613
-rw-r--r--utils/datetime.hpp140
-rw-r--r--utils/datetime_fwd.hpp46
-rw-r--r--utils/datetime_test.cpp593
-rw-r--r--utils/defs.hpp.in57
-rw-r--r--utils/env.cpp200
-rw-r--r--utils/env.hpp58
-rw-r--r--utils/env_test.cpp167
-rw-r--r--utils/format/Kyuafile7
-rw-r--r--utils/format/Makefile.am.inc59
-rw-r--r--utils/format/containers.hpp66
-rw-r--r--utils/format/containers.ipp138
-rw-r--r--utils/format/containers_test.cpp190
-rw-r--r--utils/format/exceptions.cpp110
-rw-r--r--utils/format/exceptions.hpp84
-rw-r--r--utils/format/exceptions_test.cpp74
-rw-r--r--utils/format/formatter.cpp293
-rw-r--r--utils/format/formatter.hpp123
-rw-r--r--utils/format/formatter.ipp76
-rw-r--r--utils/format/formatter_fwd.hpp45
-rw-r--r--utils/format/formatter_test.cpp265
-rw-r--r--utils/format/macros.hpp58
-rw-r--r--utils/fs/Kyuafile10
-rw-r--r--utils/fs/Makefile.am.inc84
-rw-r--r--utils/fs/auto_cleaners.cpp261
-rw-r--r--utils/fs/auto_cleaners.hpp89
-rw-r--r--utils/fs/auto_cleaners_fwd.hpp46
-rw-r--r--utils/fs/auto_cleaners_test.cpp167
-rw-r--r--utils/fs/directory.cpp360
-rw-r--r--utils/fs/directory.hpp120
-rw-r--r--utils/fs/directory_fwd.hpp55
-rw-r--r--utils/fs/directory_test.cpp190
-rw-r--r--utils/fs/exceptions.cpp162
-rw-r--r--utils/fs/exceptions.hpp110
-rw-r--r--utils/fs/exceptions_test.cpp95
-rw-r--r--utils/fs/lua_module.cpp340
-rw-r--r--utils/fs/lua_module.hpp54
-rw-r--r--utils/fs/lua_module_test.cpp376
-rw-r--r--utils/fs/operations.cpp803
-rw-r--r--utils/fs/operations.hpp72
-rw-r--r--utils/fs/operations_test.cpp826
-rw-r--r--utils/fs/path.cpp303
-rw-r--r--utils/fs/path.hpp87
-rw-r--r--utils/fs/path_fwd.hpp45
-rw-r--r--utils/fs/path_test.cpp277
-rw-r--r--utils/logging/Kyuafile6
-rw-r--r--utils/logging/Makefile.am.inc53
-rw-r--r--utils/logging/macros.hpp68
-rw-r--r--utils/logging/macros_test.cpp115
-rw-r--r--utils/logging/operations.cpp303
-rw-r--r--utils/logging/operations.hpp54
-rw-r--r--utils/logging/operations_fwd.hpp54
-rw-r--r--utils/logging/operations_test.cpp354
-rw-r--r--utils/memory.cpp158
-rw-r--r--utils/memory.hpp45
-rw-r--r--utils/memory_test.cpp63
-rw-r--r--utils/noncopyable.hpp75
-rw-r--r--utils/optional.hpp90
-rw-r--r--utils/optional.ipp252
-rw-r--r--utils/optional_fwd.hpp61
-rw-r--r--utils/optional_test.cpp285
-rw-r--r--utils/passwd.cpp194
-rw-r--r--utils/passwd.hpp72
-rw-r--r--utils/passwd_fwd.hpp45
-rw-r--r--utils/passwd_test.cpp179
-rw-r--r--utils/process/.gitignore1
-rw-r--r--utils/process/Kyuafile13
-rw-r--r--utils/process/Makefile.am.inc113
-rw-r--r--utils/process/child.cpp385
-rw-r--r--utils/process/child.hpp113
-rw-r--r--utils/process/child.ipp110
-rw-r--r--utils/process/child_fwd.hpp45
-rw-r--r--utils/process/child_test.cpp846
-rw-r--r--utils/process/deadline_killer.cpp54
-rw-r--r--utils/process/deadline_killer.hpp58
-rw-r--r--utils/process/deadline_killer_fwd.hpp45
-rw-r--r--utils/process/deadline_killer_test.cpp108
-rw-r--r--utils/process/exceptions.cpp91
-rw-r--r--utils/process/exceptions.hpp78
-rw-r--r--utils/process/exceptions_test.cpp63
-rw-r--r--utils/process/executor.cpp869
-rw-r--r--utils/process/executor.hpp231
-rw-r--r--utils/process/executor.ipp182
-rw-r--r--utils/process/executor_fwd.hpp49
-rw-r--r--utils/process/executor_test.cpp940
-rw-r--r--utils/process/fdstream.cpp76
-rw-r--r--utils/process/fdstream.hpp66
-rw-r--r--utils/process/fdstream_fwd.hpp45
-rw-r--r--utils/process/fdstream_test.cpp73
-rw-r--r--utils/process/helpers.cpp74
-rw-r--r--utils/process/isolation.cpp207
-rw-r--r--utils/process/isolation.hpp60
-rw-r--r--utils/process/isolation_test.cpp622
-rw-r--r--utils/process/operations.cpp273
-rw-r--r--utils/process/operations.hpp56
-rw-r--r--utils/process/operations_fwd.hpp49
-rw-r--r--utils/process/operations_test.cpp471
-rw-r--r--utils/process/status.cpp200
-rw-r--r--utils/process/status.hpp84
-rw-r--r--utils/process/status_fwd.hpp45
-rw-r--r--utils/process/status_test.cpp209
-rw-r--r--utils/process/system.cpp59
-rw-r--r--utils/process/system.hpp66
-rw-r--r--utils/process/systembuf.cpp152
-rw-r--r--utils/process/systembuf.hpp71
-rw-r--r--utils/process/systembuf_fwd.hpp45
-rw-r--r--utils/process/systembuf_test.cpp166
-rw-r--r--utils/sanity.cpp194
-rw-r--r--utils/sanity.hpp183
-rw-r--r--utils/sanity_fwd.hpp52
-rw-r--r--utils/sanity_test.cpp322
-rw-r--r--utils/signals/Kyuafile9
-rw-r--r--utils/signals/Makefile.am.inc73
-rw-r--r--utils/signals/exceptions.cpp102
-rw-r--r--utils/signals/exceptions.hpp83
-rw-r--r--utils/signals/exceptions_test.cpp73
-rw-r--r--utils/signals/interrupts.cpp309
-rw-r--r--utils/signals/interrupts.hpp83
-rw-r--r--utils/signals/interrupts_fwd.hpp46
-rw-r--r--utils/signals/interrupts_test.cpp266
-rw-r--r--utils/signals/misc.cpp106
-rw-r--r--utils/signals/misc.hpp49
-rw-r--r--utils/signals/misc_test.cpp133
-rw-r--r--utils/signals/programmer.cpp138
-rw-r--r--utils/signals/programmer.hpp63
-rw-r--r--utils/signals/programmer_fwd.hpp49
-rw-r--r--utils/signals/programmer_test.cpp140
-rw-r--r--utils/signals/timer.cpp547
-rw-r--r--utils/signals/timer.hpp86
-rw-r--r--utils/signals/timer_fwd.hpp45
-rw-r--r--utils/signals/timer_test.cpp426
-rw-r--r--utils/sqlite/Kyuafile9
-rw-r--r--utils/sqlite/Makefile.am.inc82
-rw-r--r--utils/sqlite/c_gate.cpp83
-rw-r--r--utils/sqlite/c_gate.hpp74
-rw-r--r--utils/sqlite/c_gate_fwd.hpp45
-rw-r--r--utils/sqlite/c_gate_test.cpp96
-rw-r--r--utils/sqlite/database.cpp328
-rw-r--r--utils/sqlite/database.hpp111
-rw-r--r--utils/sqlite/database_fwd.hpp45
-rw-r--r--utils/sqlite/database_test.cpp287
-rw-r--r--utils/sqlite/exceptions.cpp175
-rw-r--r--utils/sqlite/exceptions.hpp94
-rw-r--r--utils/sqlite/exceptions_test.cpp129
-rw-r--r--utils/sqlite/statement.cpp621
-rw-r--r--utils/sqlite/statement.hpp137
-rw-r--r--utils/sqlite/statement.ipp52
-rw-r--r--utils/sqlite/statement_fwd.hpp57
-rw-r--r--utils/sqlite/statement_test.cpp784
-rw-r--r--utils/sqlite/test_utils.hpp151
-rw-r--r--utils/sqlite/transaction.cpp142
-rw-r--r--utils/sqlite/transaction.hpp69
-rw-r--r--utils/sqlite/transaction_fwd.hpp45
-rw-r--r--utils/sqlite/transaction_test.cpp135
-rw-r--r--utils/stacktrace.cpp370
-rw-r--r--utils/stacktrace.hpp68
-rw-r--r--utils/stacktrace_helper.cpp36
-rw-r--r--utils/stacktrace_test.cpp620
-rw-r--r--utils/stream.cpp149
-rw-r--r--utils/stream.hpp57
-rw-r--r--utils/stream_test.cpp157
-rw-r--r--utils/test_utils.ipp113
-rw-r--r--utils/text/Kyuafile9
-rw-r--r--utils/text/Makefile.am.inc74
-rw-r--r--utils/text/exceptions.cpp91
-rw-r--r--utils/text/exceptions.hpp77
-rw-r--r--utils/text/exceptions_test.cpp76
-rw-r--r--utils/text/operations.cpp261
-rw-r--r--utils/text/operations.hpp68
-rw-r--r--utils/text/operations.ipp91
-rw-r--r--utils/text/operations_test.cpp435
-rw-r--r--utils/text/regex.cpp302
-rw-r--r--utils/text/regex.hpp92
-rw-r--r--utils/text/regex_fwd.hpp46
-rw-r--r--utils/text/regex_test.cpp177
-rw-r--r--utils/text/table.cpp428
-rw-r--r--utils/text/table.hpp125
-rw-r--r--utils/text/table_fwd.hpp58
-rw-r--r--utils/text/table_test.cpp413
-rw-r--r--utils/text/templates.cpp764
-rw-r--r--utils/text/templates.hpp122
-rw-r--r--utils/text/templates_fwd.hpp45
-rw-r--r--utils/text/templates_test.cpp1001
-rw-r--r--utils/units.cpp172
-rw-r--r--utils/units.hpp96
-rw-r--r--utils/units_fwd.hpp45
-rw-r--r--utils/units_test.cpp248
542 files changed, 96704 insertions, 0 deletions
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 <email address>
+# * Organization <optional email address>
+
+* 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 <your-branch> 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 <email address>
+
+* Sergey Bronnikov <sergeyb@openvz.org>
+* Enji Cooper <yaneurabeya@gmail.com>
+* Julio Merino <jmmv@google.com>
+* Craig Rodrigues <rodrigc@crodrigues.org>
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 `<atf-prefix>` with
+the appropriate path:
+
+ $ autoreconf -i -s -I <atf-prefix>/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.<variable_name>=<value>`:
+
+* `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.<test-suite-name>.<variable> = <value>`. (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 <<EOF
+syntax(2)
+
+-- We do not know how many CPUs the test machine has. However, parallelizing
+-- the execution of our tests to _any_ degree speeds up the time it takes to
+-- complete a test run because many of our tests are blocking.
+parallelism = 4
+EOF
+ [ "${UNPRIVILEGED_USER:-no}" = no ] || \
+ echo "unprivileged_user = 'travis'" >>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 <cstdlib>
+#include <string>
+
+#include <atf-c++.hpp>
+
+
+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 <cstdlib>
+#include <cstring>
+#include <iostream>
+
+#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 <cstdlib>
+#include <fstream>
+#include <utility>
+#include <vector>
+
+#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 <cstdlib>
+
+#include <atf-c++.hpp>
+
+#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 <cstdlib>
+
+#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 <cstdlib>
+
+#include <atf-c++.hpp>
+
+#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 <algorithm>
+#include <cstdlib>
+#include <iterator>
+#include <sstream>
+#include <string>
+
+#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 <string>
+
+#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 <cstring>
+
+#include <atf-c++.hpp>
+
+#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 <cstdlib>
+
+#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 <cstdlib>
+
+#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 <stdexcept>
+
+#include <atf-c++.hpp>
+
+#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 <algorithm>
+#include <cstdlib>
+
+#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 <algorithm>
+#include <cstdlib>
+#include <iterator>
+
+#include <atf-c++.hpp>
+
+#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 <cstdlib>
+#include <utility>
+#include <vector>
+
+#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 <string>
+
+#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 <atf-c++.hpp>
+
+#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 <algorithm>
+#include <cstddef>
+#include <cstdlib>
+#include <map>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#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 <algorithm>
+#include <cerrno>
+#include <cstdlib>
+#include <set>
+#include <stdexcept>
+
+#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 <cstddef>
+#include <cstdlib>
+#include <set>
+
+#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 <cstdlib>
+
+#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 <atf-c++.hpp>
+
+#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 <algorithm>
+#include <fstream>
+#include <iostream>
+#include <stdexcept>
+
+#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 <memory>
+#include <set>
+#include <vector>
+
+#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 <fstream>
+
+#include <atf-c++.hpp>
+
+#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 <atf-c++.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/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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+#include <iostream>
+#include <string>
+#include <utility>
+
+#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 <signal.h>
+}
+
+#include <cstdlib>
+
+#include <atf-c++.hpp>
+
+#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 <progname>.<timestamp>.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.<test_suite_name>.<variable_name> = <value>
+.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 <<EOF
+This is a manpage.
+
+With more than one line.
+EOF
+ atf_check "${MANBUILD}" input output
+ atf_check -o file:input cat output
+}
+
+
+atf_test_case one_replacement
+one_replacement_body() {
+ cat >input <<EOF
+This is a manpage.
+Where __FOO__ gets replaced.
+And nothing more.
+EOF
+ atf_check "${MANBUILD}" -v FOO=this input output
+ cat >expout <<EOF
+This is a manpage.
+Where this gets replaced.
+And nothing more.
+EOF
+ atf_check -o file:expout cat output
+}
+
+
+atf_test_case some_replacements
+some_replacements_body() {
+ cat >input <<EOF
+This is a manpage.
+Where __FOO__ gets __BAR__.
+And nothing more.
+EOF
+ atf_check "${MANBUILD}" -v FOO=this -v BAR=replaced input output
+ cat >expout <<EOF
+This is a manpage.
+Where this gets replaced.
+And nothing more.
+EOF
+ atf_check -o file:expout cat output
+}
+
+
+atf_test_case preserve_tricky_lines
+preserve_tricky_lines_body() {
+ cat >input <<EOF
+Begin
+ This line is intended.
+This other \\
+ continues later.
+\*(LtAnd this has strange characters\*(Gt
+End
+EOF
+ atf_check "${MANBUILD}" input output
+ cat >expout <<EOF
+Begin
+ This line is intended.
+This other \\
+ continues later.
+\*(LtAnd this has strange characters\*(Gt
+End
+EOF
+ atf_check -o file:expout cat output
+}
+
+
+atf_test_case includes_ok
+includes_ok_body() {
+ mkdir doc doc/subdir
+ cat >doc/input <<EOF
+This is a manpage.
+__include__ subdir/chunk
+There is more...
+__include__ chunk
+And done!
+EOF
+ cat >doc/subdir/chunk <<EOF
+This is the first inclusion
+and worked __OK__.
+EOF
+ cat >doc/chunk <<EOF
+This is the second inclusion.
+EOF
+ atf_check "${MANBUILD}" -v OK=ok doc/input output
+ cat >expout <<EOF
+This is a manpage.
+This is the first inclusion
+and worked ok.
+There is more...
+This is the second inclusion.
+And done!
+EOF
+ atf_check -o file:expout cat output
+}
+
+
+atf_test_case includes_parameterized
+includes_parameterized_body() {
+ cat >input <<EOF
+__include__ chunk value=first
+__include__ chunk value=second
+EOF
+ cat >chunk <<EOF
+This is a chunk with value: __value__.
+EOF
+ atf_check "${MANBUILD}" input output
+ cat >expout <<EOF
+This is a chunk with value: first.
+This is a chunk with value: second.
+EOF
+ atf_check -o file:expout cat output
+}
+
+
+atf_test_case includes_fail
+includes_fail_body() {
+ cat >input <<EOF
+This is a manpage.
+__include__ missing
+EOF
+ atf_check -s exit:1 -o ignore \
+ -e match:"manbuild.sh: Failed to generate output.*left unreplaced" \
+ "${MANBUILD}" input output
+ [ ! -f output ] || atf_fail "Output file was generated but it should" \
+ "not have been"
+}
+
+
+atf_test_case generate_fail
+generate_fail_body() {
+ touch input
+ atf_check -s exit:1 -o ignore \
+ -e match:"manbuild.sh: Failed to generate output" \
+ "${MANBUILD}" -v 'malformed&name=value' input output
+ [ ! -f output ] || atf_fail "Output file was generated but it should" \
+ "not have been"
+}
+
+
+atf_test_case validate_fail
+validate_fail_body() {
+ cat >input <<EOF
+This is a manpage.
+Where __FOO__ gets replaced.
+But where __BAR__ doesn't.
+EOF
+ atf_check -s exit:1 -o ignore \
+ -e match:"manbuild.sh: Failed to generate output.*left unreplaced" \
+ "${MANBUILD}" -v FOO=this input output
+ [ ! -f output ] || atf_fail "Output file was generated but it should" \
+ "not have been"
+}
+
+
+atf_test_case bad_args
+bad_args_body() {
+ atf_check -s exit:1 \
+ -e match:'manbuild.sh: Must provide input and output names' \
+ "${MANBUILD}"
+
+ atf_check -s exit:1 \
+ -e match:'manbuild.sh: Must provide input and output names' \
+ "${MANBUILD}" foo
+
+ atf_check -s exit:1 \
+ -e match:'manbuild.sh: Must provide input and output names' \
+ "${MANBUILD}" foo bar baz
+}
+
+
+atf_test_case bad_option
+bad_option_body() {
+ atf_check -s exit:1 -e match:'manbuild.sh: Unknown option -Z' \
+ "${MANBUILD}" -Z
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case empty
+ atf_add_test_case no_replacements
+ atf_add_test_case one_replacement
+ atf_add_test_case some_replacements
+ atf_add_test_case preserve_tricky_lines
+ atf_add_test_case includes_ok
+ atf_add_test_case includes_parameterized
+ atf_add_test_case includes_fail
+ atf_add_test_case generate_fail
+ atf_add_test_case validate_fail
+ atf_add_test_case bad_args
+ atf_add_test_case bad_option
+}
diff --git a/doc/results-file-flag-read.mdoc b/doc/results-file-flag-read.mdoc
new file mode 100644
index 000000000000..a0a24cfe0eec
--- /dev/null
+++ b/doc/results-file-flag-read.mdoc
@@ -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.
+Specifies the results file to operate on.
+Defaults to
+.Sq LATEST ,
+which causes
+.Nm
+to automatically load the latest results file from the current test suite.
+.Pp
+The following values are accepted:
+.Bl -tag -width XX
+.It Sq LATEST
+Requests the load of the latest results file available for the test suite rooted
+at the current directory.
+.It Directory
+Requests the load of the latest results file available for the test suite rooted
+at the given directory.
+.It Test suite name
+Requests the load of the latest results file available for the given test suite.
+.It Results identifier
+Requests the load of a specific results file.
+.It Explicit file name (aka everything else)
+Load the specified results file.
+.El
+.Pp
+See
+.Sx Results files
+for more details.
diff --git a/doc/results-file-flag-write.mdoc b/doc/results-file-flag-write.mdoc
new file mode 100644
index 000000000000..5960560a0665
--- /dev/null
+++ b/doc/results-file-flag-write.mdoc
@@ -0,0 +1,46 @@
+.\" 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.
+Specifies the results file to create.
+Defaults to
+.Sq LATEST ,
+which causes
+.Nm
+to automatically generate a new results file for the test run.
+.Pp
+The following values are accepted:
+.Bl -tag -width XX
+.It Sq NEW
+Requests the automatic generation of a new results filename based on the test
+suite being run and the current time.
+.It Explicit filename (aka everything else)
+Store the results file where indicated.
+.El
+.Pp
+See
+.Sx Results files
+for more details.
diff --git a/doc/results-files-report-example.mdoc b/doc/results-files-report-example.mdoc
new file mode 100644
index 000000000000..74ef99f16da4
--- /dev/null
+++ b/doc/results-files-report-example.mdoc
@@ -0,0 +1,32 @@
+.Ss Workflow with results files
+If one runs the following command twice in a row:
+.Bd -literal -offset indent
+kyua test -k /usr/tests/Kyuafile
+.Ed
+.Pp
+the two executions will generate two different files with names like:
+.Bd -literal -offset indent
+~/.kyua/store/results.usr_tests.20140731-150500-196784.db
+~/.kyua/store/results.usr_tests.20140731-151730-997451.db
+.Ed
+.Pp
+Taking advantage of the default naming scheme, the following commands would all
+generate a report for the results of the
+.Em latest
+execution of the test suite:
+.Bd -literal -offset indent
+cd /usr/tests && kyua __REPORT_COMMAND__
+cd /usr/tests && kyua __REPORT_COMMAND__ --results-file=LATEST
+kyua __REPORT_COMMAND__ --results-file=/usr/tests
+kyua __REPORT_COMMAND__ --results-file=usr_tests
+kyua __REPORT_COMMAND__ --results-file=usr_tests.20140731-151730-997451
+.Ed
+.Pp
+But it is also possible to explicitly load data for older runs or from
+explicitly-named files:
+.Bd -literal -offset indent
+kyua __REPORT_COMMAND__ \\
+ --results-file=usr_tests.20140731-150500-196784
+kyua __REPORT_COMMAND__ \\
+ --results-file=~/.kyua/store/results.usr_tests.20140731-150500-196784.db
+.Ed
diff --git a/doc/results-files.mdoc b/doc/results-files.mdoc
new file mode 100644
index 000000000000..3d93a7b16943
--- /dev/null
+++ b/doc/results-files.mdoc
@@ -0,0 +1,68 @@
+.\" 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.
+Results files contain, as their name implies, the results of the execution of a
+test suite.
+Each test suite executed by
+.Xr kyua-test 1
+generates a new results file, and such results files can be loaded later on by
+inspection commands such as
+.Xr kyua-report 1
+to analyze their contents.
+.Pp
+Results files support identifier-based lookups and also path name lookups.
+The differences between the two are described below.
+.Pp
+The default naming scheme for the results files provides simple support for
+identifier-based lookups and historical recording of test suite runs.
+Each results file is given an identifier derived from the test suite that
+generated it and the time the test suite was run.
+Kyua can later look up results files by these fields.
+.Pp
+The identifier follows this pattern:
+.Bd -literal -offset indent
+\*(Lttest_suite\*(Gt.\*(LtYYYYMMDD\*(Gt-\*(LtHHMMSS\*(Gt-\*(Ltuuuuuu\*(Gt
+.Ed
+.Pp
+where
+.Sq test_suite
+is the path to the root of the test suite that was run with all slashes replaced
+by underscores and
+.Sq YYYYMMDD-HHMMSS-uuuuuu
+is a timestamp with microsecond resolution.
+.Pp
+When using the default naming scheme, results files are stored in the
+.Pa ~/.kyua/store/
+subdirectory and each file holds a name of the form:
+.Bd -literal -offset indent
+~/.kyua/store/results.\*(Ltidentifier\*(Gt.db
+.Ed
+.Pp
+Results files are simple SQLite databases with the schema described in the
+.Pa __STOREDIR__/schema_v?.sql
+files.
+For details on the schema, please refer to the heavily commented SQL file.
diff --git a/doc/test-filters.mdoc b/doc/test-filters.mdoc
new file mode 100644
index 000000000000..d2d335999b9e
--- /dev/null
+++ b/doc/test-filters.mdoc
@@ -0,0 +1,40 @@
+.\" 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.
+A
+.Em test filter
+is a string that is used to match test cases or test programs in a test suite.
+Filters have the following form:
+.Bd -literal -offset indent
+test_program_name[:test_case_name]
+.Ed
+.Pp
+Where
+.Sq test_program_name
+is the name of a test program or a subdirectory in the test suite, and
+.Sq test_case_name
+is the name of a test case.
diff --git a/doc/test-isolation.mdoc b/doc/test-isolation.mdoc
new file mode 100644
index 000000000000..1072edb41105
--- /dev/null
+++ b/doc/test-isolation.mdoc
@@ -0,0 +1,112 @@
+.\" 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.
+The test programs and test cases run by
+.Nm
+are all executed in a deterministic environment.
+This known, clean environment serves to make the test execution as
+reproducible as possible and also to prevent clashes between tests that may,
+for example, create auxiliary files with overlapping names.
+.Pp
+For plain test programs and for TAP test programs, the whole test program
+is run under a single instance of the environment described in this page.
+For ATF test programs (see
+.Xr atf 7 ) ,
+each individual test case
+.Em and
+test cleanup routine are executed in separate environments.
+.Bl -tag -width XX
+.It Process space
+Each test is executed in an independent processes.
+Corollary: the test can do whatever it wants to the current process (such
+as modify global variables) without having to undo such changes.
+.It Session and process group
+The test is executed in its own session and its own process group.
+There is no controlling terminal attached to the session.
+.Pp
+Should the test spawn any children, the children should maintain the same
+session and process group.
+Modifying any of these settings prevents
+.Nm
+from being able to kill any stray subprocess as part of the cleanup phase.
+If modifying these settings is necessary, or if any subprocess started by
+the test decides to use a different process group or session, it is the
+responsibility of the test to ensure those subprocesses are forcibly
+terminated during cleanup.
+.It Work directory
+The test is executed in a temporary directory automatically created by the
+runtime engine.
+Corollary: the test can write to its current directory
+without needing to clean any files and/or directories it creates.
+The runtime engine takes care to recursively delete the temporary directories
+after the execution of a test case.
+Any file systems mounted within the temporary directory are also unmounted.
+.It Home directory
+The
+.Va HOME
+environment variable is set to the absolute path of the work directory.
+.It Umask
+The value of the umask is set to 0022.
+.It Environment
+The
+.Va LANG ,
+.Va LC_ALL ,
+.Va LC_COLLATE ,
+.Va LC_CTYPE ,
+.Va LC_MESSAGES ,
+.Va LC_MONETARY ,
+.Va LC_NUMERIC
+and
+.Va LC_TIME
+variables are unset.
+.Pp
+The
+.Va TZ
+variable is set to
+.Sq UTC .
+.Pp
+The
+.Va TMPDIR
+variable is set to the absolute path of the work directory.
+This is to prevent the test from mistakenly using a temporary directory
+outside of the automatically-managed work directory, should the test use the
+.Xr mktemp 3
+familiy of functions.
+.It Process limits
+The maximum soft core size limit is raised to its corresponding hard limit.
+This is a simple, best-effort attempt at allowing tests to dump core for
+further diagnostic purposes.
+.It Configuration varibles
+The test engine may pass run-time configuration variables to the test program
+via the environment.
+The name of the configuration variable is prefixed with
+.Sq TEST_ENV_
+so that a configuration variable of the form
+.Sq foo=bar
+becomes accessible in the environment as
+.Sq TEST_ENV_foo=bar .
+.El
diff --git a/drivers/Kyuafile b/drivers/Kyuafile
new file mode 100644
index 000000000000..35902737c943
--- /dev/null
+++ b/drivers/Kyuafile
@@ -0,0 +1,7 @@
+syntax(2)
+
+test_suite("kyua")
+
+atf_test_program{name="list_tests_test"}
+atf_test_program{name="report_junit_test"}
+atf_test_program{name="scan_results_test"}
diff --git a/drivers/Makefile.am.inc b/drivers/Makefile.am.inc
new file mode 100644
index 000000000000..0bd5a08209de
--- /dev/null
+++ b/drivers/Makefile.am.inc
@@ -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.
+
+DRIVERS_CFLAGS = $(ENGINE_CFLAGS) $(STORE_CFLAGS) $(MODEL_CFLAGS) \
+ $(UTILS_CFLAGS)
+DRIVERS_LIBS = libdrivers.a $(ENGINE_LIBS) $(STORE_LIBS) $(MODEL_LIBS) \
+ $(UTILS_LIBS)
+
+noinst_LIBRARIES += libdrivers.a
+libdrivers_a_CPPFLAGS = $(DRIVERS_CFLAGS)
+libdrivers_a_SOURCES = drivers/debug_test.cpp
+libdrivers_a_SOURCES += drivers/debug_test.hpp
+libdrivers_a_SOURCES += drivers/list_tests.cpp
+libdrivers_a_SOURCES += drivers/list_tests.hpp
+libdrivers_a_SOURCES += drivers/report_junit.cpp
+libdrivers_a_SOURCES += drivers/report_junit.hpp
+libdrivers_a_SOURCES += drivers/run_tests.cpp
+libdrivers_a_SOURCES += drivers/run_tests.hpp
+libdrivers_a_SOURCES += drivers/scan_results.cpp
+libdrivers_a_SOURCES += drivers/scan_results.hpp
+
+if WITH_ATF
+tests_driversdir = $(pkgtestsdir)/drivers
+
+tests_drivers_DATA = drivers/Kyuafile
+EXTRA_DIST += $(tests_drivers_DATA)
+
+tests_drivers_PROGRAMS = drivers/list_tests_helpers
+drivers_list_tests_helpers_SOURCES = drivers/list_tests_helpers.cpp
+drivers_list_tests_helpers_CXXFLAGS = $(ATF_CXX_CFLAGS)
+drivers_list_tests_helpers_LDADD = $(ATF_CXX_LIBS)
+
+tests_drivers_PROGRAMS += drivers/list_tests_test
+drivers_list_tests_test_SOURCES = drivers/list_tests_test.cpp
+drivers_list_tests_test_CXXFLAGS = $(DRIVERS_CFLAGS) $(ATF_CXX_CFLAGS)
+drivers_list_tests_test_LDADD = $(DRIVERS_LIBS) $(ATF_CXX_LIBS)
+
+tests_drivers_PROGRAMS += drivers/report_junit_test
+drivers_report_junit_test_SOURCES = drivers/report_junit_test.cpp
+drivers_report_junit_test_CXXFLAGS = $(DRIVERS_CFLAGS) $(ATF_CXX_CFLAGS)
+drivers_report_junit_test_LDADD = $(DRIVERS_LIBS) $(ATF_CXX_LIBS)
+
+tests_drivers_PROGRAMS += drivers/scan_results_test
+drivers_scan_results_test_SOURCES = drivers/scan_results_test.cpp
+drivers_scan_results_test_CXXFLAGS = $(DRIVERS_CFLAGS) $(ATF_CXX_CFLAGS)
+drivers_scan_results_test_LDADD = $(DRIVERS_LIBS) $(ATF_CXX_LIBS)
+endif
diff --git a/drivers/debug_test.cpp b/drivers/debug_test.cpp
new file mode 100644
index 000000000000..0717a9ad9419
--- /dev/null
+++ b/drivers/debug_test.cpp
@@ -0,0 +1,109 @@
+// 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/debug_test.hpp"
+
+#include <stdexcept>
+#include <utility>
+
+#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 <set>
+#include <string>
+
+#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 <cstdlib>
+
+#include <atf-c++.hpp>
+
+#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 <sys/stat.h>
+
+#include <unistd.h>
+}
+
+#include <map>
+#include <set>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#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 <algorithm>
+
+#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 << "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n";
+ _output << "<testsuite>\n";
+
+ _output << "<properties>\n";
+ _output << F("<property name=\"cwd\" value=\"%s\"/>\n")
+ % text::escape_xml(context.cwd().str());
+ for (model::properties_map::const_iterator iter =
+ context.env().begin(); iter != context.env().end(); ++iter) {
+ _output << F("<property name=\"env.%s\" value=\"%s\"/>\n")
+ % text::escape_xml((*iter).first)
+ % text::escape_xml((*iter).second);
+ }
+ _output << "</properties>\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("<testcase classname=\"%s\" name=\"%s\" time=\"%s\">\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("<failure message=\"%s\"/>\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 << "<skipped/>\n";
+ stderr_contents += ("Skipped result details\n"
+ "----------------------\n"
+ "\n"
+ + result.reason() + "\n"
+ "\n");
+ break;
+
+ default:
+ _output << F("<error message=\"%s\"/>\n")
+ % text::escape_xml(result.reason());
+ }
+
+ const std::string stdout_contents = iter.stdout_contents();
+ if (!stdout_contents.empty()) {
+ _output << F("<system-out>%s</system-out>\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 += "<EMPTY>\n";
+ } else {
+ stderr_contents += real_stderr_contents;
+ }
+ }
+ _output << "<system-err>" << text::escape_xml(stderr_contents)
+ << "</system-err>\n";
+
+ _output << "</testcase>\n";
+}
+
+
+/// Finalizes the report.
+void
+drivers::report_junit_hooks::end(const drivers::scan_results::result& /* r */)
+{
+ _output << "</testsuite>\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 <ostream>
+#include <string>
+
+#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 <sstream>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+#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 =
+ "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n"
+ "<testsuite>\n"
+ "<properties>\n"
+ "<property name=\"cwd\" value=\"/root\"/>\n"
+ "</properties>\n"
+ "</testsuite>\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() +
+ "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n"
+ "<testsuite>\n"
+ "<properties>\n"
+ "<property name=\"cwd\" value=\"/root\"/>\n"
+ "<property name=\"env.VAR0\" value=\"Value 0\"/>\n"
+ "<property name=\"env.VAR1\" value=\"Value 1\"/>\n"
+ "</properties>\n"
+
+ "<testcase classname=\"dir.prog-1\" name=\"t0\" time=\"0.500\">\n"
+ "<error message=\"Broken\"/>\n"
+ "<system-err>"
+ + 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 +
+ "&lt;EMPTY&gt;\n"
+ "</system-err>\n"
+ "</testcase>\n"
+
+ "<testcase classname=\"dir.prog-1\" name=\"t1\" time=\"1.500\">\n"
+ "<system-err>"
+ "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 +
+ "&lt;EMPTY&gt;\n"
+ "</system-err>\n"
+ "</testcase>\n"
+
+ "<testcase classname=\"dir.prog-1\" name=\"t2\" time=\"2.500\">\n"
+ "<failure message=\"Failed\"/>\n"
+ "<system-err>"
+ + 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 +
+ "&lt;EMPTY&gt;\n"
+ "</system-err>\n"
+ "</testcase>\n"
+
+ "<testcase classname=\"dir.sub.prog-2\" name=\"t0\" time=\"0.500\">\n"
+ "<system-out>stdout file 0</system-out>\n"
+ "<system-err>"
+ + 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</system-err>\n"
+ "</testcase>\n"
+
+ "<testcase classname=\"dir.sub.prog-2\" name=\"t1\" time=\"1.500\">\n"
+ "<skipped/>\n"
+ "<system-out>stdout file 1</system-out>\n"
+ "<system-err>"
+ "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</system-err>\n"
+ "</testcase>\n"
+
+ "</testsuite>\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 <utility>
+
+#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 <set>
+#include <string>
+
+#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 <stdint.h>
+}
+
+#include <set>
+
+#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 <set>
+
+#include <atf-c++.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/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 <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <fstream>
+
+#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 <sys/stat.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <algorithm>
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+#include <set>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+#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 <fstream>
+#include <string>
+#include <utility>
+
+#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 <istream>
+
+#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 <sstream>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#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 <cstdlib>
+#include <fstream>
+#include <utility>
+
+#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
+/// '&lt;&lt;NEWLINE&gt;&gt;'.
+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 += "<<NEWLINE>>" + line;
+ ret.first++;
+ }
+ } else if (input.good()) {
+ if (ret.first == 0)
+ ret.second = line;
+ else
+ ret.second += "<<NEWLINE>>" + 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 ': <reason>'") %
+ 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)]: "
+ "<reason>' 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 <istream>
+#include <ostream>
+
+#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 <signal.h>
+}
+
+#include <cstdlib>
+#include <fstream>
+#include <sstream>
+#include <stdexcept>
+
+#include <atf-c++.hpp>
+
+#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<<NEWLINE>>bar",
+ "failed: foo\nbar");
+PARSE_BROKEN(multiline__with_newline,
+ "multiple lines.*foo<<NEWLINE>>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 <sys/stat.h>
+
+#include <signal.h>
+}
+
+#include <atf-c++.hpp>
+
+#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 <stdexcept>
+
+#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 <stdexcept>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+#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 <stdexcept>
+
+#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 <cstring>
+
+#include <atf-c++.hpp>
+
+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 <algorithm>
+#include <stdexcept>
+
+#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 &lt;test_program%gt;[:&lt;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 <ostream>
+#include <string>
+#include <set>
+#include <utility>
+
+#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 <stdexcept>
+
+#include <atf-c++.hpp>
+
+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 <algorithm>
+#include <iterator>
+#include <stdexcept>
+
+#include <lutok/exceptions.hpp>
+#include <lutok/operations.hpp>
+#include <lutok/stack_cleaner.hpp>
+#include <lutok/state.ipp>
+
+#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 <string>
+#include <vector>
+
+#include <lutok/state.hpp>
+
+#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 <unistd.h>
+}
+
+#include <stdexcept>
+#include <typeinfo>
+
+#include <atf-c++.hpp>
+#include <lutok/operations.hpp>
+#include <lutok/state.ipp>
+#include <lutok/test_utils.hpp>
+
+#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 <unistd.h>
+}
+
+#include <cstdlib>
+
+#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 <sys/stat.h>
+
+#include <unistd.h>
+
+extern char** environ;
+}
+
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <sstream>
+
+#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 <signal.h>
+}
+
+#include <atf-c++.hpp>
+
+#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 <string>
+
+#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 <atf-c++.hpp>
+
+#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 <deque>
+#include <string>
+
+#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 <memory>
+#include <set>
+
+#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 <string>
+#include <utility>
+
+#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 <cstdarg>
+#include <cstddef>
+#include <typeinfo>
+
+#include <atf-c++.hpp>
+
+#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 <unistd.h>
+}
+
+#include <cstdio>
+#include <cstdlib>
+#include <fstream>
+#include <memory>
+#include <stdexcept>
+
+#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 <memory>
+#include <set>
+#include <string>
+
+#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 <memory>
+
+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 <sys/types.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#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 <unistd.h>
+}
+
+#include <cstdlib>
+
+#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 <sys/stat.h>
+
+#include <unistd.h>
+
+extern char** environ;
+}
+
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+
+#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 <fstream>
+
+#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 <cstddef>
+#include <ostream>
+#include <string>
+
+#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 <cstddef>
+#include <utility>
+
+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 <fstream>
+
+#include <atf-c++.hpp>
+
+#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 <signal.h>
+}
+
+#include <atf-c++.hpp>
+
+#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 <unistd.h>
+}
+
+#include <atf-c++.hpp>
+
+#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 <contributor@example.net>" >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 <contributor@example.net>" >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 <<EOF
+Usage error for command about: Invalid about topic 'foo'.
+Type 'kyua help about' for usage information.
+EOF
+ atf_check -s exit:3 -o empty -e file:experr kyua about foo
+}
+
+
+utils_test_case too_many_arguments
+too_many_arguments_body() {
+ cat >stderr <<EOF
+Usage error for command about: Too many arguments.
+Type 'kyua help about' for usage information.
+EOF
+ atf_check -s exit:3 -o empty -e file:stderr kyua about abc def
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case all_topics__installed
+ atf_add_test_case all_topics__override
+ atf_add_test_case topic__authors__installed
+ atf_add_test_case topic__authors__override
+ atf_add_test_case topic__license__installed
+ atf_add_test_case topic__license__override
+ atf_add_test_case topic__version
+ atf_add_test_case topic__invalid
+
+ atf_add_test_case too_many_arguments
+}
diff --git a/integration/cmd_config_test.sh b/integration/cmd_config_test.sh
new file mode 100755
index 000000000000..ed457e5c4b37
--- /dev/null
+++ b/integration/cmd_config_test.sh
@@ -0,0 +1,355 @@
+# 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 defaults
+defaults_body() {
+ atf_check -s exit:0 \
+ -o match:'^architecture = ' \
+ -o match:'^platform = ' \
+ kyua config
+}
+
+
+utils_test_case all
+all_body() {
+ mkdir "${HOME}/.kyua"
+ cat >"${HOME}/.kyua/kyua.conf" <<EOF
+syntax(2)
+architecture = "my-architecture"
+parallelism = 256
+platform = "my-platform"
+unprivileged_user = "$(id -u -n)"
+test_suites.suite1.the_variable = "value1"
+test_suites.suite2.the_variable = "value2"
+EOF
+
+ cat >expout <<EOF
+architecture = my-architecture
+parallelism = 256
+platform = my-platform
+test_suites.suite1.the_variable = value1
+test_suites.suite2.the_variable = value2
+unprivileged_user = $(id -u -n)
+EOF
+
+ atf_check -s exit:0 -o file:expout -e empty kyua config
+}
+
+
+utils_test_case one__ok
+one__ok_body() {
+ mkdir "${HOME}/.kyua"
+ cat >"${HOME}/.kyua/kyua.conf" <<EOF
+syntax(2)
+test_suites.first.one = 1
+test_suites.first.two = 2
+EOF
+
+ cat >expout <<EOF
+test_suites.first.two = 2
+EOF
+
+ atf_check -s exit:0 -o file:expout -e empty kyua config \
+ test_suites.first.two
+}
+
+
+utils_test_case one__fail
+one__fail_body() {
+ mkdir "${HOME}/.kyua"
+ cat >"${HOME}/.kyua/kyua.conf" <<EOF
+syntax(2)
+test_suites.first.one = 1
+test_suites.first.three = 3
+EOF
+
+ cat >experr <<EOF
+kyua: W: 'test_suites.first.two' is not defined.
+EOF
+
+ atf_check -s exit:1 -o empty -e file:experr kyua config \
+ test_suites.first.two
+}
+
+
+utils_test_case many__ok
+many__ok_body() {
+ mkdir "${HOME}/.kyua"
+ cat >"${HOME}/.kyua/kyua.conf" <<EOF
+syntax(2)
+architecture = "overriden"
+unknown_setting = "foo"
+test_suites.first.one = 1
+test_suites.first.two = 2
+EOF
+
+ cat >expout <<EOF
+architecture = overriden
+test_suites.first.two = 2
+test_suites.first.one = 1
+EOF
+
+ atf_check -s exit:0 -o file:expout -e empty kyua config \
+ architecture \
+ test_suites.first.two \
+ test_suites.first.one # Inverse order on purpose.
+ atf_check -s exit:0 -o match:architecture -o not-match:unknown_setting \
+ -e empty kyua config
+}
+
+
+utils_test_case many__fail
+many__fail_body() {
+ mkdir "${HOME}/.kyua"
+ cat >"${HOME}/.kyua/kyua.conf" <<EOF
+syntax(2)
+test_suites.first.one = 1
+test_suites.first.three = 3
+EOF
+
+ cat >expout <<EOF
+test_suites.first.one = 1
+test_suites.first.three = 3
+EOF
+
+ cat >experr <<EOF
+kyua: W: 'test_suites.first.two' is not defined.
+kyua: W: 'test_suites.first.fourth' is not defined.
+EOF
+
+ atf_check -s exit:1 -o file:expout -e file:experr kyua config \
+ test_suites.first.one test_suites.first.two \
+ test_suites.first.three test_suites.first.fourth
+}
+
+
+utils_test_case config_flag__default_system
+config_flag__default_system_body() {
+ cat >kyua.conf <<EOF
+syntax(2)
+test_suites.foo.var = "baz"
+EOF
+
+ atf_check -s exit:1 -o empty \
+ -e match:"kyua: W: 'test_suites.foo.var'.*not defined" \
+ kyua config test_suites.foo.var
+ export KYUA_CONFDIR="$(pwd)"
+ atf_check -s exit:0 -o match:"foo.var = baz" -e empty \
+ kyua config test_suites.foo.var
+}
+
+
+utils_test_case config_flag__default_home
+config_flag__default_home_body() {
+ cat >kyua.conf <<EOF
+syntax(2)
+test_suites.foo.var = "bar"
+EOF
+ export KYUA_CONFDIR="$(pwd)"
+ atf_check -s exit:0 -o match:"test_suites.foo.var = bar" -e empty \
+ kyua config test_suites.foo.var
+
+ # The previously-created "system-wide" file has to be ignored.
+ mkdir .kyua
+ cat >.kyua/kyua.conf <<EOF
+syntax(2)
+test_suites.foo.var = "baz"
+EOF
+ atf_check -s exit:0 -o match:"test_suites.foo.var = baz" -e empty \
+ kyua config test_suites.foo.var
+}
+
+
+utils_test_case config_flag__explicit__ok
+config_flag__explicit__ok_body() {
+ cat >kyua.conf <<EOF
+syntax(2)
+test_suites.foo.var = "baz"
+EOF
+
+ atf_check -s exit:1 -o empty \
+ -e match:"kyua: W: 'test_suites.foo.var'.*not defined" \
+ kyua config test_suites.foo.var
+ atf_check -s exit:0 -o match:"test_suites.foo.var = baz" -e empty \
+ kyua -c kyua.conf config test_suites.foo.var
+ atf_check -s exit:0 -o match:"test_suites.foo.var = baz" -e empty \
+ kyua --config=kyua.conf config test_suites.foo.var
+}
+
+
+utils_test_case config_flag__explicit__disable
+config_flag__explicit__disable_body() {
+ cat >kyua.conf <<EOF
+syntax(2)
+test_suites.foo.var = "baz"
+EOF
+ mkdir .kyua
+ cp kyua.conf .kyua/kyua.conf
+ export KYUA_CONFDIR="$(pwd)"
+
+ atf_check -s exit:0 -o match:"test_suites.foo.var = baz" -e empty \
+ kyua config test_suites.foo.var
+ atf_check -s exit:1 -o empty \
+ -e match:"kyua: W: 'test_suites.foo.var'.*not defined" \
+ kyua --config=none config test_suites.foo.var
+}
+
+
+utils_test_case config_flag__explicit__missing_file
+config_flag__explicit__missing_file_body() {
+ cat >experr <<EOF
+kyua: E: Load of 'foo' failed: File 'foo' not found.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua --config=foo config
+}
+
+
+utils_test_case config_flag__explicit__bad_file
+config_flag__explicit__bad_file_body() {
+ touch custom
+ atf_check -s exit:2 -o empty -e match:"No syntax defined" \
+ kyua --config=custom config
+}
+
+
+utils_test_case variable_flag__no_config
+variable_flag__no_config_body() {
+ atf_check -s exit:0 \
+ -o match:'test_suites.suite1.the_variable = value1' \
+ -o match:'test_suites.suite2.the_variable = value2' \
+ -e empty \
+ kyua \
+ -v "test_suites.suite1.the_variable=value1" \
+ -v "test_suites.suite2.the_variable=value2" \
+ config
+
+ atf_check -s exit:0 \
+ -o match:'test_suites.suite1.the_variable = value1' \
+ -o match:'test_suites.suite2.the_variable = value2' \
+ -e empty \
+ kyua \
+ --variable="test_suites.suite1.the_variable=value1" \
+ --variable="test_suites.suite2.the_variable=value2" \
+ config
+}
+
+
+utils_test_case variable_flag__override_default_config
+variable_flag__override_default_config_body() {
+ mkdir "${HOME}/.kyua"
+ cat >"${HOME}/.kyua/kyua.conf" <<EOF
+syntax(2)
+test_suites.suite1.the_variable = "value1"
+test_suites.suite2.the_variable = "should not be used"
+EOF
+
+ atf_check -s exit:0 \
+ -o match:'test_suites.suite1.the_variable = value1' \
+ -o match:'test_suites.suite2.the_variable = overriden' \
+ -o match:'test_suites.suite3.the_variable = new' \
+ -e empty kyua \
+ -v "test_suites.suite2.the_variable=overriden" \
+ -v "test_suites.suite3.the_variable=new" \
+ config
+
+ atf_check -s exit:0 \
+ -o match:'test_suites.suite1.the_variable = value1' \
+ -o match:'test_suites.suite2.the_variable = overriden' \
+ -o match:'test_suites.suite3.the_variable = new' \
+ -e empty kyua \
+ --variable="test_suites.suite2.the_variable=overriden" \
+ --variable="test_suites.suite3.the_variable=new" \
+ config
+}
+
+
+utils_test_case variable_flag__override_custom_config
+variable_flag__override_custom_config_body() {
+ cat >config <<EOF
+syntax(2)
+test_suites.suite1.the_variable = "value1"
+test_suites.suite2.the_variable = "should not be used"
+EOF
+
+ atf_check -s exit:0 \
+ -o match:'test_suites.suite2.the_variable = overriden' \
+ -e empty kyua -c config \
+ -v "test_suites.suite2.the_variable=overriden" config
+
+ atf_check -s exit:0 \
+ -o match:'test_suites.suite2.the_variable = overriden' \
+ -e empty kyua -c config \
+ --variable="test_suites.suite2.the_variable=overriden" config
+}
+
+
+utils_test_case variable_flag__invalid_key
+variable_flag__invalid_key_body() {
+ # CHECK_STYLE_DISABLE
+ cat >experr <<EOF
+Usage error: Invalid argument '' for option --variable: Argument does not have the form 'K=V'.
+Type 'kyua help' for usage information.
+EOF
+ # CHECK_STYLE_ENABLE
+ atf_check -s exit:3 -o empty -e file:experr kyua \
+ -v "test_suites.a.b=c" -v "" config
+}
+
+
+utils_test_case variable_flag__invalid_value
+variable_flag__invalid_value_body() {
+ cat >experr <<EOF
+kyua: E: Invalid value for property 'parallelism': Must be a positive integer.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua \
+ -v "parallelism=0" config
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case defaults
+ atf_add_test_case all
+ atf_add_test_case one__ok
+ atf_add_test_case one__fail
+ atf_add_test_case many__ok
+ atf_add_test_case many__fail
+
+ atf_add_test_case config_flag__default_system
+ atf_add_test_case config_flag__default_home
+ atf_add_test_case config_flag__explicit__ok
+ atf_add_test_case config_flag__explicit__disable
+ atf_add_test_case config_flag__explicit__missing_file
+ atf_add_test_case config_flag__explicit__bad_file
+
+ atf_add_test_case variable_flag__no_config
+ atf_add_test_case variable_flag__override_default_config
+ atf_add_test_case variable_flag__override_custom_config
+ atf_add_test_case variable_flag__invalid_key
+ atf_add_test_case variable_flag__invalid_value
+}
diff --git a/integration/cmd_db_exec_test.sh b/integration/cmd_db_exec_test.sh
new file mode 100755
index 000000000000..c260f23a78e1
--- /dev/null
+++ b/integration/cmd_db_exec_test.sh
@@ -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.
+
+
+# Creates a new database file in the store directory.
+#
+# Subsequent invocations of db-exec should just pick this new file up.
+create_empty_store() {
+ cat >Kyuafile <<EOF
+syntax(2)
+EOF
+ atf_check -s exit:0 -o ignore -e empty kyua test
+}
+
+
+utils_test_case one_arg
+one_arg_body() {
+ create_empty_store
+
+ atf_check -s exit:0 -o save:metadata.csv -e empty \
+ kyua db-exec "SELECT * FROM metadata"
+ atf_check -s exit:0 -o ignore -e empty \
+ grep 'schema_version,.*timestamp' metadata.csv
+}
+
+
+utils_test_case many_args
+many_args_body() {
+ create_empty_store
+
+ atf_check -s exit:0 -o save:metadata.csv -e empty \
+ kyua db-exec SELECT "*" FROM metadata
+ atf_check -s exit:0 -o ignore -e empty \
+ grep 'schema_version,.*timestamp' metadata.csv
+}
+
+
+utils_test_case no_args
+no_args_body() {
+ atf_check -s exit:3 -o empty -e match:"Not enough arguments" kyua db-exec
+ test ! -d .kyua/store/ || atf_fail "Database created but it should" \
+ "not have been"
+}
+
+
+utils_test_case invalid_statement
+invalid_statement_body() {
+ create_empty_store
+
+ atf_check -s exit:1 -o empty -e match:"SQLite error.*foo" \
+ kyua db-exec foo
+}
+
+
+utils_test_case no_create_store
+no_create_store_body() {
+ atf_check -s exit:1 -o empty -e match:"No previous results.*not-here" \
+ kyua db-exec --results-file=not-here "SELECT * FROM metadata"
+ if [ -f not-here ]; then
+ atf_fail "Database created but it should not have been"
+ fi
+}
+
+
+utils_test_case results_file__default_home
+results_file__default_home_body() {
+ HOME=home-dir
+ create_empty_store
+
+ atf_check -s exit:0 -o save:metadata.csv -e empty \
+ kyua db-exec "SELECT * FROM metadata"
+ test -f home-dir/.kyua/store/*.db || atf_fail "Database not created in" \
+ "the home directory"
+ atf_check -s exit:0 -o ignore -e empty \
+ grep 'schema_version,.*timestamp' metadata.csv
+}
+
+
+utils_test_case results_file__explicit__ok
+results_file__explicit__ok_body() {
+ create_empty_store
+ mv .kyua/store/*.db custom.db
+ rmdir .kyua/store
+
+ HOME=home-dir
+ atf_check -s exit:0 -o save:metadata.csv -e empty \
+ kyua --logfile=/dev/null db-exec -r custom.db "SELECT * FROM metadata"
+ test ! -d home-dir/.kyua || atf_fail "Home directory created but this" \
+ "should not have happened"
+ atf_check -s exit:0 -o ignore -e empty \
+ grep 'schema_version,.*timestamp' metadata.csv
+}
+
+
+utils_test_case results_file__explicit__fail
+results_file__explicit__fail_head() {
+ atf_set "require.user" "unprivileged"
+}
+results_file__explicit__fail_body() {
+ atf_check -s exit:1 -o empty -e match:"No previous results.*foo.db" \
+ kyua db-exec --results-file=foo.db "SELECT * FROM metadata"
+}
+
+
+utils_test_case no_headers_flag
+no_headers_flag_body() {
+ create_empty_store
+
+ atf_check kyua db-exec "CREATE TABLE data" \
+ "(a INTEGER PRIMARY KEY, b INTEGER, c TEXT)"
+ atf_check kyua db-exec "INSERT INTO data VALUES (65, 43, NULL)"
+ atf_check kyua db-exec "INSERT INTO data VALUES (23, 42, 'foo')"
+
+ cat >expout <<EOF
+a,b,c
+23,42,foo
+65,43,NULL
+EOF
+ atf_check -s exit:0 -o file:expout -e empty \
+ kyua db-exec "SELECT * FROM data ORDER BY a"
+
+ tail -n 2 <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 <<EOF
+Usage error for command db-migrate: Too many arguments.
+Type 'kyua help db-migrate' for usage information.
+EOF
+ atf_check -s exit:3 -o empty -e file:stderr kyua db-migrate abc def
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case upgrade__from_v1
+ atf_add_test_case upgrade__from_v2
+ atf_add_test_case already_up_to_date
+ atf_add_test_case need_upgrade
+
+ atf_add_test_case results_file__ok
+ atf_add_test_case results_file__fail
+
+ atf_add_test_case too_many_arguments
+}
diff --git a/integration/cmd_debug_test.sh b/integration/cmd_debug_test.sh
new file mode 100755
index 000000000000..b34a96d72eda
--- /dev/null
+++ b/integration/cmd_debug_test.sh
@@ -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.
+
+
+utils_test_case no_args
+no_args_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="simple_all_pass"}
+EOF
+ utils_cp_helper simple_all_pass .
+
+ cat >experr <<EOF
+Usage error for command debug: Not enough arguments.
+Type 'kyua help debug' for usage information.
+EOF
+ atf_check -s exit:3 -o empty -e file:experr kyua debug
+}
+
+
+utils_test_case many_args
+many_args_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >experr <<EOF
+Usage error for command debug: Too many arguments.
+Type 'kyua help debug' for usage information.
+EOF
+ atf_check -s exit:3 -o empty -e file:experr kyua debug first:pass \
+ second:pass
+}
+
+
+utils_test_case one_arg__ok_pass
+one_arg__ok_pass_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper expect_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >expout <<EOF
+This is the stdout of pass
+second:pass -> passed
+EOF
+cat >experr <<EOF
+This is the stderr of pass
+EOF
+ atf_check -s exit:0 -o file:expout -e file:experr kyua debug second:pass
+}
+
+
+utils_test_case one_arg__ok_fail
+one_arg__ok_fail_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+EOF
+ utils_cp_helper simple_some_fail first
+
+ cat >expout <<EOF
+This is the stdout of fail
+first:fail -> failed: This fails on purpose
+EOF
+ cat >experr <<EOF
+This is the stderr of fail
+EOF
+ atf_check -s exit:1 -o file:expout -e file:experr kyua debug first:fail
+}
+
+
+utils_test_case one_arg__no_match
+one_arg__no_match_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper expect_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >experr <<EOF
+kyua: E: Unknown test case 'second:die'.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua debug second:die
+}
+
+
+utils_test_case one_arg__no_test_case
+one_arg__no_test_case_body() {
+ # CHECK_STYLE_DISABLE
+ cat >experr <<EOF
+Usage error for command debug: 'foo' is not a test case identifier (missing ':'?).
+Type 'kyua help debug' for usage information.
+EOF
+ # CHECK_STYLE_ENABLE
+ atf_check -s exit:3 -o empty -e file:experr kyua debug foo
+}
+
+
+utils_test_case one_arg__bad_filter
+one_arg__bad_filter_body() {
+ cat >experr <<EOF
+kyua: E: Test case component in 'foo:' is empty.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua debug foo:
+}
+
+
+utils_test_case body_and_cleanup
+body_and_cleanup_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="single"}
+EOF
+ utils_cp_helper metadata single
+
+ cat >expout <<EOF
+single:with_cleanup -> 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 <<EOF
+Body message to stdout
+Cleanup message to stdout
+EOF
+ atf_check -s exit:0 -o file:expout -e empty cat saved.out
+
+ cat >experr <<EOF
+Body message to stderr
+Cleanup message to stderr
+EOF
+ atf_check -s exit:0 -o file:experr -e empty cat saved.err
+}
+
+
+utils_test_case stdout_stderr_flags
+stdout_stderr_flags_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper expect_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >expout <<EOF
+second:pass -> passed
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua debug \
+ --stdout=saved.out --stderr=saved.err second:pass
+
+ cat >expout <<EOF
+This is the stdout of pass
+EOF
+ cmp -s saved.out expout || atf_fail "--stdout did not redirect the" \
+ "standard output to the desired file"
+
+ cat >experr <<EOF
+This is the stderr of pass
+EOF
+ cmp -s saved.err experr || atf_fail "--stderr did not redirect the" \
+ "standard error to the desired file"
+}
+
+
+utils_test_case args_are_relative
+args_are_relative_body() {
+ mkdir root
+ cat >root/Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+include("subdir/Kyuafile")
+atf_test_program{name="prog"}
+EOF
+ utils_cp_helper simple_all_pass root/prog
+
+ mkdir root/subdir
+ cat >root/subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="prog"}
+EOF
+ utils_cp_helper simple_some_fail root/subdir/prog
+
+ cat >expout <<EOF
+This is the stdout of fail
+subdir/prog:fail -> failed: This fails on purpose
+EOF
+ cat >experr <<EOF
+This is the stderr of fail
+EOF
+ atf_check -s exit:1 -o file:expout -e file:experr kyua debug \
+ -k "$(pwd)/root/Kyuafile" subdir/prog:fail
+}
+
+
+utils_test_case only_load_used_test_programs
+only_load_used_test_programs_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper bad_test_program second
+
+ cat >expout <<EOF
+This is the stdout of pass
+first:pass -> passed
+EOF
+ cat >experr <<EOF
+This is the stderr of pass
+EOF
+ CREATE_COOKIE="$(pwd)/cookie"; export CREATE_COOKIE
+ atf_check -s exit:0 -o file:expout -e file:experr kyua debug first:pass
+ 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" <<EOF
+syntax(2)
+test_suites.suite1["the-variable"] = "value1"
+test_suites.suite2["the-variable"] = "override me"
+EOF
+
+ cat >Kyuafile <<EOF
+syntax(2)
+atf_test_program{name="config1", test_suite="suite1"}
+atf_test_program{name="config2", test_suite="suite2"}
+atf_test_program{name="config3", test_suite="suite3"}
+EOF
+ utils_cp_helper config config1
+ utils_cp_helper config config2
+ utils_cp_helper config config3
+
+ atf_check -s exit:1 -o match:'failed' -e empty \
+ kyua -c my-config -v test_suites.suite2.the-variable=value2 \
+ debug config1:get_variable
+ atf_check -s exit:0 -o match:'passed' -e empty \
+ kyua -c my-config -v test_suites.suite2.the-variable=value2 \
+ debug config2:get_variable
+ atf_check -s exit:0 -o match:'skipped' -e empty \
+ kyua -c my-config -v test_suites.suite2.the-variable=value2 \
+ debug config3:get_variable
+}
+
+
+utils_test_case build_root_flag
+build_root_flag_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ mkdir build
+ utils_cp_helper expect_all_pass build/first
+ utils_cp_helper simple_all_pass build/second
+
+ cat >expout <<EOF
+This is the stdout of pass
+second:pass -> passed
+EOF
+cat >experr <<EOF
+This is the stderr of pass
+EOF
+ atf_check -s exit:0 -o file:expout -e file:experr \
+ kyua debug --build-root=build second:pass
+}
+
+
+utils_test_case kyuafile_flag__ok
+kyuafile_flag__ok_body() {
+ cat >Kyuafile <<EOF
+This file is bogus but it is not loaded.
+EOF
+
+ cat >myfile <<EOF
+syntax(2)
+test_suite("hello-world")
+atf_test_program{name="sometest"}
+EOF
+ utils_cp_helper simple_all_pass sometest
+
+ atf_check -s exit:0 -o match:passed -e empty kyua test -k myfile sometest
+ atf_check -s exit:0 -o match:passed -e empty kyua test --kyuafile=myfile \
+ sometest
+}
+
+
+utils_test_case missing_kyuafile
+missing_kyuafile_body() {
+ cat >experr <<EOF
+kyua: E: Load of 'Kyuafile' failed: File 'Kyuafile' not found.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua debug foo:bar
+}
+
+
+utils_test_case bogus_kyuafile
+bogus_kyuafile_body() {
+ cat >Kyuafile <<EOF
+Hello, world.
+EOF
+ atf_check -s exit:2 -o empty \
+ -e match:"Load of 'Kyuafile' failed: .* Kyuafile:2:" kyua list
+}
+
+
+utils_test_case bogus_test_program
+bogus_test_program_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="crash_on_list"}
+atf_test_program{name="non_executable"}
+EOF
+ utils_cp_helper bad_test_program crash_on_list
+ echo 'I am not executable' >non_executable
+
+ cat >experr <<EOF
+kyua: E: Unknown test case 'crash_on_list:a'.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua debug crash_on_list:a
+
+ cat >experr <<EOF
+kyua: E: Unknown test case 'non_executable:a'.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua debug non_executable:a
+
+ # CHECK_STYLE_DISABLE
+ cat >expout <<EOF
+crash_on_list:__test_cases_list__ -> 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 <<EOF
+non_executable:__test_cases_list__ -> 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 <<EOF
+Usage error for command help: The command abc does not exist.
+Type 'kyua help help' for usage information.
+EOF
+ atf_check -s exit:3 -o empty -e file:stderr kyua help abc
+}
+
+
+utils_test_case too_many_arguments
+too_many_arguments_body() {
+ cat >stderr <<EOF
+Usage error for command help: Too many arguments.
+Type 'kyua help help' for usage information.
+EOF
+ atf_check -s exit:3 -o empty -e file:stderr kyua help about cde
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case global
+ atf_add_test_case one_command
+
+ atf_add_test_case ignore_bad_config
+ atf_add_test_case unknown_command
+ atf_add_test_case too_many_arguments
+}
diff --git a/integration/cmd_list_test.sh b/integration/cmd_list_test.sh
new file mode 100755
index 000000000000..a916e0f2ec4b
--- /dev/null
+++ b/integration/cmd_list_test.sh
@@ -0,0 +1,600 @@
+# 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 no_args
+no_args_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="metadata"}
+atf_test_program{name="simple_all_pass"}
+include("subdir/Kyuafile")
+EOF
+ utils_cp_helper metadata .
+ utils_cp_helper simple_all_pass .
+
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration2")
+atf_test_program{name="simple_some_fail"}
+EOF
+ utils_cp_helper simple_some_fail subdir
+
+ cat >expout <<EOF
+metadata:many_properties
+metadata:no_properties
+metadata:one_property
+metadata:with_cleanup
+simple_all_pass:pass
+simple_all_pass:skip
+subdir/simple_some_fail:fail
+subdir/simple_some_fail:pass
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list
+}
+
+
+utils_test_case one_arg__subdir
+one_arg__subdir_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+include("subdir/Kyuafile")
+EOF
+
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("in-subdir")
+atf_test_program{name="simple_all_pass"}
+EOF
+ utils_cp_helper simple_all_pass subdir
+
+ cat >expout <<EOF
+subdir/simple_all_pass:pass
+subdir/simple_all_pass:skip
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list subdir
+}
+
+
+utils_test_case one_arg__test_case
+one_arg__test_case_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >expout <<EOF
+first:skip
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list first:skip
+}
+
+
+utils_test_case one_arg__test_program
+one_arg__test_program_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_some_fail second
+
+ cat >expout <<EOF
+second:fail
+second:pass
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list second
+}
+
+
+utils_test_case one_arg__invalid
+one_arg__invalid_body() {
+cat >experr <<EOF
+kyua: E: Test case component in 'foo:' is empty.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua list foo:
+
+cat >experr <<EOF
+kyua: E: Program name '/a/b' must be relative to the test suite, not absolute.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua list /a/b
+}
+
+
+utils_test_case many_args__ok
+many_args__ok_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+include("subdir/Kyuafile")
+atf_test_program{name="first"}
+EOF
+ utils_cp_helper simple_all_pass first
+
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("in-subdir")
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_some_fail subdir/second
+
+ cat >expout <<EOF
+subdir/second:fail (in-subdir)
+subdir/second:pass (in-subdir)
+first:pass (top-level)
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list -v subdir first:pass
+}
+
+
+utils_test_case many_args__invalid
+many_args__invalid_body() {
+cat >experr <<EOF
+kyua: E: Program name component in ':badbad' is empty.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua list this-is-ok :badbad
+
+cat >experr <<EOF
+kyua: E: Program name '/foo' must be relative to the test suite, not absolute.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua list this-is-ok /foo
+}
+
+
+utils_test_case many_args__no_match__all
+many_args__no_match__all_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >experr <<EOF
+kyua: W: No test cases matched by the filter 'first1'.
+EOF
+ atf_check -s exit:1 -o empty -e file:experr kyua list first1
+}
+
+
+utils_test_case many_args__no_match__some
+many_args__no_match__some_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+atf_test_program{name="third"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_all_pass second
+ utils_cp_helper simple_some_fail third
+
+ cat >expout <<EOF
+first:pass
+first:skip
+third:fail
+third:pass
+EOF
+
+ cat >experr <<EOF
+kyua: W: No test cases matched by the filter 'fifth'.
+kyua: W: No test cases matched by the filter 'fourth'.
+EOF
+ atf_check -s exit:1 -o file:expout -e file:experr kyua list first fourth \
+ third fifth
+}
+
+
+utils_test_case args_are_relative
+args_are_relative_body() {
+ mkdir root
+ cat >root/Kyuafile <<EOF
+syntax(2)
+test_suite("integration-1")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+include("subdir/Kyuafile")
+EOF
+ utils_cp_helper simple_all_pass root/first
+ utils_cp_helper simple_some_fail root/second
+
+ mkdir root/subdir
+ cat >root/subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration-2")
+atf_test_program{name="third"}
+atf_test_program{name="fourth"}
+EOF
+ utils_cp_helper simple_all_pass root/subdir/third
+ utils_cp_helper simple_some_fail root/subdir/fourth
+
+ cat >expout <<EOF
+first:pass (integration-1)
+first:skip (integration-1)
+subdir/fourth:fail (integration-2)
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list \
+ -v -k "$(pwd)/root/Kyuafile" first subdir/fourth:fail
+}
+
+
+utils_test_case only_load_used_test_programs
+only_load_used_test_programs_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper bad_test_program second
+
+ cat >expout <<EOF
+first:pass
+first:skip
+EOF
+ CREATE_COOKIE="$(pwd)/cookie"; export CREATE_COOKIE
+ atf_check -s exit:0 -o file:expout -e empty kyua list 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" <<EOF
+syntax(2)
+test_suites.suite1["the-variable"] = "value1"
+EOF
+
+ cat >Kyuafile <<EOF
+syntax(2)
+atf_test_program{name="config1", test_suite="suite1"}
+EOF
+ utils_cp_helper config config1
+
+ CONFIG_VAR_FILE="$(pwd)/cookie"; export CONFIG_VAR_FILE
+ if [ -f "${CONFIG_VAR_FILE}" ]; then
+ atf_fail "Cookie file already created; test case list may have gotten" \
+ "a bad configuration"
+ fi
+ atf_check -s exit:0 -o ignore -e empty kyua -c my-config list
+ [ -f "${CONFIG_VAR_FILE}" ] || \
+ atf_fail "Cookie file not created; test case list did not get" \
+ "configuration variables"
+ value="$(cat "${CONFIG_VAR_FILE}")"
+ [ "${value}" = "value1" ] || \
+ atf_fail "Invalid value (${value}) in cookie file; test case list did" \
+ "not get the correct configuration variables"
+}
+
+
+utils_test_case build_root_flag
+build_root_flag_body() {
+ mkdir subdir
+ mkdir build
+ mkdir build/subdir
+
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+include("subdir/Kyuafile")
+atf_test_program{name="first"}
+EOF
+ echo 'invalid' >first
+ utils_cp_helper simple_all_pass build/first
+
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("in-subdir")
+atf_test_program{name="second"}
+EOF
+ echo 'invalid' >subdir/second
+ utils_cp_helper simple_some_fail build/subdir/second
+
+ cat >expout <<EOF
+subdir/second:fail
+subdir/second:pass
+first:pass
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list --build-root=build \
+ subdir first:pass
+}
+
+
+utils_test_case kyuafile_flag__no_args
+kyuafile_flag__no_args_body() {
+ cat >Kyuafile <<EOF
+This file is bogus but it is not loaded.
+EOF
+
+ cat >myfile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="sometest"}
+EOF
+ utils_cp_helper simple_all_pass sometest
+
+ cat >expout <<EOF
+sometest:pass
+sometest:skip
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list -k myfile
+ atf_check -s exit:0 -o file:expout -e empty kyua list --kyuafile=myfile
+}
+
+
+utils_test_case kyuafile_flag__some_args
+kyuafile_flag__some_args_body() {
+ cat >Kyuafile <<EOF
+This file is bogus but it is not loaded.
+EOF
+
+ cat >myfile <<EOF
+syntax(2)
+test_suite("hello-world")
+atf_test_program{name="sometest"}
+EOF
+ utils_cp_helper simple_all_pass sometest
+
+ cat >expout <<EOF
+sometest:pass (hello-world)
+sometest:skip (hello-world)
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list -v -k myfile sometest
+ atf_check -s exit:0 -o file:expout -e empty kyua list -v --kyuafile=myfile \
+ sometest
+}
+
+
+utils_test_case verbose_flag
+verbose_flag_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration-suite-1")
+atf_test_program{name="simple_all_pass"}
+plain_test_program{name="i_am_plain", timeout=654}
+include("subdir/Kyuafile")
+EOF
+ utils_cp_helper simple_all_pass .
+ touch i_am_plain
+
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration-suite-2")
+atf_test_program{name="metadata"}
+EOF
+ utils_cp_helper metadata subdir
+
+ cat >expout <<EOF
+simple_all_pass:pass (integration-suite-1)
+simple_all_pass:skip (integration-suite-1)
+i_am_plain:main (integration-suite-1)
+ timeout = 654
+subdir/metadata:many_properties (integration-suite-2)
+ allowed_architectures = some-architecture
+ allowed_platforms = some-platform
+ custom.no-meaning = I am a custom variable
+ description = A description with some padding
+ required_configs = var1 var2 var3
+ required_files = /my/file1 /some/other/file
+ required_programs = /nonexistent/bin3 bin1 bin2
+ required_user = root
+subdir/metadata:no_properties (integration-suite-2)
+subdir/metadata:one_property (integration-suite-2)
+ description = Does nothing but has one metadata property
+subdir/metadata:with_cleanup (integration-suite-2)
+ has_cleanup = true
+ timeout = 250
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list -v
+ atf_check -s exit:0 -o file:expout -e empty kyua list --verbose
+}
+
+
+utils_test_case no_test_program_match
+no_test_program_match_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >experr <<EOF
+kyua: W: No test cases matched by the filter 'second'.
+EOF
+ atf_check -s exit:1 -o empty -e file:experr kyua list second
+}
+
+
+utils_test_case no_test_case_match
+no_test_case_match_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+EOF
+ utils_cp_helper simple_all_pass first
+
+ cat >experr <<EOF
+kyua: W: No test cases matched by the filter 'first:foobar'.
+EOF
+ atf_check -s exit:1 -o empty -e file:experr kyua list first:foobar
+}
+
+
+utils_test_case missing_kyuafile__no_args
+missing_kyuafile__no_args_body() {
+ cat >experr <<EOF
+kyua: E: Load of 'Kyuafile' failed: File 'Kyuafile' not found.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua list
+}
+
+
+utils_test_case missing_kyuafile__test_program
+missing_kyuafile__test_program_body() {
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="unused"}
+EOF
+ utils_cp_helper simple_all_pass subdir/unused
+
+ cat >experr <<EOF
+kyua: E: Load of 'Kyuafile' failed: File 'Kyuafile' not found.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua list subdir/unused
+}
+
+
+utils_test_case missing_kyuafile__subdir
+missing_kyuafile__subdir_body() {
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="unused"}
+EOF
+ utils_cp_helper simple_all_pass subdir/unused
+
+ cat >experr <<EOF
+kyua: E: Load of 'Kyuafile' failed: File 'Kyuafile' not found.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua list subdir
+}
+
+
+utils_test_case bogus_kyuafile
+bogus_kyuafile_body() {
+ cat >Kyuafile <<EOF
+Hello, world.
+EOF
+ atf_check -s exit:2 -o empty \
+ -e match:"Load of 'Kyuafile' failed: .* Kyuafile:2:" kyua list
+}
+
+
+utils_test_case bogus_test_program
+bogus_test_program_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="crash_on_list"}
+atf_test_program{name="non_executable"}
+EOF
+ utils_cp_helper bad_test_program crash_on_list
+ echo 'I am not executable' >non_executable
+
+ cat >expout <<EOF
+crash_on_list:__test_cases_list__
+non_executable:__test_cases_list__
+EOF
+ atf_check -s exit:0 -o file:expout -e empty kyua list
+}
+
+
+utils_test_case missing_test_program
+missing_test_program_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+include("subdir/Kyuafile")
+EOF
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="ok"}
+atf_test_program{name="i-am-missing"}
+EOF
+ echo 'I should not be touched because the Kyuafile is bogus' >subdir/ok
+
+# CHECK_STYLE_DISABLE
+ cat >experr <<EOF
+kyua: E: Load of 'Kyuafile' failed: .*Non-existent test program 'subdir/i-am-missing'.
+EOF
+# CHECK_STYLE_ENABLE
+ atf_check -s exit:2 -o empty -e "match:$(cat experr)" kyua list
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case no_args
+ atf_add_test_case one_arg__subdir
+ atf_add_test_case one_arg__test_case
+ atf_add_test_case one_arg__test_program
+ atf_add_test_case one_arg__invalid
+ atf_add_test_case many_args__ok
+ atf_add_test_case many_args__invalid
+ atf_add_test_case many_args__no_match__all
+ atf_add_test_case many_args__no_match__some
+
+ 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__no_args
+ atf_add_test_case kyuafile_flag__some_args
+
+ atf_add_test_case verbose_flag
+
+ atf_add_test_case no_test_program_match
+ atf_add_test_case no_test_case_match
+
+ atf_add_test_case missing_kyuafile__no_args
+ atf_add_test_case missing_kyuafile__test_program
+ atf_add_test_case missing_kyuafile__subdir
+
+ atf_add_test_case bogus_kyuafile
+ atf_add_test_case bogus_test_program
+ atf_add_test_case missing_test_program
+}
diff --git a/integration/cmd_report_html_test.sh b/integration/cmd_report_html_test.sh
new file mode 100755
index 000000000000..9c9b4ba81c86
--- /dev/null
+++ b/integration/cmd_report_html_test.sh
@@ -0,0 +1,267 @@
+# 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.
+
+
+# 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="simple_all_pass"}
+atf_test_program{name="simple_some_fail"}
+atf_test_program{name="metadata"}
+EOF
+
+ utils_cp_helper simple_all_pass .
+ utils_cp_helper simple_some_fail .
+ utils_cp_helper metadata .
+ atf_check -s exit:1 -o save:stdout -e empty env MOCK="${mock_env}" kyua test
+ grep '^Results saved to ' stdout | cut -d ' ' -f 4 >"${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 <<EOF
+kyua: E: Output directory 'html' already exists; maybe use --force?.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua report-html
+ test ! -f html/index.html || atf_fail "Not expected file created"
+}
+
+
+utils_test_case output__explicit
+output__explicit_body() {
+ run_tests "mock1" unused_dbfile_name
+
+ mkdir output
+ atf_check -s exit:0 -o ignore -e empty kyua report-html --output=output/foo
+ test ! -d html || atf_fail "Not expected directory created"
+ test -f output/foo/index.html || atf_fail "Expected file not created"
+}
+
+
+utils_test_case results_filter__ok
+results_filter__ok_body() {
+ run_tests "mock1" unused_dbfile_name
+
+ atf_check -s exit:0 -o ignore -e empty kyua report-html \
+ --results-filter=passed
+ for f in \
+ html/index.html \
+ html/context.html \
+ html/simple_all_pass_pass.html \
+ html/simple_some_fail_pass.html \
+ html/metadata_no_properties.html \
+ html/metadata_with_cleanup.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_pass.html \
+ "This is the stdout of pass" "This is the stderr of pass"
+ check_not_in_file html/simple_all_pass_pass.html \
+ "This is the stdout of skip" "This is the stderr of skip" \
+ "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_pass.html \
+ "Test case did not write anything to stdout" \
+ "Test case did not write anything to stderr"
+ check_not_in_file html/simple_some_fail_pass.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" \
+ "This is the stdout of fail" "This is the stderr of fail"
+}
+
+
+utils_test_case results_filter__invalid
+results_filter__invalid_body() {
+ echo "kyua: E: Unknown result type 'foo-bar'." >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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="simple_all_pass"}
+EOF
+
+ utils_cp_helper simple_all_pass .
+ atf_check -s exit:0 -o save:stdout -e empty env MOCK="${mock_env}" kyua test
+ grep '^Results saved to ' stdout | cut -d ' ' -f 4 >"${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 \"</properties>\";
+ skip = 0;
+ next;
+}
+
+/<properties>/ {
+ print \"<properties>\";
+ 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 <<EOF
+<?xml version="1.0" encoding="iso-8859-1"?>
+<testsuite>
+<properties>
+CONTENTS STRIPPED BY TEST
+</properties>
+<testcase classname="simple_all_pass" name="pass" time="S.UUU">
+<system-out>This is the stdout of pass
+</system-out>
+<system-err>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
+</system-err>
+</testcase>
+<testcase classname="simple_all_pass" name="skip" time="S.UUU">
+<skipped/>
+<system-out>This is the stdout of skip
+</system-out>
+<system-err>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
+</system-err>
+</testcase>
+</testsuite>
+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 <<EOF
+<?xml version="1.0" encoding="iso-8859-1"?>
+<testsuite>
+<properties>
+CONTENTS STRIPPED BY TEST
+</properties>
+<testcase classname="simple_all_pass" name="pass" time="S.UUU">
+<system-out>This is the stdout of pass
+</system-out>
+<system-err>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
+</system-err>
+</testcase>
+<testcase classname="simple_all_pass" name="skip" time="S.UUU">
+<skipped/>
+<system-out>This is the stdout of skip
+</system-out>
+<system-err>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
+</system-err>
+</testcase>
+</testsuite>
+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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="simple_all_pass"}
+EOF
+
+ utils_cp_helper simple_all_pass .
+ atf_check -s exit:0 -o save:stdout -e empty env \
+ MOCK="${mock_env}" _='fake-value' kyua test
+ grep '^Results saved to ' stdout | cut -d ' ' -f 4 >"${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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+kyua: W: No test cases matched by the filter 'first'.
+kyua: W: No test cases matched by the filter 'simple_all_pass:second'.
+EOF
+ atf_check -s exit:1 -o file:expout -e file:experr kyua report \
+ first simple_all_pass:skip simple_all_pass:second
+}
+
+
+utils_test_case verbose
+verbose_body() {
+ # Switch to the current directory using its physical location and update
+ # HOME accordingly. Otherwise, the test below where we compare the value
+ # of HOME in the output might fail if the path to HOME contains a symlink
+ # (as is the case in OS X when HOME points to the temporary directory.)
+ local real_cwd="$(pwd -P)"
+ cd "${real_cwd}"
+ HOME="${real_cwd}"
+
+ run_tests "mock1
+has multiple lines
+and terminates here" dbfile_name
+
+ cat >expout <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+===> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="simple_all_pass"}
+EOF
+
+ cat >expout <<EOF
+simple_all_pass:pass -> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="simple_some_fail"}
+EOF
+
+ cat >expout <<EOF
+simple_some_fail:fail -> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+atf_test_program{name="third"}
+plain_test_program{name="fourth", required_files="/non-existent/foo"}
+EOF
+
+ cat >expout <<EOF
+first:pass -> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+atf_test_program{name="third"}
+plain_test_program{name="fourth"}
+EOF
+
+ cat >expout <<EOF
+first:fail -> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="expect_all_pass"}
+EOF
+
+# CHECK_STYLE_DISABLE
+ cat >expout <<EOF
+expect_all_pass:die -> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="expect_some_fail"}
+EOF
+
+# CHECK_STYLE_DISABLE
+ cat >expout <<EOF
+expect_some_fail:die -> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="bogus_test_cases"}
+EOF
+
+# CHECK_STYLE_DISABLE
+ cat >expout <<EOF
+bogus_test_cases:die -> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="simple_all_pass"}
+include("subdir/Kyuafile")
+EOF
+ utils_cp_helper metadata .
+ utils_cp_helper simple_all_pass .
+
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration2")
+atf_test_program{name="simple_some_fail"}
+EOF
+ utils_cp_helper simple_some_fail subdir
+
+ cat >expout <<EOF
+simple_all_pass:pass -> 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 <<EOF
+syntax(2)
+test_suite("top-level")
+include("subdir/Kyuafile")
+EOF
+
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("in-subdir")
+atf_test_program{name="simple_all_pass"}
+EOF
+ utils_cp_helper simple_all_pass subdir
+
+# CHECK_STYLE_DISABLE
+ cat >expout <<EOF
+subdir/simple_all_pass:pass -> 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 <<EOF
+syntax(2)
+test_suite("top-level")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >expout <<EOF
+first:skip -> 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 <<EOF
+syntax(2)
+test_suite("top-level")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_some_fail second
+
+ cat >expout <<EOF
+second:fail -> 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 <<EOF
+kyua: E: Test case component in 'foo:' is empty.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua test foo:
+
+cat >experr <<EOF
+kyua: E: Program name '/a/b' must be relative to the test suite, not absolute.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua test /a/b
+}
+
+
+utils_test_case many_args__ok
+many_args__ok_body() {
+ utils_install_stable_test_wrapper
+
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+include("subdir/Kyuafile")
+atf_test_program{name="first"}
+EOF
+ utils_cp_helper simple_all_pass first
+
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("in-subdir")
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_some_fail subdir/second
+
+ cat >expout <<EOF
+first:pass -> 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 <<EOF
+kyua: E: Program name component in ':badbad' is empty.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua test this-is-ok :badbad
+
+cat >experr <<EOF
+kyua: E: Program name '/foo' must be relative to the test suite, not absolute.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua test this-is-ok /foo
+}
+
+
+utils_test_case many_args__no_match__all
+many_args__no_match__all_body() {
+ utils_install_stable_test_wrapper
+
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >expout <<EOF
+Results file id is $(utils_results_id)
+Results saved to $(utils_results_file)
+EOF
+ cat >experr <<EOF
+kyua: W: No test cases matched by the filter 'first1'.
+EOF
+ atf_check -s exit:1 -o file:expout -e file:experr kyua test first1
+}
+
+
+utils_test_case many_args__no_match__some
+many_args__no_match__some_body() {
+ utils_install_stable_test_wrapper
+
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("top-level")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+atf_test_program{name="third"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_all_pass second
+ utils_cp_helper simple_some_fail third
+
+ cat >expout <<EOF
+first:pass -> 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 <<EOF
+kyua: W: No test cases matched by the filter 'fifth'.
+kyua: W: No test cases matched by the filter 'fourth'.
+EOF
+ atf_check -s exit:1 -o file:expout -e file:experr kyua test first fourth \
+ third fifth
+}
+
+
+utils_test_case args_are_relative
+args_are_relative_body() {
+ utils_install_stable_test_wrapper
+
+ mkdir root
+ cat >root/Kyuafile <<EOF
+syntax(2)
+test_suite("integration-1")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+include("subdir/Kyuafile")
+EOF
+ utils_cp_helper simple_all_pass root/first
+ utils_cp_helper simple_some_fail root/second
+
+ mkdir root/subdir
+ cat >root/subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration-2")
+atf_test_program{name="third"}
+atf_test_program{name="fourth"}
+EOF
+ utils_cp_helper simple_all_pass root/subdir/third
+ utils_cp_helper simple_some_fail root/subdir/fourth
+
+ cat >expout <<EOF
+first:pass -> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+atf_test_program{name="second"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper bad_test_program second
+
+ cat >expout <<EOF
+first:pass -> 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" <<EOF
+syntax(2)
+test_suites.suite1["the-variable"] = "value1"
+test_suites.suite2["the-variable"] = "override me"
+EOF
+
+ cat >Kyuafile <<EOF
+syntax(2)
+atf_test_program{name="config1", test_suite="suite1"}
+atf_test_program{name="config2", test_suite="suite2"}
+atf_test_program{name="config3", test_suite="suite3"}
+EOF
+ utils_cp_helper config config1
+ utils_cp_helper config config2
+ utils_cp_helper config config3
+
+ atf_check -s exit:1 -o save:stdout -e empty \
+ kyua -c my-config -v test_suites.suite2.the-variable=value2 test
+ atf_check -s exit:0 -o ignore -e empty \
+ grep 'config1:get_variable.*failed' stdout
+ atf_check -s exit:0 -o ignore -e empty \
+ grep 'config2:get_variable.*passed' stdout
+ atf_check -s exit:0 -o ignore -e empty \
+ grep 'config3:get_variable.*skipped' stdout
+
+ CONFIG_VAR_FILE="$(pwd)/cookie"; export CONFIG_VAR_FILE
+ if [ -f "${CONFIG_VAR_FILE}" ]; then
+ atf_fail "Cookie file already created; test case list may have gotten" \
+ "a bad configuration"
+ fi
+ atf_check -s exit:1 -o ignore -e empty kyua -c my-config test config1
+ [ -f "${CONFIG_VAR_FILE}" ] || \
+ atf_fail "Cookie file not created; test case list did not get" \
+ "configuration variables"
+ value="$(cat "${CONFIG_VAR_FILE}")"
+ [ "${value}" = "value1" ] || \
+ atf_fail "Invalid value (${value}) in cookie file; test case list did" \
+ "not get the correct configuration variables"
+}
+
+
+utils_test_case store_contents
+store_contents_body() {
+ utils_install_stable_test_wrapper
+
+ cat >Kyuafile <<EOF
+syntax(2)
+atf_test_program{name="some-program", test_suite="suite1"}
+EOF
+ utils_cp_helper simple_some_fail some-program
+ cat >expout <<EOF
+some-program:fail -> 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 <<EOF
+some-program,fail,failed,This fails on purpose
+some-program,pass,passed,NULL
+EOF
+ atf_check -s exit:0 -o file:expout -e empty \
+ kyua db-exec --no-headers \
+ "SELECT " \
+ " test_programs.relative_path, test_cases.name, " \
+ " test_results.result_type, test_results.result_reason " \
+ "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.relative_path, test_cases.name"
+}
+
+
+utils_test_case results_file__ok
+results_file__ok_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+atf_test_program{name="config1", test_suite="suite1"}
+EOF
+ utils_cp_helper config config1
+
+ atf_check -s exit:0 -o ignore -e empty kyua test -r foo1.db
+ test -f foo1.db || atf_fail "-s did not work"
+ atf_check -s exit:0 -o ignore -e empty kyua test --results-file=foo2.db
+ test -f foo2.db || atf_fail "--results-file did not work"
+ test ! -f .kyua/store.db || atf_fail "Default database created"
+}
+
+
+utils_test_case results_file__fail
+results_file__fail_body() {
+ cat >Kyuafile <<EOF
+syntax(2)
+atf_test_program{name="config1", test_suite="suite1"}
+EOF
+ utils_cp_helper config config1
+
+ atf_check -s exit:3 -o empty -e match:"Invalid.*--results-file" \
+ kyua test --results-file=
+}
+
+
+utils_test_case results_file__reuse
+results_file__reuse_body() {
+ utils_install_stable_test_wrapper
+
+ cat >Kyuafile <<EOF
+syntax(2)
+atf_test_program{name="simple_all_pass", test_suite="integration"}
+EOF
+ utils_cp_helper simple_all_pass .
+ atf_check -s exit:0 -o ignore -e empty kyua test -r results.db
+
+ atf_check -s exit:2 -o empty -e match:"results.db already exists" \
+ kyua test --results-file="results.db"
+}
+
+
+utils_test_case build_root_flag
+build_root_flag_body() {
+ utils_install_stable_test_wrapper
+
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+include("subdir/Kyuafile")
+EOF
+
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="second"}
+atf_test_program{name="third"}
+EOF
+
+ cat >expout <<EOF
+first:pass -> 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 <<EOF
+This file is bogus but it is not loaded.
+EOF
+
+ cat >myfile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="sometest"}
+EOF
+ utils_cp_helper simple_all_pass sometest
+
+ cat >expout <<EOF
+sometest:pass -> 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 <<EOF
+This file is bogus but it is not loaded.
+EOF
+
+ cat >myfile <<EOF
+syntax(2)
+test_suite("hello-world")
+atf_test_program{name="sometest"}
+EOF
+ utils_cp_helper simple_all_pass sometest
+
+ cat >expout <<EOF
+sometest:pass -> 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 <<EOF
+sometest:pass -> 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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="interrupts"}
+EOF
+ utils_cp_helper interrupts .
+
+ kyua \
+ -v test_suites.integration.body-cookie="$(pwd)/body" \
+ -v test_suites.integration.cleanup-cookie="$(pwd)/cleanup" \
+ test >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 <<EOF
+syntax(2)
+test_suite("integration")
+EOF
+ for i in $(seq 100); do
+ echo 'plain_test_program{name="race", is_exclusive=true}' >>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 <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+EOF
+ utils_cp_helper simple_all_pass first
+ utils_cp_helper simple_all_pass second
+
+ cat >expout <<EOF
+Results file id is $(utils_results_id)
+Results saved to $(utils_results_file)
+EOF
+ cat >experr <<EOF
+kyua: W: No test cases matched by the filter 'second'.
+EOF
+ atf_check -s exit:1 -o file:expout -e file:experr kyua test second
+}
+
+
+utils_test_case no_test_case_match
+no_test_case_match_body() {
+ utils_install_stable_test_wrapper
+
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="first"}
+EOF
+ utils_cp_helper simple_all_pass first
+
+ cat >expout <<EOF
+Results file id is $(utils_results_id)
+Results saved to $(utils_results_file)
+EOF
+ cat >experr <<EOF
+kyua: W: No test cases matched by the filter 'first:foobar'.
+EOF
+ atf_check -s exit:1 -o file:expout -e file:experr kyua test first:foobar
+}
+
+
+utils_test_case missing_kyuafile__no_args
+missing_kyuafile__no_args_body() {
+ cat >experr <<EOF
+kyua: E: Load of 'Kyuafile' failed: File 'Kyuafile' not found.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua test
+}
+
+
+utils_test_case missing_kyuafile__test_program
+missing_kyuafile__test_program_body() {
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="unused"}
+EOF
+ utils_cp_helper simple_all_pass subdir/unused
+
+ cat >experr <<EOF
+kyua: E: Load of 'Kyuafile' failed: File 'Kyuafile' not found.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua test subdir/unused
+}
+
+
+utils_test_case missing_kyuafile__subdir
+missing_kyuafile__subdir_body() {
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="unused"}
+EOF
+ utils_cp_helper simple_all_pass subdir/unused
+
+ cat >experr <<EOF
+kyua: E: Load of 'Kyuafile' failed: File 'Kyuafile' not found.
+EOF
+ atf_check -s exit:2 -o empty -e file:experr kyua test subdir
+}
+
+
+utils_test_case bogus_config
+bogus_config_body() {
+ mkdir .kyua
+ cat >"${HOME}/.kyua/kyua.conf" <<EOF
+Hello, world.
+EOF
+
+ file_re='.*\.kyua/kyua.conf'
+ atf_check -s exit:2 -o empty \
+ -e match:"^kyua: E: Load of '${file_re}' failed: Failed to load Lua" \
+ kyua test
+}
+
+
+utils_test_case bogus_kyuafile
+bogus_kyuafile_body() {
+ cat >Kyuafile <<EOF
+Hello, world.
+EOF
+ atf_check -s exit:2 -o empty \
+ -e match:"Load of 'Kyuafile' failed: .* Kyuafile:2:" kyua list
+}
+
+
+utils_test_case bogus_test_program
+bogus_test_program_body() {
+ utils_install_stable_test_wrapper
+
+ cat >Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="crash_on_list"}
+atf_test_program{name="non_executable"}
+EOF
+ utils_cp_helper bad_test_program crash_on_list
+ echo 'I am not executable' >non_executable
+
+# CHECK_STYLE_DISABLE
+ cat >expout <<EOF
+crash_on_list:__test_cases_list__ -> 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 <<EOF
+syntax(2)
+include("subdir/Kyuafile")
+EOF
+ mkdir subdir
+ cat >subdir/Kyuafile <<EOF
+syntax(2)
+test_suite("integration")
+atf_test_program{name="ok"}
+atf_test_program{name="i-am-missing"}
+EOF
+ echo 'I should not be touched because the Kyuafile is bogus' >subdir/ok
+
+# CHECK_STYLE_DISABLE
+ cat >experr <<EOF
+kyua: E: Load of 'Kyuafile' failed: .*Non-existent test program 'subdir/i-am-missing'.
+EOF
+# CHECK_STYLE_ENABLE
+ atf_check -s exit:2 -o empty -e "match:$(cat experr)" kyua list
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case one_test_program__all_pass
+ atf_add_test_case one_test_program__some_fail
+ atf_add_test_case many_test_programs__all_pass
+ atf_add_test_case many_test_programs__some_fail
+ atf_add_test_case expect__all_pass
+ atf_add_test_case expect__some_fail
+ atf_add_test_case premature_exit
+
+ atf_add_test_case no_args
+ atf_add_test_case one_arg__subdir
+ atf_add_test_case one_arg__test_case
+ atf_add_test_case one_arg__test_program
+ atf_add_test_case one_arg__invalid
+ atf_add_test_case many_args__ok
+ atf_add_test_case many_args__invalid
+ atf_add_test_case many_args__no_match__all
+ atf_add_test_case many_args__no_match__some
+
+ 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 store_contents
+ atf_add_test_case results_file__ok
+ atf_add_test_case results_file__fail
+ atf_add_test_case results_file__reuse
+
+ atf_add_test_case build_root_flag
+
+ atf_add_test_case kyuafile_flag__no_args
+ atf_add_test_case kyuafile_flag__some_args
+
+ atf_add_test_case interrupt
+
+ atf_add_test_case exclusive_tests
+
+ atf_add_test_case no_test_program_match
+ atf_add_test_case no_test_case_match
+
+ atf_add_test_case missing_kyuafile__no_args
+ atf_add_test_case missing_kyuafile__test_program
+ atf_add_test_case missing_kyuafile__subdir
+
+ atf_add_test_case bogus_config
+ atf_add_test_case bogus_kyuafile
+ atf_add_test_case bogus_test_program
+ atf_add_test_case missing_test_program
+}
diff --git a/integration/global_test.sh b/integration/global_test.sh
new file mode 100755
index 000000000000..9cc8be2d1dec
--- /dev/null
+++ b/integration/global_test.sh
@@ -0,0 +1,146 @@
+# 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 no_args
+no_args_body() {
+ cat >experr <<EOF
+Usage error: No command provided.
+Type 'kyua help' for usage information.
+EOF
+
+ atf_check -s exit:3 -o empty -e file:experr kyua
+}
+
+
+utils_test_case unknown_option
+unknown_option_body() {
+ cat >experr <<EOF
+Usage error: Unknown option --this_is_unknown.
+Type 'kyua help' for usage information.
+EOF
+
+ atf_check -s exit:3 -o empty -e file:experr kyua --this_is_unknown
+}
+
+
+utils_test_case unknown_command
+unknown_command_body() {
+ cat >experr <<EOF
+Usage error: Unknown command 'i_am_not_known'.
+Type 'kyua help' for usage information.
+EOF
+
+ atf_check -s exit:3 -o empty -e file:experr kyua i_am_not_known
+}
+
+
+utils_test_case logfile__default
+logfile__default_body() {
+ atf_check -s exit:0 test ! -d .kyua/logs/
+ atf_check -s exit:3 -o empty -e ignore kyua
+ atf_check -s exit:0 test -d .kyua/logs/
+}
+
+
+utils_test_case logfile__override
+logfile__override_body() {
+ atf_check -s exit:0 test ! -f test.log
+ atf_check -s exit:3 -o empty -e ignore kyua --logfile=test.log
+
+ atf_check -s exit:0 test ! -d .kyua/logs/
+ atf_check -s exit:0 test -f test.log
+
+ grep ' E .* No command provided' test.log || atf_fail "Log file does" \
+ "contain required message"
+}
+
+
+utils_test_case loglevel__default
+loglevel__default_body() {
+ atf_check -s exit:0 test ! -f test.log
+ atf_check -s exit:3 -o empty -e ignore kyua --logfile=test.log
+
+ atf_check -s exit:0 test ! -d .kyua/logs/
+ atf_check -s exit:0 test -f test.log
+
+ grep ' E .* No command provided' test.log || atf_fail "Log file does" \
+ "contain required message"
+ if grep ' D ' test.log; then
+ atf_fail "Log file contains debug messages but it should not"
+ fi
+}
+
+
+utils_test_case loglevel__lower
+loglevel__lower_body() {
+ atf_check -s exit:0 test ! -f test.log
+ atf_check -s exit:3 -o empty -e ignore kyua --logfile=test.log \
+ --loglevel=warning
+
+ atf_check -s exit:0 test ! -d .kyua/logs/
+ atf_check -s exit:0 test -f test.log
+
+ grep ' E .* No command provided' test.log || atf_fail "Log file does" \
+ "contain required message"
+ if grep ' I ' test.log; then
+ atf_fail "Log file contains info messages but it should not"
+ fi
+}
+
+
+utils_test_case loglevel__higher
+loglevel__higher_body() {
+ atf_check -s exit:0 test ! -f test.log
+ atf_check -s exit:3 -o empty -e ignore kyua --logfile=test.log \
+ --loglevel=debug
+
+ atf_check -s exit:0 test ! -d .kyua/logs/
+ atf_check -s exit:0 test -f test.log
+
+ grep ' E .* No command provided' test.log || atf_fail "Log file does" \
+ "contain required message"
+ grep ' D ' test.log || atf_fail "Log file does not contain debug messages"
+}
+
+
+atf_init_test_cases() {
+ atf_add_test_case no_args
+ atf_add_test_case unknown_option
+ atf_add_test_case unknown_command
+
+ atf_add_test_case logfile__default
+ atf_add_test_case logfile__override
+
+ atf_add_test_case loglevel__default
+ atf_add_test_case loglevel__lower
+ atf_add_test_case loglevel__higher
+
+ # Tests for the global configuration-related flags are found in the
+ # cmd_config_test test program.
+}
diff --git a/integration/helpers/.gitignore b/integration/helpers/.gitignore
new file mode 100644
index 000000000000..11634bcfeb85
--- /dev/null
+++ b/integration/helpers/.gitignore
@@ -0,0 +1,11 @@
+bad_test_program
+bogus_test_cases
+config
+dump_env
+expect_all_pass
+expect_some_fail
+interrupts
+metadata
+race
+simple_all_pass
+simple_some_fail
diff --git a/integration/helpers/Makefile.am.inc b/integration/helpers/Makefile.am.inc
new file mode 100644
index 000000000000..d835aba51c67
--- /dev/null
+++ b/integration/helpers/Makefile.am.inc
@@ -0,0 +1,90 @@
+# 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_integration_helpersdir = $(pkgtestsdir)/integration/helpers
+
+tests_integration_helpers_PROGRAMS = integration/helpers/bad_test_program
+integration_helpers_bad_test_program_SOURCES = \
+ integration/helpers/bad_test_program.cpp
+
+tests_integration_helpers_PROGRAMS += integration/helpers/bogus_test_cases
+integration_helpers_bogus_test_cases_SOURCES = \
+ integration/helpers/bogus_test_cases.cpp
+integration_helpers_bogus_test_cases_CXXFLAGS = $(ATF_CXX_CFLAGS)
+integration_helpers_bogus_test_cases_LDADD = $(ATF_CXX_LIBS)
+
+tests_integration_helpers_PROGRAMS += integration/helpers/config
+integration_helpers_config_SOURCES = integration/helpers/config.cpp
+integration_helpers_config_CXXFLAGS = $(ATF_CXX_CFLAGS)
+integration_helpers_config_LDADD = $(ATF_CXX_LIBS)
+
+tests_integration_helpers_PROGRAMS += integration/helpers/dump_env
+integration_helpers_dump_env_SOURCES = integration/helpers/dump_env.cpp
+integration_helpers_dump_env_CXXFLAGS = $(UTILS_CFLAGS)
+integration_helpers_dump_env_LDADD = $(UTILS_LIBS)
+
+tests_integration_helpers_PROGRAMS += integration/helpers/expect_all_pass
+integration_helpers_expect_all_pass_SOURCES = \
+ integration/helpers/expect_all_pass.cpp
+integration_helpers_expect_all_pass_CXXFLAGS = $(ATF_CXX_CFLAGS)
+integration_helpers_expect_all_pass_LDADD = $(ATF_CXX_LIBS)
+
+tests_integration_helpers_PROGRAMS += integration/helpers/expect_some_fail
+integration_helpers_expect_some_fail_SOURCES = \
+ integration/helpers/expect_some_fail.cpp
+integration_helpers_expect_some_fail_CXXFLAGS = $(ATF_CXX_CFLAGS)
+integration_helpers_expect_some_fail_LDADD = $(ATF_CXX_LIBS)
+
+tests_integration_helpers_PROGRAMS += integration/helpers/interrupts
+integration_helpers_interrupts_SOURCES = integration/helpers/interrupts.cpp
+integration_helpers_interrupts_CXXFLAGS = $(ATF_CXX_CFLAGS)
+integration_helpers_interrupts_LDADD = $(ATF_CXX_LIBS)
+
+tests_integration_helpers_PROGRAMS += integration/helpers/metadata
+integration_helpers_metadata_SOURCES = integration/helpers/metadata.cpp
+integration_helpers_metadata_CXXFLAGS = $(ATF_CXX_CFLAGS)
+integration_helpers_metadata_LDADD = $(ATF_CXX_LIBS)
+
+tests_integration_helpers_PROGRAMS += integration/helpers/race
+integration_helpers_race_SOURCES = integration/helpers/race.cpp
+integration_helpers_race_CXXFLAGS = $(UTILS_CFLAGS)
+integration_helpers_race_LDADD = $(UTILS_LIBS)
+
+tests_integration_helpers_PROGRAMS += integration/helpers/simple_all_pass
+integration_helpers_simple_all_pass_SOURCES = \
+ integration/helpers/simple_all_pass.cpp
+integration_helpers_simple_all_pass_CXXFLAGS = $(ATF_CXX_CFLAGS)
+integration_helpers_simple_all_pass_LDADD = $(ATF_CXX_LIBS)
+
+tests_integration_helpers_PROGRAMS += integration/helpers/simple_some_fail
+integration_helpers_simple_some_fail_SOURCES = \
+ integration/helpers/simple_some_fail.cpp
+integration_helpers_simple_some_fail_CXXFLAGS = $(ATF_CXX_CFLAGS)
+integration_helpers_simple_some_fail_LDADD = $(ATF_CXX_LIBS)
+endif
diff --git a/integration/helpers/bad_test_program.cpp b/integration/helpers/bad_test_program.cpp
new file mode 100644
index 000000000000..210709ee8976
--- /dev/null
+++ b/integration/helpers/bad_test_program.cpp
@@ -0,0 +1,50 @@
+// 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 <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+
+
+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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+
+#include <atf-c++.hpp>
+
+
+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 <cstdlib>
+
+#include <atf-c++.hpp>
+
+
+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 <cstdlib>
+#include <iostream>
+
+#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 <prefix> <continuation-prefix>\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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+
+#include <atf-c++.hpp>
+
+#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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+
+#include <atf-c++.hpp>
+
+
+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 <unistd.h>
+}
+
+#include <atf-c++.hpp>
+
+#include <fstream>
+
+
+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 <cstdlib>
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <sys/types.h>
+
+#include <unistd.h>
+}
+
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+
+#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 <iostream>
+
+#include <atf-c++.hpp>
+
+
+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 <iostream>
+
+#include <atf-c++.hpp>
+
+
+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 <<EOF
+#! /bin/sh
+
+PATH=${PATH}
+
+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 <<EOF
+#! /bin/sh
+
+PATH=${PATH}
+
+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 <bkoz@redhat.com>
+# Copyright (c) 2012 Zack Weinberg <zackw@panix.com>
+# Copyright (c) 2013 Roy Stogner <roystgnr@ices.utexas.edu>
+# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov <sokolov@google.com>
+# Copyright (c) 2015 Paul Norman <penorman@mac.com>
+# Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+# Copyright (c) 2016, 2018 Krzesimir Nowak <qdlacz@gmail.com>
+# Copyright (c) 2019 Enji Cooper <yaneurabeya@gmail.com>
+#
+# 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 <typename T>
+ 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<void> single_type;
+ typedef check<check<void>> double_type;
+ typedef check<check<check<void>>> triple_type;
+ typedef check<check<check<check<void>>>> 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<T, T>
+ {
+ 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<int, decltype(0)>::value == true, "");
+ static_assert(is_same<int, decltype(c)>::value == false, "");
+ static_assert(is_same<int, decltype(v)>::value == false, "");
+ auto ac = c;
+ auto av = v;
+ auto sumi = ac + av + 'x';
+ auto sumf = ac + av + 1.0;
+ static_assert(is_same<int, decltype(ac)>::value == true, "");
+ static_assert(is_same<int, decltype(av)>::value == true, "");
+ static_assert(is_same<int, decltype(sumi)>::value == true, "");
+ static_assert(is_same<int, decltype(sumf)>::value == false, "");
+ static_assert(is_same<int, decltype(add(c, v))>::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 <int...>
+ struct sum;
+
+ template <int N0, int... N1toN>
+ struct sum<N0, N1toN...>
+ {
+ static constexpr auto value = N0 + sum<N1toN...>::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<typename T>
+ using member = typename T::member_type;
+
+ template<typename T>
+ void func(...) {}
+
+ template<typename T>
+ void func(member<T>*) {}
+
+ void test();
+
+ void test() { func<foo>(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<T, T>
+ {
+ static constexpr auto value = true;
+ };
+
+ int
+ test()
+ {
+ auto x = 0;
+ static_assert(is_same<int, decltype(f(x))>::value, "");
+ static_assert(is_same<int&, decltype(g(x))>::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 <initializer_list>
+#include <utility>
+#include <type_traits>
+
+namespace cxx17
+{
+
+ namespace test_constexpr_lambdas
+ {
+
+ constexpr int foo = [](){return 42;}();
+
+ }
+
+ namespace test::nested_namespace::definitions
+ {
+
+ }
+
+ namespace test_fold_expression
+ {
+
+ template<typename... Args>
+ int multiply(Args... args)
+ {
+ return (args * ... * 1);
+ }
+
+ template<typename... Args>
+ 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<std::initializer_list<int>, decltype(foo)>::value);
+ static_assert(std::is_same<int, decltype(bar)>::value);
+ }
+
+ namespace test_typename_in_template_template_parameter
+ {
+
+ template<template<typename> 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 <bool cond>
+ 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 <typename T1, typename T2>
+ 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 <auto n>
+ struct B
+ {};
+
+ B<5> b1;
+ B<'a'> b2;
+
+ }
+
+ namespace test_structured_bindings
+ {
+
+ int arr[2] = { 1, 2 };
+ std::pair<int, int> pr = { 1, 2 };
+
+ auto f1() -> int(&)[2]
+ {
+ return arr;
+ }
+
+ auto f2() -> std::pair<int, int>&
+ {
+ 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<typename T>
+ Bad
+ f(T*, T*);
+
+ template<typename T1, typename T2>
+ Good
+ f(T1*, T2*);
+
+ static_assert (std::is_same_v<Good, decltype(f(g1, g2))>);
+
+ }
+
+ namespace test_inline_variables
+ {
+
+ template<class T> void f(T)
+ {}
+
+ template<class T> 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 <stdio.h>], [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 <stdlib.h>
+#include <unistd.h>
+], [
+ 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 <sys/stat.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+], [
+ 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 <stdlib.h>
+#include <string.h>
+#include <unistd.h>], [
+ 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 <stdlib.h>
+#include <unistd.h>
+
+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 <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+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 <sys/types.h>
+#endif
+#if defined(HAVE_SYS_PARAM_H)
+# include <sys/param.h>
+#endif
+#if defined(HAVE_SYS_SYSCTL_H)
+# include <sys/sysctl.h>
+#endif
+#include <stdint.h>
+#include <stdlib.h>
+], [
+ 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 <err.h>
+#include <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdio.h>
+#include <stdlib.h>], [
+ 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 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!--
+ 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.
+-->
+<html>
+<head>
+ <title>Execution context</title>
+ <link rel="stylesheet" type="text/css" href="%%css%%" />
+</head>
+
+<body>
+
+<h1>Execution context</h1>
+
+<ul>
+ <li>Work directory: %%cwd%%</li>
+</ul>
+
+<h2>Environment variables</h2>
+
+<ul>
+%loop env_var iter
+ <li>%%env_var(iter)%%: %%env_var_value(iter)%%</li>
+%endloop
+</ul>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!--
+ 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.
+-->
+
+<html>
+<head>
+ <title>Tests summary</title>
+ <link rel="stylesheet" type="text/css" href="%%css%%" />
+</head>
+
+<body>
+
+
+<h1>Summary of test results</h1>
+
+<p class="overall">Overall result:
+%if bad_tests_count
+ <font class="bad">%%bad_tests_count%% TESTS FAILING</font>
+%else
+ <font class="good">ALL TESTS PASSING</font>
+%endif
+</p>
+
+<table class="tests-count">
+ <thead>
+ <tr>
+ <td>Test case result</td>
+ <td>Count</td>
+ </tr>
+ </thead>
+
+ <tbody>
+%if length(broken_test_cases)
+ <tr class="bad">
+ <td><a href="#broken">Broken</a></td>
+ <td class="numeric">%%length(broken_test_cases)%%</td>
+ </tr>
+%else
+ <tr>
+ <td>Broken</td>
+ <td class="numeric">%%broken_tests_count%%</td>
+ </tr>
+%endif
+%if length(failed_test_cases)
+ <tr class="bad">
+ <td><a href="#failed">Failed</a></td>
+ <td class="numeric">%%length(failed_test_cases)%%</td>
+ </tr>
+%else
+ <tr>
+ <td>Failed</td>
+ <td class="numeric">%%failed_tests_count%%</td>
+ </tr>
+%endif
+ <tr>
+%if length(xfail_test_cases)
+ <td><a href="#xfail">Expected failures</a></td>
+%else
+ <td>Expected failures</td>
+%endif
+ <td class="numeric">%%xfail_tests_count%%</td>
+ </tr>
+ <tr>
+%if length(skipped_test_cases)
+ <td><a href="#skipped">Skipped</a></td>
+%else
+ <td>Skipped</td>
+%endif
+ <td class="numeric">%%skipped_tests_count%%</td>
+ </tr>
+ <tr>
+%if length(passed_test_cases)
+ <td><a href="#passed">Passed</a></td>
+%else
+ <td>Passed</td>
+%endif
+ <td class="numeric">%%passed_tests_count%%</td>
+ </tr>
+ </tbody>
+</table>
+
+<p><a href="context.html">Execution context</a></p>
+
+<p>Timing data:</p>
+
+<ul>
+ <li>Start time: %%start_time%%</li>
+ <li>End time: %%end_time%%</li>
+ <li>Duration: %%duration%%</li>
+</ul>
+
+
+%if length(broken_test_cases)
+<h2><a name="broken">Broken test cases</a></h2>
+
+<ul>
+%loop broken_test_cases iter
+ <li>
+ <a href="%%broken_test_cases_file(iter)%%">%%broken_test_cases(iter)%%</a>
+ </li>
+%endloop
+</ul>
+%endif
+
+
+%if length(failed_test_cases)
+<h2><a name="failed">Failed test cases</a></h2>
+
+<ul>
+%loop failed_test_cases iter
+ <li>
+ <a href="%%failed_test_cases_file(iter)%%">%%failed_test_cases(iter)%%</a>
+ </li>
+%endloop
+</ul>
+%endif
+
+
+%if length(xfail_test_cases)
+<h2><a name="xfail">Expected failures</a></h2>
+
+<ul>
+%loop xfail_test_cases iter
+ <li>
+ <a href="%%xfail_test_cases_file(iter)%%">%%xfail_test_cases(iter)%%</a>
+ </li>
+%endloop
+</ul>
+%endif
+
+
+%if length(skipped_test_cases)
+<h2><a name="skipped">Skipped test cases</a></h2>
+
+<ul>
+%loop skipped_test_cases iter
+ <li>
+ <a href="%%skipped_test_cases_file(iter)%%">%%skipped_test_cases(iter)%%</a>
+ </li>
+%endloop
+</ul>
+%endif
+
+
+%if length(passed_test_cases)
+<h2><a name="passed">Passed test cases</a></h2>
+
+<ul>
+%loop passed_test_cases iter
+ <li>
+ <a href="%%passed_test_cases_file(iter)%%">%%passed_test_cases(iter)%%</a>
+ </li>
+%endloop
+</ul>
+%endif
+
+
+</body>
+</html>
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 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!--
+ 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.
+-->
+<html>
+<head>
+ <title>Test case: %%test_case%%</title>
+ <link rel="stylesheet" type="text/css" href="%%css%%" />
+</head>
+
+<body>
+
+<h1>Test case: %%test_case%%</h1>
+
+<ul>
+ <li>Test program: %%test_program%%</li>
+ <li>Result: %%result%%</li>
+ <li>Start time: %%start_time%%</li>
+ <li>End time: %%end_time%%</li>
+ <li>Duration: %%duration%%</li>
+ <li><a href="context.html">Execution context</a></li>
+</ul>
+
+<h2>Metadata</h2>
+
+<ul>
+%loop metadata_var iter
+ <li><tt>%%metadata_var(iter)%% = %%metadata_value(iter)%%</tt></li>
+%endloop
+</ul>
+
+<h2>Standard output</h2>
+
+%if defined(stdout)
+<pre>%%stdout%%</pre>
+%else
+Test case did not write anything to stdout.
+%endif
+
+<h2>Standard error</h2>
+
+%if defined(stderr)
+<pre>%%stderr%%</pre>
+%else
+Test case did not write anything to stderr.
+%endif
+
+</body>
+</html>
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 <map>
+#include <memory>
+#include <ostream>
+#include <string>
+
+#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 <map>
+#include <sstream>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#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 <stdexcept>
+
+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 <cstring>
+
+#include <atf-c++.hpp>
+
+
+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 <memory>
+
+#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 <memory>
+#include <ostream>
+#include <string>
+
+#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 <sstream>
+
+#include <atf-c++.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(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 <memory>
+#include <ostream>
+#include <string>
+
+#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 <map>
+#include <string>
+
+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 <sstream>
+
+#include <atf-c++.hpp>
+
+#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 <map>
+#include <sstream>
+
+#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 <memory>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#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 <memory>
+#include <vector>
+
+
+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 <sys/stat.h>
+
+#include <signal.h>
+}
+
+#include <set>
+#include <sstream>
+
+#include <atf-c++.hpp>
+
+#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 <ostream>
+#include <string>
+
+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 <sstream>
+
+#include <atf-c++.hpp>
+
+
+/// 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 <map>
+#include <set>
+#include <string>
+
+#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 <string>
+
+#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 <atf-c++.hpp>
+
+#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 <stdexcept>
+
+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 <cstring>
+
+#include <atf-c++.hpp>
+
+
+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 <algorithm>
+#include <cstring>
+
+#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<int>(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 <string>
+
+#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 <string>
+#include <utility>
+
+#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 <unistd.h>
+}
+
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <stdint.h>
+}
+
+#include <cstddef>
+
+#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 <atf-c++.hpp>
+
+#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 <stdexcept>
+
+#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 <sys/stat.h>
+}
+
+#include <atf-c++.hpp>
+
+#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 <memory>
+
+#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 <atf-c++.hpp>
+
+#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 <stdint.h>
+}
+
+#include <map>
+#include <utility>
+
+#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 <stdint.h>
+}
+
+#include <memory>
+#include <string>
+
+#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 <map>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#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 <map>
+
+#include <atf-c++.hpp>
+
+#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 <map>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#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 <stdexcept>
+
+#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 <memory>
+
+#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 <atf-c++.hpp>
+
+#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 <stdint.h>
+}
+
+#include <fstream>
+#include <map>
+
+#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 <stdint.h>
+}
+
+#include <memory>
+#include <string>
+
+#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 <cstring>
+#include <map>
+#include <string>
+
+#include <atf-c++.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/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 <cstddef>
+
+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 <sys/types.h>
+}
+
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <string>
+
+#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 <atf-c++.hpp>
+
+#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 <map>
+#include <memory>
+#include <set>
+#include <string>
+
+#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 <atf-c++.hpp>
+
+#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 <stdexcept>
+#include <string>
+
+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 <cstring>
+
+#include <atf-c++.hpp>
+
+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 <string>
+
+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 <atf-c++.hpp>
+
+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 <stdexcept>
+#include <vector>
+
+#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 <string>
+#include <utility>
+#include <vector>
+
+#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:
+///
+/// <ul>
+/// <li>A public option_type typedef that defines the type of the
+/// option.</li>
+///
+/// <li>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.</li>
+///
+/// <li>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.</li>
+/// </ul>
+///
+/// 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 <atf-c++.hpp>
+
+#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 <getopt.h>
+}
+
+#include <cstdlib>
+#include <cstring>
+#include <limits>
+
+#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 <map>
+#include <string>
+#include <vector>
+
+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 <string>
+#include <vector>
+
+#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 <fcntl.h>
+#include <getopt.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <utility>
+
+#include <atf-c++.hpp>
+
+#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 <sys/param.h>
+#include <sys/ioctl.h>
+
+#if defined(HAVE_TERMIOS_H)
+# include <termios.h>
+#endif
+#include <unistd.h>
+}
+
+#include <iostream>
+
+#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 <cstddef>
+#include <string>
+
+#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 <iostream>
+
+#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 <cstddef>
+#include <string>
+#include <vector>
+
+#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 <sys/param.h>
+#include <sys/ioctl.h>
+
+#include <fcntl.h>
+#if defined(HAVE_TERMIOS_H)
+# include <termios.h>
+#endif
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstring>
+
+#include <atf-c++.hpp>
+
+#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 <stdexcept>
+
+#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 <cstring>
+
+#include <atf-c++.hpp>
+
+#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 <string>
+
+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 <string>
+#include <vector>
+
+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 <atf-c++.hpp>
+
+#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 <lutok/stack_cleaner.hpp>
+#include <lutok/state.ipp>
+
+#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 <string>
+
+#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 <atf-c++.hpp>
+
+#include <lutok/exceptions.hpp>
+#include <lutok/operations.hpp>
+#include <lutok/state.ipp>
+
+#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 <memory>
+
+#include <lutok/state.ipp>
+
+#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 <set>
+#include <string>
+
+#include <lutok/state.hpp>
+
+#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 <memory>
+#include <typeinfo>
+
+#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 <map>
+#include <string>
+
+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 <atf-c++.hpp>
+
+#include <lutok/state.ipp>
+
+#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 <lutok/exceptions.hpp>
+#include <lutok/operations.hpp>
+#include <lutok/stack_cleaner.hpp>
+#include <lutok/state.ipp>
+
+#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 <memory>
+
+#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 <stdexcept>
+
+#include <atf-c++.hpp>
+
+#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 <memory>
+#include <string>
+
+#include <lutok/state.hpp>
+
+#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 <typeinfo>
+
+#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 <map>
+#include <string>
+
+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 <atf-c++.hpp>
+
+#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 <sys/time.h>
+
+#include <time.h>
+}
+
+#include <stdexcept>
+
+#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(&current_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 <stdint.h>
+}
+
+#include <cstddef>
+#include <memory>
+#include <ostream>
+#include <string>
+
+
+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 <time.h>
+#include <unistd.h>
+}
+
+#include <sstream>
+#include <stdexcept>
+
+#include <atf-c++.hpp>
+
+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 <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <stdexcept>
+
+#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 <map>
+#include <string>
+
+#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 <atf-c++.hpp>
+
+#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 <map>
+#include <memory>
+#include <ostream>
+#include <set>
+#include <utility>
+#include <vector>
+
+
+// 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 <ostream>
+
+
+/// 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 << "<NULL>";
+ } 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 <memory>
+#include <ostream>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+
+
+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("<NULL>", std::shared_ptr< char >());
+ do_check("<NULL>", 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 <stdexcept>
+#include <string>
+
+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 <cstring>
+
+#include <atf-c++.hpp>
+
+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 <memory>
+#include <string>
+#include <utility>
+
+#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 <sstream>
+#include <string>
+
+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 <ostream>
+
+#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 <ostream>
+
+#include <atf-c++.hpp>
+
+#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 <em>must not be included from other header files</em>.
+
+#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 <memory>
+#include <string>
+
+#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 <unistd.h>
+}
+
+#include <atf-c++.hpp>
+
+#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 <sys/types.h>
+
+#include <dirent.h>
+}
+
+#include <cerrno>
+#include <memory>
+
+#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 <memory>
+#include <ostream>
+#include <string>
+
+#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 <sstream>
+
+#include <atf-c++.hpp>
+
+#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 <cstring>
+
+#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 <stdexcept>
+#include <string>
+
+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 <cerrno>
+#include <cstring>
+
+#include <atf-c++.hpp>
+
+#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 <dirent.h>
+}
+
+#include <cerrno>
+#include <cstring>
+#include <stdexcept>
+#include <string>
+
+#include <lutok/operations.hpp>
+#include <lutok/stack_cleaner.hpp>
+#include <lutok/state.ipp>
+
+#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 <lutok/state.hpp>
+
+#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 <atf-c++.hpp>
+#include <lutok/operations.hpp>
+#include <lutok/state.hpp>
+#include <lutok/test_utils.hpp>
+
+#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 <sys/param.h>
+#if defined(HAVE_SYS_MOUNT_H)
+# include <sys/mount.h>
+#endif
+#include <sys/stat.h>
+#if defined(HAVE_SYS_STATVFS_H) && defined(HAVE_STATVFS)
+# include <sys/statvfs.h>
+#endif
+#if defined(HAVE_SYS_VFS_H)
+# include <sys/vfs.h>
+#endif
+#include <sys/wait.h>
+
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <sstream>
+#include <string>
+
+#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 <set>
+#include <string>
+
+#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 <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <dirent.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+#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 <string>
+#include <ostream>
+
+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 <unistd.h>
+}
+
+#include <set>
+
+#include <atf-c++.hpp>
+
+#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 <em>must not be included from other header files</em>.
+
+#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 <fstream>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#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 <unistd.h>
+}
+
+#include <stdexcept>
+#include <string>
+#include <utility>
+#include <vector>
+
+#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 <string>
+
+#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 <unistd.h>
+}
+
+#include <fstream>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#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 <sys/types.h>
+#endif
+#if defined(HAVE_SYS_PARAM_H)
+# include <sys/param.h>
+#endif
+#if defined(HAVE_SYS_SYSCTL_H)
+# include <sys/sysctl.h>
+#endif
+}
+
+#include <cerrno>
+#include <cstddef>
+#include <cstring>
+#include <stdexcept>
+
+#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 <cstring>
+
+#include <atf-c++.hpp>
+
+#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 <ostream>
+
+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 <cstddef>
+
+#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 <iostream>
+#include <sstream>
+
+#include <atf-c++.hpp>
+
+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 <sys/types.h>
+
+#include <pwd.h>
+#include <unistd.h>
+}
+
+#include <stdexcept>
+
+#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 <string>
+#include <vector>
+
+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 <sys/wait.h>
+
+#include <pwd.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+#include <stdexcept>
+
+#include <atf-c++.hpp>
+
+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 <sys/stat.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <iostream>
+#include <memory>
+
+#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 <istream>
+#include <memory>
+#include <stdexcept>
+
+#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 <cstdlib>
+
+#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 <sys/stat.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdarg>
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <stdexcept>
+
+#include <atf-c++.hpp>
+
+#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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+
+#include <atf-c++.hpp>
+
+#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 <cstring>
+
+#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 <stdexcept>
+
+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 <cerrno>
+#include <cstring>
+
+#include <atf-c++.hpp>
+
+#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 <sys/types.h>
+#include <sys/wait.h>
+
+#include <signal.h>
+}
+
+#include <fstream>
+#include <map>
+#include <memory>
+#include <stdexcept>
+
+#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 <cstddef>
+#include <memory>
+
+#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 <sys/types.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+#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 <istream>
+#include <memory>
+
+#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 <unistd.h>
+}
+
+#include <atf-c++.hpp>
+
+#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 <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <sstream>
+
+
+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 <sys/stat.h>
+
+#include <grp.h>
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+
+#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 <sys/types.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <sys/types.h>
+#include <sys/wait.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+
+#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 <string>
+#include <vector>
+
+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 <sys/types.h>
+#include <sys/wait.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <sys/wait.h>
+}
+
+#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 <ostream>
+#include <utility>
+
+#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 <sys/wait.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+
+#include <atf-c++.hpp>
+
+#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 <sys/types.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+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 <unistd.h>
+}
+
+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 <unistd.h>
+}
+
+#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 <cstddef>
+#include <memory>
+#include <streambuf>
+
+#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 <sys/stat.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+}
+
+#include <fstream>
+
+#include <atf-c++.hpp>
+
+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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+
+#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 <cstddef>
+#include <string>
+
+#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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <cstring>
+
+#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 <stdexcept>
+
+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 <cerrno>
+#include <cstring>
+
+#include <atf-c++.hpp>
+
+#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 <sys/types.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+#include <cstring>
+#include <set>
+
+#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 <unistd.h>
+
+#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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <signal.h>
+}
+
+#include <cerrno>
+#include <cstddef>
+
+#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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cstdlib>
+
+#include <atf-c++.hpp>
+
+#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 <signal.h>
+}
+
+#include <cerrno>
+
+#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 <memory>
+
+#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 <signal.h>
+#include <unistd.h>
+}
+
+#include <atf-c++.hpp>
+
+#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 <sys/time.h>
+
+#include <signal.h>
+}
+
+#include <cerrno>
+#include <map>
+#include <set>
+#include <vector>
+
+#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 <memory>
+
+#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 <signal.h>
+#include <unistd.h>
+}
+
+#include <cstddef>
+#include <iostream>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+#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 <sqlite3.h>
+}
+
+#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 <atf-c++.hpp>
+
+#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 <sqlite3.h>
+}
+
+#include <cstring>
+#include <stdexcept>
+
+#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 <stdint.h>
+}
+
+#include <cstddef>
+#include <memory>
+
+#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 <atf-c++.hpp>
+
+#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 <sqlite3.h>
+}
+
+#include <string>
+
+#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 <stdexcept>
+#include <string>
+
+#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 <sqlite3.h>
+}
+
+#include <cstring>
+#include <string>
+
+#include <atf-c++.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::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 <sqlite3.h>
+}
+
+#include <map>
+
+#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 <stdint.h>
+}
+
+#include <memory>
+#include <string>
+
+#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 <stdint.h>
+}
+
+#include <cstring>
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <memory>
+
+#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 <atf-c++.hpp>
+
+#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 <sys/param.h>
+#include <sys/resource.h>
+
+#include <unistd.h>
+}
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#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 <ostream>
+
+#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 <cstdlib>
+
+
+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 <sys/resource.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <signal.h>
+#include <unistd.h>
+}
+
+#include <iostream>
+#include <sstream>
+
+#include <atf-c++.hpp>
+
+#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 <fstream>
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+
+#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 <cstddef>
+#include <istream>
+#include <memory>
+#include <ostream>
+#include <string>
+
+#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 <cstdlib>
+#include <sstream>
+
+#include <atf-c++.hpp>
+
+#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 <sys/resource.h>
+}
+
+#include <cstdlib>
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#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 <stdexcept>
+
+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 <cstring>
+
+#include <atf-c++.hpp>
+
+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 <sstream>
+
+#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 << "&quot;";
+ } else if (c == '&') {
+ quoted << "&amp;";
+ } else if (c == '<') {
+ quoted << "&lt;";
+ } else if (c == '>') {
+ quoted << "&gt;";
+ } else if (c == '\'') {
+ quoted << "&apos;";
+ } 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 '&amp;#[decimal ASCII value];'
+ // so that in the XML file we will see the escaped
+ // character.
+ quoted << "&amp;#" << 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 <cstddef>
+#include <string>
+#include <vector>
+
+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 <sstream>
+
+#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 <iostream>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <atf-c++.hpp>
+
+#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("&apos;", text::escape_xml("'"));
+
+ ATF_REQUIRE_EQ("foo &quot;bar&amp; &lt;tag&gt; yay&apos; baz",
+ text::escape_xml("foo \"bar& <tag> yay' baz"));
+
+ ATF_REQUIRE_EQ("&quot;&amp;&lt;&gt;&apos;", text::escape_xml("\"&<>'"));
+ ATF_REQUIRE_EQ("&amp;&amp;&amp;", text::escape_xml("&&&"));
+ ATF_REQUIRE_EQ("&amp;#8;&amp;#11;", text::escape_xml("\b\v"));
+ ATF_REQUIRE_EQ("\t&amp;#127;BAR&amp;", 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 <sys/types.h>
+
+#include <regex.h>
+}
+
+#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 <cstddef>
+#include <memory>
+
+
+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 <atf-c++.hpp>
+
+#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 <algorithm>
+#include <iterator>
+#include <limits>
+#include <sstream>
+
+#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 <cstddef>
+#include <string>
+#include <vector>
+
+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 <cstddef>
+#include <string>
+#include <vector>
+
+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 <algorithm>
+
+#include <atf-c++.hpp>
+
+#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 <algorithm>
+#include <fstream>
+#include <sstream>
+#include <stack>
+
+#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 <istream>
+#include <map>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#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 <fstream>
+#include <sstream>
+
+#include <atf-c++.hpp>
+
+#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 <stdint.h>
+}
+
+#include <stdexcept>
+
+#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 <stdint.h>
+}
+
+#include <istream>
+#include <ostream>
+#include <string>
+
+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 <sstream>
+#include <stdexcept>
+
+#include <atf-c++.hpp>
+
+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);
+}