From 4773d3e6e7eaff8fd2e86f62ec957ef77a8455d7 Mon Sep 17 00:00:00 2001 From: Onsi Fakhouri Date: Thu, 10 Sep 2020 13:38:50 -0600 Subject: [PATCH] [v2] First Public Release of V2 Ginkgo V2 is an extensive effort to clean up the Ginkgo codebase and introduce several new highly-requested features. This first public release represents a significant clean up of the codebase, deprecation of several pieces of functionality that wil not be making it into V2, and the beginnings of some new functionality. Details of changes so far are in the `docs/MIGRATING_TO_V2.md` file. More extensive discussion of V2 is in #711 and the google doc linked there. --- .gitignore | 2 +- CHANGELOG.md | 5 +- config/cli_config.go | 213 ++ config/cli_go_flags.go | 148 ++ config/config.go | 282 +-- .../config_suite_test.go | 10 +- config/config_test.go | 154 ++ config/flags.go | 467 ++++ config/flags_test.go | 460 ++++ docs/MIGRATING_TO_V2.md | 168 ++ docs/index.md | 2169 +++++++++++++++++ extensions/table/table.go | 27 +- extensions/table/table_entry.go | 18 +- .../LICENSE => formatter/colorable_others.go | 20 + .../colorable_windows.go | 77 +- formatter/formatter.go | 5 + .../formatter_suite_test.go | 10 +- formatter/formatter_test.go | 88 + ginkgo/bootstrap_command.go | 200 -- ginkgo/build/build_command.go | 57 + ginkgo/build_command.go | 66 - ginkgo/command/abort.go | 53 + ginkgo/command/abort_test.go | 91 + ginkgo/command/command.go | 50 + .../command/command_suite_test.go | 10 +- ginkgo/command/command_test.go | 94 + ginkgo/command/program.go | 179 ++ ginkgo/command/program_test.go | 303 +++ ginkgo/convert/ginkgo_ast_nodes.go | 123 - ginkgo/convert/import.go | 90 - ginkgo/convert/package_rewriter.go | 128 - ginkgo/convert/test_finder.go | 56 - ginkgo/convert/testfile_rewriter.go | 162 -- ginkgo/convert/testing_t_rewriter.go | 130 - ginkgo/convert_command.go | 51 - ginkgo/generators/boostrap_templates.go | 48 + ginkgo/generators/bootstrap_command.go | 114 + ginkgo/{ => generators}/generate_command.go | 160 +- ginkgo/generators/generate_templates.go | 41 + ginkgo/generators/generators_common.go | 48 + ginkgo/ginkgo_cli_suite_test.go | 17 + ginkgo/ginkgo_cli_test.go | 17 + ginkgo/help_command.go | 31 - ginkgo/internal/cli_internal_suite_test.go | 16 + ginkgo/internal/compile.go | 61 + ginkgo/internal/profiles.go | 83 + ginkgo/internal/run.go | 206 ++ ginkgo/internal/test_suite.go | 184 ++ ginkgo/internal/testsuite_test.go | 290 +++ ginkgo/internal/utils.go | 47 + ginkgo/internal/utils_test.go | 73 + ginkgo/interrupthandler/interrupt_handler.go | 53 +- ginkgo/main.go | 213 +- ginkgo/nodot/nodot_command.go | 70 + ginkgo/nodot/nodot_suite_test.go | 7 - ginkgo/nodot_command.go | 77 - ginkgo/notifications.go | 141 -- ginkgo/outline/outline_command.go | 98 + ginkgo/outline/outline_test.go | 1 - ginkgo/outline_command.go | 95 - ginkgo/run/run_command.go | 220 ++ ginkgo/run_command.go | 312 --- ginkgo/run_watch_and_build_command_flags.go | 169 -- ginkgo/suite_runner.go | 173 -- ginkgo/testrunner/build_args.go | 7 - ginkgo/testrunner/build_args_old.go | 7 - ginkgo/testrunner/log_writer.go | 52 - ginkgo/testrunner/run_result.go | 27 - ginkgo/testrunner/test_runner.go | 554 ----- ginkgo/testrunner/test_runner_test.go | 64 - ginkgo/testsuite/test_suite.go | 115 - ginkgo/testsuite/testsuite_test.go | 212 -- ginkgo/testsuite/vendor_check_go15.go | 16 - ginkgo/testsuite/vendor_check_go15_test.go | 201 -- ginkgo/testsuite/vendor_check_go16.go | 15 - ginkgo/{ => unfocus}/unfocus_command.go | 26 +- ginkgo/version_command.go | 24 - ginkgo/watch/delta_tracker.go | 8 +- ginkgo/watch/suite.go | 6 +- ginkgo/watch/watch_command.go | 187 ++ ginkgo/watch_command.go | 175 -- ginkgo_dsl.go | 437 ++-- go.mod | 3 +- go.sum | 26 +- .../after_run_hook.go} | 0 .../after_run_hook_suite_test.go} | 4 +- .../after_run_hook_test.go} | 2 +- .../first_package/coverage_fixture_test.go | 2 +- .../external_coverage.go | 0 .../convert_fixtures/extra_functions_test.go | 14 - .../convert_fixtures/nested/nested_test.go | 10 - .../subpackage/nested_subpackage_test.go | 9 - .../convert_fixtures/outside_package_test.go | 16 - .../_fixtures/convert_fixtures/xunit_test.go | 41 - .../extra_functions_test.go | 17 - .../fixtures_suite_test.go | 13 - .../nested_subpackage_test.go | 11 - .../convert_goldmasters/nested_suite_test.go | 13 - .../convert_goldmasters/nested_test.go | 13 - .../outside_package_test.go | 19 - .../convert_goldmasters/suite_test.go | 13 - .../convert_goldmasters/xunit_test.go | 44 - .../_fixtures/coverage_fixture/coverage.go | 2 +- .../coverage_fixture/coverage_fixture_test.go | 2 +- .../external_coverage.go | 0 .../deprecated_features_fixture_suite_test.go | 21 + .../does_not_compile_suite_test.go | 0 .../does_not_compile_test.go | 0 .../eventually_failing_suite_test.go | 0 .../eventually_failing_test.go | 0 .../exiting_synchronized_setup_suite_test.go} | 4 +- .../fail_fixture/fail_fixture_test.go | 71 - .../failing_after_suite_suite_test.go | 22 - .../failing_after_suite_test.go | 15 - .../failing_before_suite_suite_test.go | 22 - .../failing_before_suite_test.go | 15 - .../failing_ginkgo_tests.go | 0 .../failing_ginkgo_tests_suite_test.go | 0 .../failing_ginkgo_tests_test.go | 0 .../{flags_tests => flags_fixture}/flags.go | 0 .../flags_suite_test.go | 0 .../flags_test.go | 14 +- .../focused_fixture/focused_fixture_test.go | 8 - .../internal/focused_fixture_test.go | 8 - .../focused_fixture_suite_test.go | 0 .../focused_fixture_test.go | 8 - .../vendor/foo/bar/bar.go | 0 .../vendor/foo/foo.go | 0 .../vendor/vendored.go | 0 .../hanging_suite_test.go} | 0 .../hanging_test.go} | 14 + .../malformed_fixture_suite_test.go | 13 + .../malformed_fixture_test.go | 13 + .../more_ginkgo_tests.go | 0 .../more_ginkgo_tests_suite_test.go | 0 .../more_ginkgo_tests_test.go | 2 +- .../no_test_fn.go | 0 .../no_test_fn_test.go | 2 +- .../no_tests.go | 0 .../passing_ginkgo_tests.go | 0 .../passing_ginkgo_tests_suite_test.go | 0 .../passing_ginkgo_tests_test.go | 2 +- .../passing_suite_setup_suite_test.go | 26 - .../passing_suite_setup/passing_suite_test.go | 28 - .../skip_fixture/skip_fixture_suite_test.go | 4 +- .../skip_fixture/skip_fixture_test.go | 33 +- .../synchronized_setup_tests_suite_test.go | 0 .../ignored_test.go | 2 +- .../tags_tests_suite_test.go | 2 +- .../tags_tests_test.go | 2 +- .../test_description_suite_test.go | 13 - .../test_description/test_description_test.go | 23 - integration/_fixtures/watch_fixture/A/A.go | 7 + .../A/A_suite_test.go | 0 .../A/A_test.go | 2 +- integration/_fixtures/watch_fixture/B/B.go | 7 + .../B/B_suite_test.go | 0 .../B/B_test.go | 2 +- .../{watch_fixtures => watch_fixture}/C/C.go | 0 .../C/C.json | 0 .../C/C_suite_test.go | 0 .../C/C_test.go | 2 +- integration/_fixtures/watch_fixture/D/D.go | 7 + .../D/D_suite_test.go | 0 .../D/D_test.go | 2 +- integration/_fixtures/watch_fixtures/A/A.go | 7 - integration/_fixtures/watch_fixtures/B/B.go | 7 - integration/_fixtures/watch_fixtures/D/D.go | 7 - .../xunit_tests.go | 0 .../xunit_tests_test.go | 0 integration/convert_test.go | 121 - integration/coverage_test.go | 192 +- integration/deprecations_test.go | 24 + integration/fail_test.go | 95 +- integration/flags_test.go | 138 +- integration/integration_suite_test.go | 131 +- integration/interrupt_test.go | 27 +- integration/precompiled_test.go | 20 +- integration/progress_test.go | 8 +- integration/run_test.go | 207 +- integration/skip_test.go | 20 +- integration/subcommand_test.go | 114 +- integration/suite_command_test.go | 35 +- integration/suite_setup_test.go | 129 +- integration/tags_test.go | 8 +- integration/test_description_test.go | 25 - integration/verbose_and_succinct_test.go | 25 +- integration/watch_test.go | 87 +- internal/codelocation/code_location.go | 48 - .../codelocation/code_location_suite_test.go | 13 - internal/codelocation/code_location_test.go | 110 - internal/containernode/container_node.go | 151 -- .../container_node_suite_test.go | 13 - internal/containernode/container_node_test.go | 213 -- internal/counter.go | 44 + internal/counter_test.go | 107 + internal/{failer => }/failer.go | 58 +- internal/failer/failer_test.go | 141 -- internal/failer_test.go | 140 ++ internal/focus.go | 119 + internal/focus_test.go | 234 ++ internal/global/init.go | 15 +- .../config_dry_run_test.go | 49 + .../config_fail_fast_test.go | 44 + .../config_flake_attempts_test.go | 69 + .../config_progress_test.go | 48 + .../current_test_description_test.go | 71 + internal/internal_integration/fail_test.go | 341 +++ internal/internal_integration/focus_test.go | 220 ++ .../internal_integration_suite_test.go | 128 + .../internal_integration/interrupt_test.go | 99 + .../internal_integration/parallel_test.go | 157 ++ internal/internal_integration/run_test.go | 133 + internal/internal_integration/shuffle_test.go | 89 + internal/internal_suite_test.go | 109 + internal/interrupt_handler.go | 58 + internal/leafnodes/benchmarker.go | 103 - internal/leafnodes/interfaces.go | 19 - internal/leafnodes/it_node.go | 47 - internal/leafnodes/it_node_test.go | 22 - internal/leafnodes/leaf_node_suite_test.go | 13 - internal/leafnodes/measure_node.go | 62 - internal/leafnodes/measure_node_test.go | 155 -- internal/leafnodes/runner.go | 117 - internal/leafnodes/setup_nodes.go | 48 - internal/leafnodes/setup_nodes_test.go | 48 - internal/leafnodes/shared_runner_test.go | 353 --- internal/leafnodes/suite_nodes.go | 55 - internal/leafnodes/suite_nodes_test.go | 230 -- .../synchronized_after_suite_node.go | 90 - .../synchronized_after_suite_node_test.go | 199 -- .../synchronized_before_suite_node.go | 181 -- .../synchronized_before_suite_node_test.go | 446 ---- internal/node.go | 183 ++ internal/node_test.go | 304 +++ .../parallel_support/forwarding_reporter.go | 114 + .../forwarding_reporter_test.go | 162 ++ .../output_interceptor_unix.go | 37 +- .../output_interceptor_win.go | 5 +- .../parallel_support_suite_test.go | 63 + .../{remote => parallel_support}/server.go | 134 +- .../server_test.go | 171 +- internal/remote/aggregator.go | 249 -- internal/remote/aggregator_test.go | 313 --- .../remote/fake_output_interceptor_test.go | 22 - internal/remote/fake_poster_test.go | 33 - internal/remote/forwarding_reporter.go | 147 -- internal/remote/forwarding_reporter_test.go | 181 -- internal/remote/output_interceptor.go | 13 - internal/remote/remote_suite_test.go | 13 - internal/shuffle.go | 49 + internal/shuffle_test.go | 146 ++ internal/spec.go | 68 + internal/spec/spec.go | 247 -- internal/spec/spec_test.go | 739 ------ internal/spec/specs.go | 144 -- internal/spec/specs_test.go | 292 --- internal/spec_iterator/index_computer.go | 55 - internal/spec_iterator/index_computer_test.go | 149 -- .../spec_iterator/parallel_spec_iterator.go | 59 - .../parallel_spec_iterator_test.go | 112 - .../spec_iterator/serial_spec_iterator.go | 45 - .../serial_spec_iterator_test.go | 64 - .../sharded_parallel_spec_iterator.go | 47 - .../sharded_parallel_spec_iterator_test.go | 62 - internal/spec_iterator/spec_iterator.go | 20 - .../spec_iterator/spec_iterator_suite_test.go | 13 - internal/spec_test.go | 119 + internal/specrunner/random_id.go | 15 - internal/specrunner/spec_runner.go | 411 ---- internal/specrunner/spec_runner_suite_test.go | 13 - internal/specrunner/spec_runner_test.go | 787 ------ internal/suite.go | 396 +++ internal/suite/suite.go | 227 -- internal/suite/suite_suite_test.go | 51 - internal/suite/suite_test.go | 385 --- internal/suite_node_builder.go | 183 ++ internal/suite_node_builder_test.go | 383 +++ internal/suite_test.go | 210 ++ internal/test_helpers/fake_reporter.go | 206 ++ .../test_helpers/markdown_heading_loader.go | 34 + internal/test_helpers/run_tracker.go | 115 + internal/test_helpers/status_code_poller.go | 14 + internal/tree.go | 67 + internal/tree_test.go | 168 ++ internal/{writer => }/writer.go | 42 +- internal/writer/fake_writer.go | 36 - internal/writer/writer_suite_test.go | 13 - internal/writer/writer_test.go | 75 - internal/writer_test.go | 60 + reporters/default_reporter.go | 365 ++- reporters/default_reporter_test.go | 1286 ++++++---- reporters/fake_reporter.go | 59 - reporters/junit_reporter.go | 97 +- reporters/junit_reporter_test.go | 207 +- reporters/multi_reporter.go | 40 + reporters/multi_reporter_test.go | 46 + reporters/reporter.go | 49 + reporters/stenographer/console_logging.go | 64 - reporters/stenographer/fake_stenographer.go | 142 -- reporters/stenographer/stenographer.go | 572 ----- reporters/stenographer/support/README.md | 6 - .../support/go-colorable/README.md | 43 - .../support/go-colorable/colorable_others.go | 24 - .../support/go-colorable/noncolorable.go | 57 - .../stenographer/support/go-isatty/LICENSE | 9 - .../stenographer/support/go-isatty/README.md | 37 - .../stenographer/support/go-isatty/doc.go | 2 - .../support/go-isatty/isatty_appengine.go | 9 - .../support/go-isatty/isatty_bsd.go | 18 - .../support/go-isatty/isatty_linux.go | 18 - .../support/go-isatty/isatty_solaris.go | 16 - .../support/go-isatty/isatty_windows.go | 19 - reporters/teamcity_reporter.go | 75 +- reporters/teamcity_reporter_test.go | 137 +- types/code_location.go | 42 + types/code_location_test.go | 76 + types/deprecated_support_test.go | 172 ++ types/deprecation_support.go | 177 +- types/errors.go | 204 ++ types/errors_test.go | 67 + types/synchronization.go | 30 - types/types.go | 258 +- types/types_suite_test.go | 8 +- types/types_test.go | 118 +- 325 files changed, 15578 insertions(+), 16126 deletions(-) create mode 100644 config/cli_config.go create mode 100644 config/cli_go_flags.go rename internal/spec/spec_suite_test.go => config/config_suite_test.go (57%) create mode 100644 config/config_test.go create mode 100644 config/flags.go create mode 100644 config/flags_test.go create mode 100644 docs/MIGRATING_TO_V2.md create mode 100644 docs/index.md rename reporters/stenographer/support/go-colorable/LICENSE => formatter/colorable_others.go (76%) rename {reporters/stenographer/support/go-colorable => formatter}/colorable_windows.go (90%) rename ginkgo/testsuite/testsuite_suite_test.go => formatter/formatter_suite_test.go (55%) create mode 100644 formatter/formatter_test.go delete mode 100644 ginkgo/bootstrap_command.go create mode 100644 ginkgo/build/build_command.go delete mode 100644 ginkgo/build_command.go create mode 100644 ginkgo/command/abort.go create mode 100644 ginkgo/command/abort_test.go create mode 100644 ginkgo/command/command.go rename internal/failer/failer_suite_test.go => ginkgo/command/command_suite_test.go (56%) create mode 100644 ginkgo/command/command_test.go create mode 100644 ginkgo/command/program.go create mode 100644 ginkgo/command/program_test.go delete mode 100644 ginkgo/convert/ginkgo_ast_nodes.go delete mode 100644 ginkgo/convert/import.go delete mode 100644 ginkgo/convert/package_rewriter.go delete mode 100644 ginkgo/convert/test_finder.go delete mode 100644 ginkgo/convert/testfile_rewriter.go delete mode 100644 ginkgo/convert/testing_t_rewriter.go delete mode 100644 ginkgo/convert_command.go create mode 100644 ginkgo/generators/boostrap_templates.go create mode 100644 ginkgo/generators/bootstrap_command.go rename ginkgo/{ => generators}/generate_command.go (53%) create mode 100644 ginkgo/generators/generate_templates.go create mode 100644 ginkgo/generators/generators_common.go create mode 100644 ginkgo/ginkgo_cli_suite_test.go create mode 100644 ginkgo/ginkgo_cli_test.go delete mode 100644 ginkgo/help_command.go create mode 100644 ginkgo/internal/cli_internal_suite_test.go create mode 100644 ginkgo/internal/compile.go create mode 100644 ginkgo/internal/profiles.go create mode 100644 ginkgo/internal/run.go create mode 100644 ginkgo/internal/test_suite.go create mode 100644 ginkgo/internal/testsuite_test.go create mode 100644 ginkgo/internal/utils.go create mode 100644 ginkgo/internal/utils_test.go create mode 100644 ginkgo/nodot/nodot_command.go delete mode 100644 ginkgo/nodot_command.go delete mode 100644 ginkgo/notifications.go create mode 100644 ginkgo/outline/outline_command.go delete mode 100644 ginkgo/outline_command.go create mode 100644 ginkgo/run/run_command.go delete mode 100644 ginkgo/run_command.go delete mode 100644 ginkgo/run_watch_and_build_command_flags.go delete mode 100644 ginkgo/suite_runner.go delete mode 100644 ginkgo/testrunner/build_args.go delete mode 100644 ginkgo/testrunner/build_args_old.go delete mode 100644 ginkgo/testrunner/log_writer.go delete mode 100644 ginkgo/testrunner/run_result.go delete mode 100644 ginkgo/testrunner/test_runner.go delete mode 100644 ginkgo/testrunner/test_runner_test.go delete mode 100644 ginkgo/testsuite/test_suite.go delete mode 100644 ginkgo/testsuite/testsuite_test.go delete mode 100644 ginkgo/testsuite/vendor_check_go15.go delete mode 100644 ginkgo/testsuite/vendor_check_go15_test.go delete mode 100644 ginkgo/testsuite/vendor_check_go16.go rename ginkgo/{ => unfocus}/unfocus_command.go (87%) delete mode 100644 ginkgo/version_command.go create mode 100644 ginkgo/watch/watch_command.go delete mode 100644 ginkgo/watch_command.go rename integration/_fixtures/{suite_command_tests/suite_command.go => after_run_hook_fixture/after_run_hook.go} (100%) rename integration/_fixtures/{suite_command_tests/suite_command_suite_test.go => after_run_hook_fixture/after_run_hook_suite_test.go} (64%) rename integration/_fixtures/{suite_command_tests/suite_command_test.go => after_run_hook_fixture/after_run_hook_test.go} (84%) rename integration/_fixtures/combined_coverage_fixture/first_package/{external_coverage_fixture => external_coverage}/external_coverage.go (100%) delete mode 100644 integration/_fixtures/convert_fixtures/extra_functions_test.go delete mode 100644 integration/_fixtures/convert_fixtures/nested/nested_test.go delete mode 100644 integration/_fixtures/convert_fixtures/nested_without_gofiles/subpackage/nested_subpackage_test.go delete mode 100644 integration/_fixtures/convert_fixtures/outside_package_test.go delete mode 100644 integration/_fixtures/convert_fixtures/xunit_test.go delete mode 100644 integration/_fixtures/convert_goldmasters/extra_functions_test.go delete mode 100644 integration/_fixtures/convert_goldmasters/fixtures_suite_test.go delete mode 100644 integration/_fixtures/convert_goldmasters/nested_subpackage_test.go delete mode 100644 integration/_fixtures/convert_goldmasters/nested_suite_test.go delete mode 100644 integration/_fixtures/convert_goldmasters/nested_test.go delete mode 100644 integration/_fixtures/convert_goldmasters/outside_package_test.go delete mode 100644 integration/_fixtures/convert_goldmasters/suite_test.go delete mode 100644 integration/_fixtures/convert_goldmasters/xunit_test.go rename integration/_fixtures/coverage_fixture/{external_coverage_fixture => external_coverage}/external_coverage.go (100%) create mode 100644 integration/_fixtures/deprecated_features_fixture/deprecated_features_fixture_suite_test.go rename integration/_fixtures/{does_not_compile => does_not_compile_fixture}/does_not_compile_suite_test.go (100%) rename integration/_fixtures/{does_not_compile => does_not_compile_fixture}/does_not_compile_test.go (100%) rename integration/_fixtures/{eventually_failing => eventually_failing_fixture}/eventually_failing_suite_test.go (100%) rename integration/_fixtures/{eventually_failing => eventually_failing_fixture}/eventually_failing_test.go (100%) rename integration/_fixtures/{exiting_synchronized_setup_tests/exiting_synchronized_setup_tests_suite_test.go => exiting_synchronized_setup_fixture/exiting_synchronized_setup_suite_test.go} (86%) delete mode 100644 integration/_fixtures/failing_after_suite/failing_after_suite_suite_test.go delete mode 100644 integration/_fixtures/failing_after_suite/failing_after_suite_test.go delete mode 100644 integration/_fixtures/failing_before_suite/failing_before_suite_suite_test.go delete mode 100644 integration/_fixtures/failing_before_suite/failing_before_suite_test.go rename integration/_fixtures/{failing_ginkgo_tests => failing_ginkgo_tests_fixture}/failing_ginkgo_tests.go (100%) rename integration/_fixtures/{failing_ginkgo_tests => failing_ginkgo_tests_fixture}/failing_ginkgo_tests_suite_test.go (100%) rename integration/_fixtures/{failing_ginkgo_tests => failing_ginkgo_tests_fixture}/failing_ginkgo_tests_test.go (100%) rename integration/_fixtures/{flags_tests => flags_fixture}/flags.go (100%) rename integration/_fixtures/{flags_tests => flags_fixture}/flags_suite_test.go (100%) rename integration/_fixtures/{flags_tests => flags_fixture}/flags_test.go (86%) rename integration/_fixtures/{focused_fixture_with_vendor => focused_with_vendor_fixture}/focused_fixture_suite_test.go (100%) rename integration/_fixtures/{focused_fixture_with_vendor => focused_with_vendor_fixture}/focused_fixture_test.go (88%) rename integration/_fixtures/{focused_fixture_with_vendor => focused_with_vendor_fixture}/vendor/foo/bar/bar.go (100%) rename integration/_fixtures/{focused_fixture_with_vendor => focused_with_vendor_fixture}/vendor/foo/foo.go (100%) rename integration/_fixtures/{focused_fixture_with_vendor => focused_with_vendor_fixture}/vendor/vendored.go (100%) rename integration/_fixtures/{hanging_suite/hanging_suite_suite_test.go => hanging_fixture/hanging_suite_test.go} (100%) rename integration/_fixtures/{hanging_suite/hanging_suite_test.go => hanging_fixture/hanging_test.go} (64%) create mode 100644 integration/_fixtures/malformed_fixture/malformed_fixture_suite_test.go create mode 100644 integration/_fixtures/malformed_fixture/malformed_fixture_test.go rename integration/_fixtures/{more_ginkgo_tests => more_ginkgo_tests_fixture}/more_ginkgo_tests.go (100%) rename integration/_fixtures/{more_ginkgo_tests => more_ginkgo_tests_fixture}/more_ginkgo_tests_suite_test.go (100%) rename integration/_fixtures/{more_ginkgo_tests => more_ginkgo_tests_fixture}/more_ginkgo_tests_test.go (96%) rename integration/_fixtures/{no_test_fn => no_test_fn_fixture}/no_test_fn.go (100%) rename integration/_fixtures/{no_test_fn => no_test_fn_fixture}/no_test_fn_test.go (76%) rename integration/_fixtures/{no_tests => no_tests_fixture}/no_tests.go (100%) rename integration/_fixtures/{passing_ginkgo_tests => passing_ginkgo_tests_fixture}/passing_ginkgo_tests.go (100%) rename integration/_fixtures/{passing_ginkgo_tests => passing_ginkgo_tests_fixture}/passing_ginkgo_tests_suite_test.go (100%) rename integration/_fixtures/{passing_ginkgo_tests => passing_ginkgo_tests_fixture}/passing_ginkgo_tests_test.go (97%) delete mode 100644 integration/_fixtures/passing_suite_setup/passing_suite_setup_suite_test.go delete mode 100644 integration/_fixtures/passing_suite_setup/passing_suite_test.go rename integration/_fixtures/{synchronized_setup_tests => synchronized_setup_tests_fixture}/synchronized_setup_tests_suite_test.go (100%) rename integration/_fixtures/{tags_tests => tags_fixture}/ignored_test.go (89%) rename integration/_fixtures/{tags_tests => tags_fixture}/tags_tests_suite_test.go (88%) rename integration/_fixtures/{tags_tests => tags_fixture}/tags_tests_test.go (83%) delete mode 100644 integration/_fixtures/test_description/test_description_suite_test.go delete mode 100644 integration/_fixtures/test_description/test_description_test.go create mode 100644 integration/_fixtures/watch_fixture/A/A.go rename integration/_fixtures/{watch_fixtures => watch_fixture}/A/A_suite_test.go (100%) rename integration/_fixtures/{watch_fixtures => watch_fixture}/A/A_test.go (74%) create mode 100644 integration/_fixtures/watch_fixture/B/B.go rename integration/_fixtures/{watch_fixtures => watch_fixture}/B/B_suite_test.go (100%) rename integration/_fixtures/{watch_fixtures => watch_fixture}/B/B_test.go (74%) rename integration/_fixtures/{watch_fixtures => watch_fixture}/C/C.go (100%) rename integration/_fixtures/{watch_fixtures => watch_fixture}/C/C.json (100%) rename integration/_fixtures/{watch_fixtures => watch_fixture}/C/C_suite_test.go (100%) rename integration/_fixtures/{watch_fixtures => watch_fixture}/C/C_test.go (74%) create mode 100644 integration/_fixtures/watch_fixture/D/D.go rename integration/_fixtures/{watch_fixtures => watch_fixture}/D/D_suite_test.go (100%) rename integration/_fixtures/{watch_fixtures => watch_fixture}/D/D_test.go (74%) delete mode 100644 integration/_fixtures/watch_fixtures/A/A.go delete mode 100644 integration/_fixtures/watch_fixtures/B/B.go delete mode 100644 integration/_fixtures/watch_fixtures/D/D.go rename integration/_fixtures/{xunit_tests => xunit_fixture}/xunit_tests.go (100%) rename integration/_fixtures/{xunit_tests => xunit_fixture}/xunit_tests_test.go (100%) delete mode 100644 integration/convert_test.go create mode 100644 integration/deprecations_test.go delete mode 100644 integration/test_description_test.go delete mode 100644 internal/codelocation/code_location.go delete mode 100644 internal/codelocation/code_location_suite_test.go delete mode 100644 internal/codelocation/code_location_test.go delete mode 100644 internal/containernode/container_node.go delete mode 100644 internal/containernode/container_node_suite_test.go delete mode 100644 internal/containernode/container_node_test.go create mode 100644 internal/counter.go create mode 100644 internal/counter_test.go rename internal/{failer => }/failer.go (59%) delete mode 100644 internal/failer/failer_test.go create mode 100644 internal/failer_test.go create mode 100644 internal/focus.go create mode 100644 internal/focus_test.go create mode 100644 internal/internal_integration/config_dry_run_test.go create mode 100644 internal/internal_integration/config_fail_fast_test.go create mode 100644 internal/internal_integration/config_flake_attempts_test.go create mode 100644 internal/internal_integration/config_progress_test.go create mode 100644 internal/internal_integration/current_test_description_test.go create mode 100644 internal/internal_integration/fail_test.go create mode 100644 internal/internal_integration/focus_test.go create mode 100644 internal/internal_integration/internal_integration_suite_test.go create mode 100644 internal/internal_integration/interrupt_test.go create mode 100644 internal/internal_integration/parallel_test.go create mode 100644 internal/internal_integration/run_test.go create mode 100644 internal/internal_integration/shuffle_test.go create mode 100644 internal/internal_suite_test.go create mode 100644 internal/interrupt_handler.go delete mode 100644 internal/leafnodes/benchmarker.go delete mode 100644 internal/leafnodes/interfaces.go delete mode 100644 internal/leafnodes/it_node.go delete mode 100644 internal/leafnodes/it_node_test.go delete mode 100644 internal/leafnodes/leaf_node_suite_test.go delete mode 100644 internal/leafnodes/measure_node.go delete mode 100644 internal/leafnodes/measure_node_test.go delete mode 100644 internal/leafnodes/runner.go delete mode 100644 internal/leafnodes/setup_nodes.go delete mode 100644 internal/leafnodes/setup_nodes_test.go delete mode 100644 internal/leafnodes/shared_runner_test.go delete mode 100644 internal/leafnodes/suite_nodes.go delete mode 100644 internal/leafnodes/suite_nodes_test.go delete mode 100644 internal/leafnodes/synchronized_after_suite_node.go delete mode 100644 internal/leafnodes/synchronized_after_suite_node_test.go delete mode 100644 internal/leafnodes/synchronized_before_suite_node.go delete mode 100644 internal/leafnodes/synchronized_before_suite_node_test.go create mode 100644 internal/node.go create mode 100644 internal/node_test.go create mode 100644 internal/parallel_support/forwarding_reporter.go create mode 100644 internal/parallel_support/forwarding_reporter_test.go rename internal/{remote => parallel_support}/output_interceptor_unix.go (63%) rename internal/{remote => parallel_support}/output_interceptor_win.go (87%) create mode 100644 internal/parallel_support/parallel_support_suite_test.go rename internal/{remote => parallel_support}/server.go (59%) rename internal/{remote => parallel_support}/server_test.go (54%) delete mode 100644 internal/remote/aggregator.go delete mode 100644 internal/remote/aggregator_test.go delete mode 100644 internal/remote/fake_output_interceptor_test.go delete mode 100644 internal/remote/fake_poster_test.go delete mode 100644 internal/remote/forwarding_reporter.go delete mode 100644 internal/remote/forwarding_reporter_test.go delete mode 100644 internal/remote/output_interceptor.go delete mode 100644 internal/remote/remote_suite_test.go create mode 100644 internal/shuffle.go create mode 100644 internal/shuffle_test.go create mode 100644 internal/spec.go delete mode 100644 internal/spec/spec.go delete mode 100644 internal/spec/spec_test.go delete mode 100644 internal/spec/specs.go delete mode 100644 internal/spec/specs_test.go delete mode 100644 internal/spec_iterator/index_computer.go delete mode 100644 internal/spec_iterator/index_computer_test.go delete mode 100644 internal/spec_iterator/parallel_spec_iterator.go delete mode 100644 internal/spec_iterator/parallel_spec_iterator_test.go delete mode 100644 internal/spec_iterator/serial_spec_iterator.go delete mode 100644 internal/spec_iterator/serial_spec_iterator_test.go delete mode 100644 internal/spec_iterator/sharded_parallel_spec_iterator.go delete mode 100644 internal/spec_iterator/sharded_parallel_spec_iterator_test.go delete mode 100644 internal/spec_iterator/spec_iterator.go delete mode 100644 internal/spec_iterator/spec_iterator_suite_test.go create mode 100644 internal/spec_test.go delete mode 100644 internal/specrunner/random_id.go delete mode 100644 internal/specrunner/spec_runner.go delete mode 100644 internal/specrunner/spec_runner_suite_test.go delete mode 100644 internal/specrunner/spec_runner_test.go create mode 100644 internal/suite.go delete mode 100644 internal/suite/suite.go delete mode 100644 internal/suite/suite_suite_test.go delete mode 100644 internal/suite/suite_test.go create mode 100644 internal/suite_node_builder.go create mode 100644 internal/suite_node_builder_test.go create mode 100644 internal/suite_test.go create mode 100644 internal/test_helpers/fake_reporter.go create mode 100644 internal/test_helpers/markdown_heading_loader.go create mode 100644 internal/test_helpers/run_tracker.go create mode 100644 internal/test_helpers/status_code_poller.go create mode 100644 internal/tree.go create mode 100644 internal/tree_test.go rename internal/{writer => }/writer.go (51%) delete mode 100644 internal/writer/fake_writer.go delete mode 100644 internal/writer/writer_suite_test.go delete mode 100644 internal/writer/writer_test.go create mode 100644 internal/writer_test.go delete mode 100644 reporters/fake_reporter.go create mode 100644 reporters/multi_reporter.go create mode 100644 reporters/multi_reporter_test.go delete mode 100644 reporters/stenographer/console_logging.go delete mode 100644 reporters/stenographer/fake_stenographer.go delete mode 100644 reporters/stenographer/stenographer.go delete mode 100644 reporters/stenographer/support/README.md delete mode 100644 reporters/stenographer/support/go-colorable/README.md delete mode 100644 reporters/stenographer/support/go-colorable/colorable_others.go delete mode 100644 reporters/stenographer/support/go-colorable/noncolorable.go delete mode 100644 reporters/stenographer/support/go-isatty/LICENSE delete mode 100644 reporters/stenographer/support/go-isatty/README.md delete mode 100644 reporters/stenographer/support/go-isatty/doc.go delete mode 100644 reporters/stenographer/support/go-isatty/isatty_appengine.go delete mode 100644 reporters/stenographer/support/go-isatty/isatty_bsd.go delete mode 100644 reporters/stenographer/support/go-isatty/isatty_linux.go delete mode 100644 reporters/stenographer/support/go-isatty/isatty_solaris.go delete mode 100644 reporters/stenographer/support/go-isatty/isatty_windows.go create mode 100644 types/code_location_test.go create mode 100644 types/deprecated_support_test.go create mode 100644 types/errors.go create mode 100644 types/errors_test.go delete mode 100644 types/synchronization.go diff --git a/.gitignore b/.gitignore index b9f9659d29..801c307e93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .DS_Store -TODO +TODO.md tmp/**/* *.coverprofile .vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index f99f894802..8aac0309ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.0 + +See [https://github.com/onsi/ginkgo/blob/v2/docs/MIGRATING_TO_V2.md](https://github.com/onsi/ginkgo/blob/v2/docs/MIGRATING_TO_V2.md) + ## 1.16.0 ### Features @@ -8,7 +12,6 @@ - Add slim-sprig template functions to bootstrap/generate (#775) [9162b86] -### Fixes - Fix accidental reference to 1488 (#784) [9fb7fe4] ## 1.15.2 diff --git a/config/cli_config.go b/config/cli_config.go new file mode 100644 index 0000000000..089087ba45 --- /dev/null +++ b/config/cli_config.go @@ -0,0 +1,213 @@ +package config + +import ( + "os" + "runtime" + "time" +) + +type GinkgoCLIConfigType struct { + //for build, run, and watch + Recurse bool + SkipPackage string + RequireSuite bool + NumCompilers int + + //for run and watch only + Nodes int + Parallel bool + AfterRunHook string + Timeout time.Duration + OutputDir string + KeepSeparateCoverprofiles bool + + //for run only + KeepGoing bool + UntilItFails bool + RandomizeSuites bool + + //for watch only + Depth int + WatchRegExp string +} + +func (g GinkgoCLIConfigType) ComputedNodes() int { + if g.Nodes > 0 { + return g.Nodes + } + + n := 1 + if g.Parallel { + n = runtime.NumCPU() + if n > 4 { + n = n - 1 + } + } + return n +} + +func NewDefaultGinkgoCLIConfig() GinkgoCLIConfigType { + return GinkgoCLIConfigType{ + Timeout: time.Hour * 24, + Depth: 1, + WatchRegExp: `\.go$`, + } +} + +type deprecatedGinkgoCLIConfig struct { + Stream bool + Notify bool +} + +var GinkgoCLISharedFlags = GinkgoFlags{ + {KeyPath: "C.Recurse", Name: "r", SectionKey: "multiple-suites", + Usage: "If set, ginkgo finds and runs test suites under the current directory recursively."}, + {KeyPath: "C.SkipPackage", Name: "skip-package", SectionKey: "multiple-suites", DeprecatedName: "skipPackage", DeprecatedDocLink: "changed-command-line-flags", + UsageArgument: "comma-separated list of packages", + Usage: "A comma-separated list of package names to be skipped. If any part of the package's path matches, that package is ignored."}, + {KeyPath: "C.RequireSuite", Name: "require-suite", SectionKey: "failure", DeprecatedName: "requireSuite", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, Ginkgo fails if there are ginkgo tests in a directory but no invocation of RunSpecs."}, + {KeyPath: "C.NumCompilers", Name: "compilers", SectionKey: "multiple-suites", UsageDefaultValue: "0 (will autodetect)", + Usage: "When running multiple packages, the number of concurrent compilations to perform."}, +} + +var GinkgoCLIRunAndWatchFlags = GinkgoFlags{ + {KeyPath: "C.Nodes", Name: "nodes", SectionKey: "parallel", UsageDefaultValue: "1 (run in series)", + Usage: "The number of parallel test nodes to run."}, + {KeyPath: "C.Parallel", Name: "p", SectionKey: "parallel", + Usage: "If set, ginkgo will run in parallel with an auto-detected number of nodes."}, + {KeyPath: "C.AfterRunHook", Name: "after-run-hook", SectionKey: "misc", DeprecatedName: "afterSuiteHook", DeprecatedDocLink: "changed-command-line-flags", + Usage: "Command to run when a test suite completes."}, + {KeyPath: "C.Timeout", Name: "timeout", SectionKey: "debug", UsageDefaultValue: "24h", + Usage: "Test suite fails if it does not complete within the specified timeout."}, + {KeyPath: "C.OutputDir", Name: "output-dir", SectionKey: "output", UsageArgument: "directory", DeprecatedName: "outputdir", DeprecatedDocLink: "changed-profiling-support", + Usage: "A location to place all generated profiles and reports."}, + {KeyPath: "C.KeepSeparateCoverprofiles", Name: "keep-separate-coverprofiles", SectionKey: "code-and-coverage-analysis", + Usage: "If set, Ginkgo does not merge coverprofiles into one monolithic coverprofile. The coverprofiles will remain in their respective package direcotries or in -output-dir if set."}, + + {KeyPath: "Dcli.Stream", DeprecatedName: "stream", DeprecatedDocLink: "removed--stream"}, + {KeyPath: "Dcli.Notify", DeprecatedName: "notify", DeprecatedDocLink: "removed--notify"}, +} + +var GinkgoCLIRunFlags = GinkgoFlags{ + {KeyPath: "C.KeepGoing", Name: "keep-going", SectionKey: "multiple-suites", DeprecatedName: "keepGoing", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, failures from earlier test suites do not prevent later test suites from running."}, + {KeyPath: "C.UntilItFails", Name: "until-it-fails", SectionKey: "debug", DeprecatedName: "untilItFails", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, ginkgo will keep rerunning test suites until a failure occurs."}, + {KeyPath: "C.RandomizeSuites", Name: "randomize-suites", SectionKey: "order", DeprecatedName: "randomizeSuites", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, ginkgo will randomize the order in which test suites run."}, +} + +var GinkgoCLIWatchFlags = GinkgoFlags{ + {KeyPath: "C.Depth", Name: "depth", SectionKey: "watch", + Usage: "Ginkgo will watch dependencies down to this depth in the dependency tree."}, + {KeyPath: "C.WatchRegExp", Name: "watch-regexp", SectionKey: "watch", DeprecatedName: "watchRegExp", DeprecatedDocLink: "changed-command-line-flags", + UsageArgument: "Regular Expression", + UsageDefaultValue: `\.go$`, + Usage: "Only files matching this regular expression will be watched for changes."}, +} + +func VetAndInitializeCLIAndGoConfig(cliConfig GinkgoCLIConfigType, goFlagsConfig GoFlagsConfigType) (GinkgoCLIConfigType, GoFlagsConfigType, []error) { + errors := []error{} + + //initialize the output directory + if cliConfig.OutputDir != "" { + err := os.MkdirAll(cliConfig.OutputDir, 0777) + if err != nil { + errors = append(errors, err) + } + } + + //ensure cover mode is configured appropriately + if goFlagsConfig.CoverMode != "" || goFlagsConfig.CoverPkg != "" || goFlagsConfig.CoverProfile != "" { + goFlagsConfig.Cover = true + } + if goFlagsConfig.Cover && goFlagsConfig.CoverProfile == "" { + goFlagsConfig.CoverProfile = "coverprofile.out" + } + + return cliConfig, goFlagsConfig, errors +} + +func GenerateTestRunArgs(ginkgoConfig GinkgoConfigType, reporterConfig DefaultReporterConfigType, goFlagsConfig GoFlagsConfigType) ([]string, error) { + var flags GinkgoFlags + flags = GinkgoConfigFlags.WithPrefix("ginkgo") + flags = flags.CopyAppend(GinkgoParallelConfigFlags.WithPrefix("ginkgo")...) + flags = flags.CopyAppend(ReporterConfigFlags.WithPrefix("ginkgo")...) + flags = flags.CopyAppend(GoRunFlags.WithPrefix("test")...) + bindings := map[string]interface{}{ + "G": &ginkgoConfig, + "R": &reporterConfig, + "Go": &goFlagsConfig, + } + + return GenerateFlagArgs(flags, bindings) +} + +func BuildRunCommandFlagSet(ginkgoConfig *GinkgoConfigType, reporterConfig *DefaultReporterConfigType, cliConfig *GinkgoCLIConfigType, goFlagsConfig *GoFlagsConfigType) (GinkgoFlagSet, error) { + flags := GinkgoConfigFlags + flags = flags.CopyAppend(ReporterConfigFlags...) + flags = flags.CopyAppend(GinkgoCLISharedFlags...) + flags = flags.CopyAppend(GinkgoCLIRunAndWatchFlags...) + flags = flags.CopyAppend(GinkgoCLIRunFlags...) + flags = flags.CopyAppend(GoBuildFlags...) + flags = flags.CopyAppend(GoRunFlags...) + + bindings := map[string]interface{}{ + "G": ginkgoConfig, + "R": reporterConfig, + "C": cliConfig, + "Go": goFlagsConfig, + "D": &deprecatedConfigsType{}, + "Dcli": &deprecatedGinkgoCLIConfig{}, + } + + return NewGinkgoFlagSet(flags, bindings, FlagSections) +} + +func BuildWatchCommandFlagSet(ginkgoConfig *GinkgoConfigType, reporterConfig *DefaultReporterConfigType, cliConfig *GinkgoCLIConfigType, goFlagsConfig *GoFlagsConfigType) (GinkgoFlagSet, error) { + flags := GinkgoConfigFlags + flags = flags.CopyAppend(ReporterConfigFlags...) + flags = flags.CopyAppend(GinkgoCLISharedFlags...) + flags = flags.CopyAppend(GinkgoCLIRunAndWatchFlags...) + flags = flags.CopyAppend(GinkgoCLIWatchFlags...) + flags = flags.CopyAppend(GoBuildFlags...) + flags = flags.CopyAppend(GoRunFlags...) + + bindings := map[string]interface{}{ + "G": ginkgoConfig, + "R": reporterConfig, + "C": cliConfig, + "Go": goFlagsConfig, + "D": &deprecatedConfigsType{}, + "Dcli": &deprecatedGinkgoCLIConfig{}, + } + + return NewGinkgoFlagSet(flags, bindings, FlagSections) +} + +func BuildBuildCommandFlagSet(cliConfig *GinkgoCLIConfigType, goFlagsConfig *GoFlagsConfigType) (GinkgoFlagSet, error) { + flags := GinkgoCLISharedFlags + flags = flags.CopyAppend(GoBuildFlags...) + + bindings := map[string]interface{}{ + "C": cliConfig, + "Go": goFlagsConfig, + "D": &deprecatedConfigsType{}, + "DCli": &deprecatedGinkgoCLIConfig{}, + } + + flagSections := make(GinkgoFlagSections, len(FlagSections)) + copy(flagSections, FlagSections) + for i := range flagSections { + if flagSections[i].Key == "multiple-suites" { + flagSections[i].Heading = "Building Multiple Suites" + } + if flagSections[i].Key == "go-build" { + flagSections[i] = GinkgoFlagSection{Key: "go-build", Style: "{{/}}", Heading: "Go Build Flags", + Description: "These flags are inherited from go build."} + } + } + + return NewGinkgoFlagSet(flags, bindings, flagSections) +} diff --git a/config/cli_go_flags.go b/config/cli_go_flags.go new file mode 100644 index 0000000000..c744e322b5 --- /dev/null +++ b/config/cli_go_flags.go @@ -0,0 +1,148 @@ +package config + +// A subset of Go flags are exposed by Ginkgo. Some are avaiable at compile time (e.g. ginkgo build) and others only at run time (e.g. ginkgo run - which has both build and run time flags). +// More details can be found at: +// https://docs.google.com/spreadsheets/d/1zkp-DS4hU4sAJl5eHh1UmgwxCPQhf3s5a8fbiOI8tJU/ + +type GoFlagsConfigType struct { + //build-time flags for code-and-performance analysis + Race bool + Cover bool + CoverMode string + CoverPkg string + Vet string + + //run-time flags for code-and-performance analysis + BlockProfile string + BlockProfileRate int + CoverProfile string + CPUProfile string + MemProfile string + MemProfileRate int + MutexProfile string + MutexProfileFraction int + Trace string + + //build-time flags for building + A bool + ASMFlags string + BuildMode string + Compiler string + GCCGoFlags string + GCFlags string + InstallSuffix string + LDFlags string + LinkShared bool + Mod bool + N bool + ModFile string + ModCacheRW bool + MSan bool + PkgDir string + Tags string + TrimPath bool + ToolExec string + Work bool + X bool +} + +func NewDefaultGoFlagsConfig() GoFlagsConfigType { + return GoFlagsConfigType{} +} + +var GoBuildFlags = GinkgoFlags{ + {KeyPath: "Go.Race", Name: "race", SectionKey: "code-and-coverage-analysis", + Usage: "enable data race detection. Supported only on linux/amd64, freebsd/amd64, darwin/amd64, windows/amd64, linux/ppc64le and linux/arm64 (only for 48-bit VMA)."}, + {KeyPath: "Go.Vet", Name: "vet", UsageArgument: "list", SectionKey: "code-and-coverage-analysis", + Usage: `Configure the invocation of "go vet" during "go test" to use the comma-separated list of vet checks. If list is empty, "go test" runs "go vet" with a curated list of checks believed to be always worth addressing. If list is "off", "go test" does not run "go vet" at all. Available checks can be found by running 'go doc cmd/vet'`}, + {KeyPath: "Go.Cover", Name: "cover", SectionKey: "code-and-coverage-analysis", + Usage: "Enable coverage analysis. Note that because coverage works by annotating the source code before compilation, compilation and test failures with coverage enabled may report line numbers that don't correspond to the original sources."}, + {KeyPath: "Go.CoverMode", Name: "covermode", UsageArgument: "set,count,atomic", SectionKey: "code-and-coverage-analysis", + Usage: `Set the mode for coverage analysis for the package[s] being tested. 'set': does this statement run? 'count': how many times does this statement run? 'atomic': like count, but correct in multithreaded tests and more expensive (must use atomic with -race). Sets -cover`}, + {KeyPath: "Go.CoverPkg", Name: "coverpkg", UsageArgument: "pattern1,pattern2,pattern3", SectionKey: "code-and-coverage-analysis", + Usage: "Apply coverage analysis in each test to packages matching the patterns. The default is for each test to analyze only the package being tested. See 'go help packages' for a description of package patterns. Sets -cover."}, + + {KeyPath: "Go.A", Name: "a", SectionKey: "go-build", + Usage: "force rebuilding of packages that are already up-to-date."}, + {KeyPath: "Go.ASMFlags", Name: "asmflags", UsageArgument: "'[pattern=]arg list'", SectionKey: "go-build", + Usage: "arguments to pass on each go tool asm invocation."}, + {KeyPath: "Go.BuildMode", Name: "buildmode", UsageArgument: "mode", SectionKey: "go-build", + Usage: "build mode to use. See 'go help buildmode' for more."}, + {KeyPath: "Go.Compiler", Name: "compiler", UsageArgument: "name", SectionKey: "go-build", + Usage: "name of compiler to use, as in runtime.Compiler (gccgo or gc)."}, + {KeyPath: "Go.GCCGoFlags", Name: "gccgoflags", UsageArgument: "'[pattern=]arg list'", SectionKey: "go-build", + Usage: "arguments to pass on each gccgo compiler/linker invocation."}, + {KeyPath: "Go.GCFlags", Name: "gcflags", UsageArgument: "'[pattern=]arg list'", SectionKey: "go-build", + Usage: "arguments to pass on each go tool compile invocation."}, + {KeyPath: "Go.InstallSuffix", Name: "installsuffix", SectionKey: "go-build", + Usage: "a suffix to use in the name of the package installation directory, in order to keep output separate from default builds. If using the -race flag, the install suffix is automatically set to raceor, if set explicitly, has _race appended to it. Likewise for the -msan flag. Using a -buildmode option that requires non-default compile flags has a similar effect."}, + {KeyPath: "Go.LDFlags", Name: "ldflags", UsageArgument: "'[pattern=]arg list'", SectionKey: "go-build", + Usage: "arguments to pass on each go tool link invocation."}, + {KeyPath: "Go.LinkShared", Name: "linkshared", SectionKey: "go-build", + Usage: "build code that will be linked against shared libraries previously created with -buildmode=shared."}, + {KeyPath: "Go.Mod", Name: "mod", UsageArgument: "mode (readonly, vender, or mod)", SectionKey: "go-build", + Usage: "module download mode to use: readonly, vendor, or mod. See 'go help modules' for more."}, + {KeyPath: "Go.ModCacheRW", Name: "modcacherw", SectionKey: "go-build", + Usage: "leave newly-created directories in the module cache read-write instead of making them read-only."}, + {KeyPath: "Go.ModFile", Name: "modfile", UsageArgument: "file", SectionKey: "go-build", + Usage: `in module aware mode, read (and possibly write) an alternate go.mod file instead of the one in the module root directory. A file named go.mod must still be present in order to determine the module root directory, but it is not accessed. When -modfile is specified, an alternate go.sum file is also used: its path is derived from the -modfile flag by trimming the ".mod" extension and appending ".sum".`}, + {KeyPath: "Go.MSan", Name: "msan", SectionKey: "go-build", + Usage: "enable interoperation with memory sanitizer. Supported only on linux/amd64, linux/arm64 and only with Clang/LLVM as the host C compiler. On linux/arm64, pie build mode will be used."}, + {KeyPath: "Go.N", Name: "n", SectionKey: "go-build", + Usage: "print the commands but do not run them."}, + {KeyPath: "Go.PkgDir", Name: "pkgdir", UsageArgument: "dir", SectionKey: "go-build", + Usage: "install and load all packages from dir instead of the usual locations. For example, when building with a non-standard configuration, use -pkgdir to keep generated packages in a separate location."}, + {KeyPath: "Go.Tags", Name: "tags", UsageArgument: "tag,list", SectionKey: "go-build", + Usage: "a comma-separated list of build tags to consider satisfied during the build. For more information about build tags, see the description of build constraints in the documentation for the go/build package. (Earlier versions of Go used a space-separated list, and that form is deprecated but still recognized.)"}, + {KeyPath: "Go.TrimPath", Name: "trimpath", SectionKey: "go-build", + Usage: `remove all file system paths from the resulting executable. Instead of absolute file system paths, the recorded file names will begin with either "go" (for the standard library), or a module path@version (when using modules), or a plain import path (when using GOPATH).`}, + {KeyPath: "Go.ToolExec", Name: "toolexec", UsageArgument: "'cmd args'", SectionKey: "go-build", + Usage: "a program to use to invoke toolchain programs like vet and asm. For example, instead of running asm, the go command will run cmd args /path/to/asm '."}, + {KeyPath: "Go.Work", Name: "work", SectionKey: "go-build", + Usage: "print the name of the temporary work directory and do not delete it when exiting."}, + {KeyPath: "Go.X", Name: "x", SectionKey: "go-build", + Usage: "print the commands."}, +} + +var GoRunFlags = GinkgoFlags{ + {KeyPath: "Go.CoverProfile", Name: "coverprofile", UsageArgument: "file", SectionKey: "code-and-coverage-analysis", + Usage: `Write a coverage profile to the file after all tests have passed. Sets -cover.`}, + {KeyPath: "Go.BlockProfile", Name: "blockprofile", UsageArgument: "rile", SectionKey: "performance-analysis", + Usage: `Write a goroutine blocking profile to the specified file when all tests are complete. Preserves test binary.`}, + {KeyPath: "Go.BlockProfileRate", Name: "blockprofilerate", UsageArgument: "rate", SectionKey: "performance-analysis", + Usage: `Control the detail provided in goroutine blocking profiles by calling runtime.SetBlockProfileRate with rate. See 'go doc runtime.SetBlockProfileRate'. The profiler aims to sample, on average, one blocking event every n nanoseconds the program spends blocked. By default, if -test.blockprofile is set without this flag, all blocking events are recorded, equivalent to -test.blockprofilerate=1.`}, + {KeyPath: "Go.CPUProfile", Name: "cpuprofile", UsageArgument: "file", SectionKey: "performance-analysis", + Usage: `Write a CPU profile to the specified file before exiting. Preserves test binary.`}, + {KeyPath: "Go.MemProfile", Name: "memprofile", UsageArgument: "file", SectionKey: "performance-analysis", + Usage: `Write an allocation profile to the file after all tests have passed. Preserves test binary.`}, + {KeyPath: "Go.MemProfileRate", Name: "memprofilerate", UsageArgument: "rate", SectionKey: "performance-analysis", + Usage: `Enable more precise (and expensive) memory allocation profiles by setting runtime.MemProfileRate. See 'go doc runtime.MemProfileRate'. To profile all memory allocations, use -test.memprofilerate=1.`}, + {KeyPath: "Go.MutexProfile", Name: "mutexprofile", UsageArgument: "file", SectionKey: "performance-analysis", + Usage: `Write a mutex contention profile to the specified file when all tests are complete. Preserves test binary.`}, + {KeyPath: "Go.MutexProfileFraction", Name: "mutexprofilefraction", UsageArgument: "n", SectionKey: "performance-analysis", + Usage: `if >= 0, calls runtime.SetMutexProfileFraction() Sample 1 in n stack traces of goroutines holding a contended mutex.`}, + {KeyPath: "Go.Trace", Name: "execution-trace", UsageArgument: "file", ExportAs: "trace", SectionKey: "performance-analysis", + Usage: `Write an execution trace to the specified file before exiting.`}, +} + +func GenerateGoTestCompileArgs(goFlagsConfig GoFlagsConfigType, destination string, packageToBuild string) ([]string, error) { + // if the user has set the CoverProfile run-time flag make sure to set the build-time cover flag to make sure + // the built test binary can generate a coverprofile + if goFlagsConfig.CoverProfile != "" { + goFlagsConfig.Cover = true + } + + args := []string{"test", "-c", "-o", destination, packageToBuild} + goArgs, err := GenerateFlagArgs( + GoBuildFlags, + map[string]interface{}{ + "Go": &goFlagsConfig, + }, + ) + + if err != nil { + return []string{}, err + } + args = append(args, goArgs...) + return args, nil +} diff --git a/config/config.go b/config/config.go index 25f8758a62..f2e972dc87 100644 --- a/config/config.go +++ b/config/config.go @@ -15,12 +15,13 @@ package config import ( "flag" + "net/http" "time" - "fmt" + "github.com/onsi/ginkgo/types" ) -const VERSION = "1.16.0" +const VERSION = "2.0.0-alpha" type GinkgoConfigType struct { RandomSeed int64 @@ -28,7 +29,6 @@ type GinkgoConfigType struct { RegexScansFilePath bool FocusStrings []string SkipStrings []string - SkipMeasurements bool FailOnPending bool FailFast bool FlakeAttempts int @@ -38,195 +38,169 @@ type GinkgoConfigType struct { ParallelNode int ParallelTotal int - SyncHost string - StreamHost string + ParallelHost string } -var GinkgoConfig = GinkgoConfigType{} - type DefaultReporterConfigType struct { NoColor bool SlowSpecThreshold float64 - NoisyPendings bool - NoisySkippings bool Succinct bool Verbose bool FullTrace bool ReportPassed bool - ReportFile string + JUnitReportFile string } -var DefaultReporterConfig = DefaultReporterConfigType{} - -func processPrefix(prefix string) string { - if prefix != "" { - prefix += "." - } - return prefix +type deprecatedConfigsType struct { + NoisySkippings bool + NoisyPendings bool } -type flagFunc func(string) - -func (f flagFunc) String() string { return "" } -func (f flagFunc) Set(s string) error { f(s); return nil } - -func Flags(flagSet *flag.FlagSet, prefix string, includeParallelFlags bool) { - prefix = processPrefix(prefix) - flagSet.Int64Var(&(GinkgoConfig.RandomSeed), prefix+"seed", time.Now().Unix(), "The seed used to randomize the spec suite.") - flagSet.BoolVar(&(GinkgoConfig.RandomizeAllSpecs), prefix+"randomizeAllSpecs", false, "If set, ginkgo will randomize all specs together. By default, ginkgo only randomizes the top level Describe, Context and When groups.") - flagSet.BoolVar(&(GinkgoConfig.SkipMeasurements), prefix+"skipMeasurements", false, "If set, ginkgo will skip any measurement specs.") - flagSet.BoolVar(&(GinkgoConfig.FailOnPending), prefix+"failOnPending", false, "If set, ginkgo will mark the test suite as failed if any specs are pending.") - flagSet.BoolVar(&(GinkgoConfig.FailFast), prefix+"failFast", false, "If set, ginkgo will stop running a test suite after a failure occurs.") - - flagSet.BoolVar(&(GinkgoConfig.DryRun), prefix+"dryRun", false, "If set, ginkgo will walk the test hierarchy without actually running anything. Best paired with -v.") - - flagSet.Var(flagFunc(flagFocus), prefix+"focus", "If set, ginkgo will only run specs that match this regular expression. Can be specified multiple times, values are ORed.") - flagSet.Var(flagFunc(flagSkip), prefix+"skip", "If set, ginkgo will only run specs that do not match this regular expression. Can be specified multiple times, values are ORed.") - - flagSet.BoolVar(&(GinkgoConfig.RegexScansFilePath), prefix+"regexScansFilePath", false, "If set, ginkgo regex matching also will look at the file path (code location).") - - flagSet.IntVar(&(GinkgoConfig.FlakeAttempts), prefix+"flakeAttempts", 1, "Make up to this many attempts to run each spec. Please note that if any of the attempts succeed, the suite will not be failed. But any failures will still be recorded.") - - flagSet.BoolVar(&(GinkgoConfig.EmitSpecProgress), prefix+"progress", false, "If set, ginkgo will emit progress information as each spec runs to the GinkgoWriter.") - - flagSet.BoolVar(&(GinkgoConfig.DebugParallel), prefix+"debug", false, "If set, ginkgo will emit node output to files when running in parallel.") - - if includeParallelFlags { - flagSet.IntVar(&(GinkgoConfig.ParallelNode), prefix+"parallel.node", 1, "This worker node's (one-indexed) node number. For running specs in parallel.") - flagSet.IntVar(&(GinkgoConfig.ParallelTotal), prefix+"parallel.total", 1, "The total number of worker nodes. For running specs in parallel.") - flagSet.StringVar(&(GinkgoConfig.SyncHost), prefix+"parallel.synchost", "", "The address for the server that will synchronize the running nodes.") - flagSet.StringVar(&(GinkgoConfig.StreamHost), prefix+"parallel.streamhost", "", "The address for the server that the running nodes should stream data to.") +func NewDefaultGinkgoConfig() GinkgoConfigType { + return GinkgoConfigType{ + RandomSeed: time.Now().Unix(), + ParallelNode: 1, + ParallelTotal: 1, } - - flagSet.BoolVar(&(DefaultReporterConfig.NoColor), prefix+"noColor", false, "If set, suppress color output in default reporter.") - flagSet.Float64Var(&(DefaultReporterConfig.SlowSpecThreshold), prefix+"slowSpecThreshold", 5.0, "(in seconds) Specs that take longer to run than this threshold are flagged as slow by the default reporter.") - flagSet.BoolVar(&(DefaultReporterConfig.NoisyPendings), prefix+"noisyPendings", true, "If set, default reporter will shout about pending tests.") - flagSet.BoolVar(&(DefaultReporterConfig.NoisySkippings), prefix+"noisySkippings", true, "If set, default reporter will shout about skipping tests.") - flagSet.BoolVar(&(DefaultReporterConfig.Verbose), prefix+"v", false, "If set, default reporter print out all specs as they begin.") - flagSet.BoolVar(&(DefaultReporterConfig.Succinct), prefix+"succinct", false, "If set, default reporter prints out a very succinct report") - flagSet.BoolVar(&(DefaultReporterConfig.FullTrace), prefix+"trace", false, "If set, default reporter prints out the full stack trace when a failure occurs") - flagSet.BoolVar(&(DefaultReporterConfig.ReportPassed), prefix+"reportPassed", false, "If set, default reporter prints out captured output of passed tests.") - flagSet.StringVar(&(DefaultReporterConfig.ReportFile), prefix+"reportFile", "", "Override the default reporter output file path.") - } -func BuildFlagArgs(prefix string, ginkgo GinkgoConfigType, reporter DefaultReporterConfigType) []string { - prefix = processPrefix(prefix) - result := make([]string, 0) - - if ginkgo.RandomSeed > 0 { - result = append(result, fmt.Sprintf("--%sseed=%d", prefix, ginkgo.RandomSeed)) - } - - if ginkgo.RandomizeAllSpecs { - result = append(result, fmt.Sprintf("--%srandomizeAllSpecs", prefix)) - } - - if ginkgo.SkipMeasurements { - result = append(result, fmt.Sprintf("--%sskipMeasurements", prefix)) - } - - if ginkgo.FailOnPending { - result = append(result, fmt.Sprintf("--%sfailOnPending", prefix)) - } - - if ginkgo.FailFast { - result = append(result, fmt.Sprintf("--%sfailFast", prefix)) - } - - if ginkgo.DryRun { - result = append(result, fmt.Sprintf("--%sdryRun", prefix)) - } - - for _, s := range ginkgo.FocusStrings { - result = append(result, fmt.Sprintf("--%sfocus=%s", prefix, s)) - } - - for _, s := range ginkgo.SkipStrings { - result = append(result, fmt.Sprintf("--%sskip=%s", prefix, s)) - } - - if ginkgo.FlakeAttempts > 1 { - result = append(result, fmt.Sprintf("--%sflakeAttempts=%d", prefix, ginkgo.FlakeAttempts)) - } - - if ginkgo.EmitSpecProgress { - result = append(result, fmt.Sprintf("--%sprogress", prefix)) - } - - if ginkgo.DebugParallel { - result = append(result, fmt.Sprintf("--%sdebug", prefix)) - } - - if ginkgo.ParallelNode != 0 { - result = append(result, fmt.Sprintf("--%sparallel.node=%d", prefix, ginkgo.ParallelNode)) +func NewDefaultReporterConfig() DefaultReporterConfigType { + return DefaultReporterConfigType{ + SlowSpecThreshold: 5.0, } +} - if ginkgo.ParallelTotal != 0 { - result = append(result, fmt.Sprintf("--%sparallel.total=%d", prefix, ginkgo.ParallelTotal)) - } +var GinkgoConfig = NewDefaultGinkgoConfig() +var DefaultReporterConfig = NewDefaultReporterConfig() + +var GinkgoConfigFlags = GinkgoFlags{ + {KeyPath: "G.RandomSeed", Name: "seed", SectionKey: "order", UsageDefaultValue: "randomly generated by Ginkgo", + Usage: "The seed used to randomize the spec suite."}, + {KeyPath: "G.RandomizeAllSpecs", Name: "randomize-all", SectionKey: "order", DeprecatedName: "randomizeAllSpecs", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, ginkgo will randomize all specs together. By default, ginkgo only randomizes the top level Describe, Context and When containers."}, + + {KeyPath: "G.FailOnPending", Name: "fail-on-pending", SectionKey: "failure", DeprecatedName: "failOnPending", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, ginkgo will mark the test suite as failed if any specs are pending."}, + {KeyPath: "G.FailFast", Name: "fail-fast", SectionKey: "failure", DeprecatedName: "failFast", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, ginkgo will stop running a test suite after a failure occurs."}, + {KeyPath: "G.FlakeAttempts", Name: "flake-attempts", SectionKey: "failure", UsageDefaultValue: "0 - failed tests are not retried", DeprecatedName: "flakeAttempts", DeprecatedDocLink: "changed-command-line-flags", + Usage: "Make up to this many attempts to run each spec. If any of the attempts succeed, the suite will not be failed."}, + + {KeyPath: "G.DryRun", Name: "dry-run", SectionKey: "debug", DeprecatedName: "dryRun", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, ginkgo will walk the test hierarchy without actually running anything. Best paired with -v."}, + {KeyPath: "G.EmitSpecProgress", Name: "progress", SectionKey: "debug", + Usage: "If set, ginkgo will emit progress information as each spec runs to the GinkgoWriter."}, + + {KeyPath: "G.FocusStrings", Name: "focus", SectionKey: "filter", + Usage: "If set, ginkgo will only run specs that match this regular expression. Can be specified multiple times, values are ORed."}, + {KeyPath: "G.SkipStrings", Name: "skip", SectionKey: "filter", + Usage: "If set, ginkgo will only run specs that do not match this regular expression. Can be specified multiple times, values are ORed."}, + {KeyPath: "G.RegexScansFilePath", Name: "regex-scans-filepath", SectionKey: "filter", DeprecatedName: "regexScansFilePath", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, ginkgo regex matching also will look at the file path (code location)."}, + {KeyPath: "G.DebugParallel", Name: "debug-parallel", SectionKey: "debug", DeprecatedName: "debug", + Usage: "If set, ginkgo will emit node output to files when running in parallel."}, +} - if ginkgo.StreamHost != "" { - result = append(result, fmt.Sprintf("--%sparallel.streamhost=%s", prefix, ginkgo.StreamHost)) - } +var GinkgoParallelConfigFlags = GinkgoFlags{ + {KeyPath: "G.ParallelNode", Name: "parallel.node", SectionKey: "low-level-parallel", UsageDefaultValue: "1", + Usage: "This worker node's (one-indexed) node number. For running specs in parallel."}, + {KeyPath: "G.ParallelTotal", Name: "parallel.total", SectionKey: "low-level-parallel", UsageDefaultValue: "1", + Usage: "The total number of worker nodes. For running specs in parallel."}, + {KeyPath: "G.ParallelHost", Name: "parallel.host", SectionKey: "low-level-parallel", UsageDefaultValue: "set by Ginkgo CLI", + Usage: "The address for the server that will synchronize the running nodes."}, +} - if ginkgo.SyncHost != "" { - result = append(result, fmt.Sprintf("--%sparallel.synchost=%s", prefix, ginkgo.SyncHost)) - } +var ReporterConfigFlags = GinkgoFlags{ + {KeyPath: "R.NoColor", Name: "no-color", SectionKey: "output", DeprecatedName: "noColor", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, suppress color output in default reporter."}, + {KeyPath: "R.SlowSpecThreshold", Name: "slow-spec-threshold", SectionKey: "output", UsageArgument: "float, in seconds", UsageDefaultValue: "5.0", DeprecatedName: "slowSpecThreshold", DeprecatedDocLink: "changed-command-line-flags", + Usage: "Specs that take longer to run than this threshold are flagged as slow by the default reporter."}, + {KeyPath: "D.NoisyPendings", DeprecatedName: "noisyPendings", DeprecatedDocLink: "removed--noisypendings-and--noisyskippings"}, + {KeyPath: "D.NoisySkippings", DeprecatedName: "noisySkippings", DeprecatedDocLink: "removed--noisypendings-and--noisyskippings"}, + {KeyPath: "R.Verbose", Name: "v", SectionKey: "output", + Usage: "If set, default reporter print out all specs as they begin."}, + {KeyPath: "R.Succinct", Name: "succinct", SectionKey: "output", + Usage: "If set, default reporter prints out a very succinct report"}, + {KeyPath: "R.FullTrace", Name: "trace", SectionKey: "output", + Usage: "If set, default reporter prints out the full stack trace when a failure occurs"}, + {KeyPath: "R.ReportPassed", Name: "report-passed", SectionKey: "output", DeprecatedName: "reportPassed", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, default reporter prints out captured output of passed tests."}, + {KeyPath: "R.JUnitReportFile", Name: "junit-report", SectionKey: "output", DeprecatedName: "reportFile", DeprecatedDocLink: "changed-command-line-flags", + Usage: "If set, Ginkgo will generate a junit test report at the specified destination."}, +} - if ginkgo.RegexScansFilePath { - result = append(result, fmt.Sprintf("--%sregexScansFilePath", prefix)) - } +var FlagSections = GinkgoFlagSections{ + {Key: "multiple-suites", Style: "{{dark-green}}", Heading: "Running Multiple Test Suites"}, + {Key: "order", Style: "{{green}}", Heading: "Controlling Test Order"}, + {Key: "parallel", Style: "{{yellow}}", Heading: "Controlling Test Parallelism"}, + {Key: "low-level-parallel", Style: "{{yellow}}", Heading: "Controlling Test Parallelism", + Description: "These are set by the Ginkgo CLI, {{red}}{{bold}}do not set them manually{{/}} via go test.\nUse ginkgo -p or ginkgo -nodes=N instead."}, + {Key: "filter", Style: "{{cyan}}", Heading: "Filtering Tests"}, + {Key: "failure", Style: "{{red}}", Heading: "Failure Handling"}, + {Key: "output", Style: "{{magenta}}", Heading: "Controlling Output Formatting"}, + {Key: "code-and-coverage-analysis", Style: "{{orange}}", Heading: "Code and Coverage Analysis"}, + {Key: "performance-analysis", Style: "{{coral}}", Heading: "Performance Analysis"}, + {Key: "debug", Style: "{{blue}}", Heading: "Debugging Tests"}, + {Key: "watch", Style: "{{light-yellow}}", Heading: "Controlling Ginkgo Watch"}, + {Key: "misc", Style: "{{light-gray}}", Heading: "Miscellaneous"}, + {Key: "go-build", Style: "{{light-gray}}", Heading: "Go Build Flags", Succinct: true, + Description: "These flags are inherited from go build. Run {{bold}}ginkgo help build{{/}} for more detailed flag documentation."}, +} - if reporter.NoColor { - result = append(result, fmt.Sprintf("--%snoColor", prefix)) +// The FlagSet for the user's test sute itself. +func BuildTestSuiteFlagSet() (GinkgoFlagSet, error) { + flags := GinkgoConfigFlags.CopyAppend(GinkgoParallelConfigFlags...).CopyAppend(ReporterConfigFlags...) + flags = flags.WithPrefix("ginkgo") + bindings := map[string]interface{}{ + "G": &GinkgoConfig, + "R": &DefaultReporterConfig, + "D": &deprecatedConfigsType{}, } + extraGoFlagsSection := GinkgoFlagSection{Style: "{{gray}}", Heading: "Go test flags"} - if reporter.SlowSpecThreshold > 0 { - result = append(result, fmt.Sprintf("--%sslowSpecThreshold=%.5f", prefix, reporter.SlowSpecThreshold)) - } + return NewAttachedGinkgoFlagSet(flag.CommandLine, flags, bindings, FlagSections, extraGoFlagsSection) +} - if !reporter.NoisyPendings { - result = append(result, fmt.Sprintf("--%snoisyPendings=false", prefix)) - } +func VetConfig(flagSet GinkgoFlagSet, config GinkgoConfigType, reporterConfig DefaultReporterConfigType) []error { + errors := []error{} - if !reporter.NoisySkippings { - result = append(result, fmt.Sprintf("--%snoisySkippings=false", prefix)) + if flagSet.WasSet("count") || flagSet.WasSet("test.count") { + errors = append(errors, types.GinkgoErrors.InvalidGoFlagCount()) } - if reporter.Verbose { - result = append(result, fmt.Sprintf("--%sv", prefix)) + if flagSet.WasSet("parallel") || flagSet.WasSet("test.parallel") { + errors = append(errors, types.GinkgoErrors.InvalidGoFlagParallel()) } - if reporter.Succinct { - result = append(result, fmt.Sprintf("--%ssuccinct", prefix)) + if config.ParallelTotal < 1 { + errors = append(errors, types.GinkgoErrors.InvalidParallelTotalConfiguration()) } - if reporter.FullTrace { - result = append(result, fmt.Sprintf("--%strace", prefix)) + if config.ParallelNode > config.ParallelTotal || config.ParallelNode < 1 { + errors = append(errors, types.GinkgoErrors.InvalidParallelNodeConfiguration()) } - if reporter.ReportPassed { - result = append(result, fmt.Sprintf("--%sreportPassed", prefix)) + if config.ParallelTotal > 1 { + if config.ParallelHost == "" { + errors = append(errors, types.GinkgoErrors.MissingParallelHostConfiguration()) + } else { + resp, err := http.Get(config.ParallelHost + "/up") + if err != nil || resp.StatusCode != http.StatusOK { + errors = append(errors, types.GinkgoErrors.UnreachableParallelHost(config.ParallelHost)) + } + if err != nil { + resp.Body.Close() + } + } } - if reporter.ReportFile != "" { - result = append(result, fmt.Sprintf("--%sreportFile=%s", prefix, reporter.ReportFile)) + if config.DryRun && config.ParallelTotal > 1 { + errors = append(errors, types.GinkgoErrors.DryRunInParallelConfiguration()) } - return result -} - -// flagFocus implements the -focus flag. -func flagFocus(arg string) { - if arg != "" { - GinkgoConfig.FocusStrings = append(GinkgoConfig.FocusStrings, arg) + if reporterConfig.Succinct && reporterConfig.Verbose { + errors = append(errors, types.GinkgoErrors.ConflictingVerboseSuccinctConfiguration()) } -} -// flagSkip implements the -skip flag. -func flagSkip(arg string) { - if arg != "" { - GinkgoConfig.SkipStrings = append(GinkgoConfig.SkipStrings, arg) - } + return errors } diff --git a/internal/spec/spec_suite_test.go b/config/config_suite_test.go similarity index 57% rename from internal/spec/spec_suite_test.go rename to config/config_suite_test.go index 8681a72068..c7d2e82ab5 100644 --- a/internal/spec/spec_suite_test.go +++ b/config/config_suite_test.go @@ -1,13 +1,13 @@ -package spec_test +package config_test import ( + "testing" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - - "testing" ) -func TestSpec(t *testing.T) { +func TestConfig(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Spec Suite") + RunSpecs(t, "Config Suite") } diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000000..26d26f0435 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,154 @@ +package config_test + +import ( + "flag" + "net/http" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal/test_helpers" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/ghttp" +) + +var DEPRECATION_ANCHORS = test_helpers.LoadMarkdownHeadingAnchors("../docs/MIGRATING_TO_V2.md") + +var _ = Describe("Config", func() { + It("has valid deprecation doc links", func() { + flags := config.GinkgoConfigFlags.CopyAppend(config.GinkgoParallelConfigFlags...).CopyAppend(config.ReporterConfigFlags...) + for _, flag := range flags { + if flag.DeprecatedDocLink != "" { + Ω(flag.DeprecatedDocLink).Should(BeElementOf(DEPRECATION_ANCHORS)) + } + } + }) + + Describe("VetConfig", func() { + var conf config.GinkgoConfigType + var repConf config.DefaultReporterConfigType + var flagSet config.GinkgoFlagSet + var goFlagSet *flag.FlagSet + + BeforeEach(func() { + var err error + goFlagSet = flag.NewFlagSet("test", flag.ContinueOnError) + goFlagSet.Bool("count", false, "") + goFlagSet.Int("parallel", 0, "") + flagSet, err = config.NewAttachedGinkgoFlagSet(goFlagSet, config.GinkgoFlags{}, nil, config.GinkgoFlagSections{}, config.GinkgoFlagSection{}) + Ω(err).ShouldNot(HaveOccurred()) + + conf = config.NewDefaultGinkgoConfig() + repConf = config.NewDefaultReporterConfig() + }) + + Context("when all is well", func() { + It("retuns no errors", func() { + errors := config.VetConfig(flagSet, conf, repConf) + Ω(errors).Should(BeEmpty()) + }) + }) + + Context("when unsupported go flags are parsed", func() { + BeforeEach(func() { + goFlagSet.Parse([]string{"-count", "-parallel=2"}) + }) + It("returns errors when unsupported go flags are set", func() { + errors := config.VetConfig(flagSet, conf, repConf) + Ω(errors).Should(ConsistOf(types.GinkgoErrors.InvalidGoFlagCount(), types.GinkgoErrors.InvalidGoFlagParallel())) + }) + }) + + Describe("errors related to parallelism", func() { + Context("when parallel total is less than one", func() { + BeforeEach(func() { + conf.ParallelTotal = 0 + }) + + It("errors", func() { + errors := config.VetConfig(flagSet, conf, repConf) + Ω(errors).Should(ContainElement(types.GinkgoErrors.InvalidParallelTotalConfiguration())) + }) + }) + + Context("when parallel node is less than one", func() { + BeforeEach(func() { + conf.ParallelNode = 0 + }) + + It("errors", func() { + errors := config.VetConfig(flagSet, conf, repConf) + Ω(errors).Should(ConsistOf(types.GinkgoErrors.InvalidParallelNodeConfiguration())) + }) + }) + + Context("when parallel node is greater than parallel total", func() { + BeforeEach(func() { + conf.ParallelNode = conf.ParallelTotal + 1 + }) + + It("errors", func() { + errors := config.VetConfig(flagSet, conf, repConf) + Ω(errors).Should(ConsistOf(types.GinkgoErrors.InvalidParallelNodeConfiguration())) + }) + }) + + Context("when running in parallel", func() { + var server *ghttp.Server + BeforeEach(func() { + server = ghttp.NewServer() + server.SetAllowUnhandledRequests(true) + server.SetUnhandledRequestStatusCode(http.StatusOK) + + conf.ParallelTotal = 2 + conf.ParallelHost = server.URL() + }) + + AfterEach(func() { + server.Close() + }) + + Context("and parallel host is not set", func() { + BeforeEach(func() { + conf.ParallelHost = "" + }) + It("errors", func() { + errors := config.VetConfig(flagSet, conf, repConf) + Ω(errors).Should(ConsistOf(types.GinkgoErrors.MissingParallelHostConfiguration())) + }) + }) + + Context("and parallel host is set but fails", func() { + BeforeEach(func() { + server.SetUnhandledRequestStatusCode(http.StatusGone) + }) + It("errors", func() { + errors := config.VetConfig(flagSet, conf, repConf) + Ω(errors).Should(ConsistOf(types.GinkgoErrors.UnreachableParallelHost(server.URL()))) + }) + }) + + Context("when trying to dry run in parallel", func() { + BeforeEach(func() { + conf.DryRun = true + }) + It("errors", func() { + errors := config.VetConfig(flagSet, conf, repConf) + Ω(errors).Should(ConsistOf(types.GinkgoErrors.DryRunInParallelConfiguration())) + }) + }) + }) + }) + + Context("when succint and verbose are both set", func() { + BeforeEach(func() { + repConf.Succinct = true + repConf.Verbose = true + }) + It("errors", func() { + errors := config.VetConfig(flagSet, conf, repConf) + Ω(errors).Should(ConsistOf(types.GinkgoErrors.ConflictingVerboseSuccinctConfiguration())) + }) + }) + }) +}) diff --git a/config/flags.go b/config/flags.go new file mode 100644 index 0000000000..c50cfb31ba --- /dev/null +++ b/config/flags.go @@ -0,0 +1,467 @@ +package config + +import ( + "flag" + "fmt" + "io/ioutil" + "reflect" + "strings" + "time" + + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/types" +) + +type GinkgoFlag struct { + Name string + KeyPath string + SectionKey string + + Usage string + UsageArgument string + UsageDefaultValue string + + DeprecatedName string + DeprecatedDocLink string + + ExportAs string +} + +type GinkgoFlags []GinkgoFlag + +func (f GinkgoFlags) CopyAppend(flags ...GinkgoFlag) GinkgoFlags { + out := GinkgoFlags{} + out = append(out, f...) + out = append(out, flags...) + return out +} + +func (f GinkgoFlags) WithPrefix(prefix string) GinkgoFlags { + if prefix == "" { + return f + } + out := GinkgoFlags{} + for _, flag := range f { + if flag.Name != "" { + flag.Name = prefix + "." + flag.Name + } + if flag.DeprecatedName != "" { + flag.DeprecatedName = prefix + "." + flag.DeprecatedName + } + if flag.ExportAs != "" { + flag.ExportAs = prefix + "." + flag.ExportAs + } + out = append(out, flag) + } + return out +} + +type GinkgoFlagSection struct { + Key string + Style string + Succinct bool + Heading string + Description string +} + +type GinkgoFlagSections []GinkgoFlagSection + +func (gfs GinkgoFlagSections) Lookup(key string) (GinkgoFlagSection, bool) { + for _, section := range gfs { + if section.Key == key { + return section, true + } + } + + return GinkgoFlagSection{}, false +} + +type GinkgoFlagSet struct { + flags GinkgoFlags + bindings interface{} + + sections GinkgoFlagSections + extraGoFlagsSection GinkgoFlagSection + + flagSet *flag.FlagSet +} + +// Call NewGinkgoFlagSet to create GinkgoFlagSet that creates and binds to it's own *flag.FlagSet +func NewGinkgoFlagSet(flags GinkgoFlags, bindings interface{}, sections GinkgoFlagSections) (GinkgoFlagSet, error) { + return bindFlagSet(GinkgoFlagSet{ + flags: flags, + bindings: bindings, + sections: sections, + }, nil) +} + +// Call NewGinkgoFlagSet to create GinkgoFlagSet that extends an existing *flag.FlagSet +func NewAttachedGinkgoFlagSet(flagSet *flag.FlagSet, flags GinkgoFlags, bindings interface{}, sections GinkgoFlagSections, extraGoFlagsSection GinkgoFlagSection) (GinkgoFlagSet, error) { + return bindFlagSet(GinkgoFlagSet{ + flags: flags, + bindings: bindings, + sections: sections, + extraGoFlagsSection: extraGoFlagsSection, + }, flagSet) +} + +func bindFlagSet(f GinkgoFlagSet, flagSet *flag.FlagSet) (GinkgoFlagSet, error) { + if flagSet == nil { + f.flagSet = flag.NewFlagSet("", flag.ContinueOnError) + //supress all output as Ginkgo is reponsible for formatting usage + f.flagSet.SetOutput(ioutil.Discard) + } else { + f.flagSet = flagSet + //we're piggybacking on an existing flagset (typically go test) so we have limited control + //on user feedback + f.flagSet.Usage = f.substituteUsage + } + + for _, flag := range f.flags { + name := flag.Name + + deprecatedUsage := "[DEPRECATED]" + deprecatedName := flag.DeprecatedName + if name != "" { + deprecatedUsage = fmt.Sprintf("[DEPRECATED] use --%s instead", name) + } + + value, ok := valueAtKeyPath(f.bindings, flag.KeyPath) + if !ok { + return GinkgoFlagSet{}, fmt.Errorf("could not load KeyPath: %s", flag.KeyPath) + } + + iface, addr := value.Interface(), value.Addr().Interface() + + switch value.Type() { + case reflect.TypeOf(string("")): + if name != "" { + f.flagSet.StringVar(addr.(*string), name, iface.(string), flag.Usage) + } + if deprecatedName != "" { + f.flagSet.StringVar(addr.(*string), deprecatedName, iface.(string), deprecatedUsage) + } + case reflect.TypeOf(int64(0)): + if name != "" { + f.flagSet.Int64Var(addr.(*int64), name, iface.(int64), flag.Usage) + } + if deprecatedName != "" { + f.flagSet.Int64Var(addr.(*int64), deprecatedName, iface.(int64), deprecatedUsage) + } + case reflect.TypeOf(float64(0)): + if name != "" { + f.flagSet.Float64Var(addr.(*float64), name, iface.(float64), flag.Usage) + } + if deprecatedName != "" { + f.flagSet.Float64Var(addr.(*float64), deprecatedName, iface.(float64), deprecatedUsage) + } + case reflect.TypeOf(int(0)): + if name != "" { + f.flagSet.IntVar(addr.(*int), name, iface.(int), flag.Usage) + } + if deprecatedName != "" { + f.flagSet.IntVar(addr.(*int), deprecatedName, iface.(int), deprecatedUsage) + } + case reflect.TypeOf(bool(true)): + if name != "" { + f.flagSet.BoolVar(addr.(*bool), name, iface.(bool), flag.Usage) + } + if deprecatedName != "" { + f.flagSet.BoolVar(addr.(*bool), deprecatedName, iface.(bool), deprecatedUsage) + } + case reflect.TypeOf(time.Duration(0)): + if name != "" { + f.flagSet.DurationVar(addr.(*time.Duration), name, iface.(time.Duration), flag.Usage) + } + if deprecatedName != "" { + f.flagSet.DurationVar(addr.(*time.Duration), deprecatedName, iface.(time.Duration), deprecatedUsage) + } + + case reflect.TypeOf([]string{}): + if name != "" { + f.flagSet.Var(stringSliceVar{value}, name, flag.Usage) + } + if deprecatedName != "" { + f.flagSet.Var(stringSliceVar{value}, deprecatedName, deprecatedUsage) + } + default: + return GinkgoFlagSet{}, fmt.Errorf("unsupported type %T", iface) + } + } + + return f, nil +} + +func (f GinkgoFlagSet) IsZero() bool { + return f.flagSet == nil +} + +func (f GinkgoFlagSet) WasSet(name string) bool { + found := false + f.flagSet.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + + return found +} + +func (f GinkgoFlagSet) Parse(args []string) ([]string, error) { + if f.IsZero() { + return args, nil + } + err := f.flagSet.Parse(args) + if err != nil { + return []string{}, err + } + return f.flagSet.Args(), nil +} + +func (f GinkgoFlagSet) ValidateDeprecations(deprecationTracker *types.DeprecationTracker) { + if f.IsZero() { + return + } + f.flagSet.Visit(func(flag *flag.Flag) { + for _, ginkgoFlag := range f.flags { + if ginkgoFlag.DeprecatedName != "" && strings.HasSuffix(flag.Name, ginkgoFlag.DeprecatedName) { + message := fmt.Sprintf("--%s is deprecated", ginkgoFlag.DeprecatedName) + if ginkgoFlag.Name != "" { + message = fmt.Sprintf("--%s is deprecated, use --%s instead", ginkgoFlag.DeprecatedName, ginkgoFlag.Name) + } + + deprecationTracker.TrackDeprecation(types.Deprecation{ + Message: message, + DocLink: ginkgoFlag.DeprecatedDocLink, + }) + } + } + }) +} + +func (f GinkgoFlagSet) Usage() string { + if f.IsZero() { + return "" + } + groupedFlags := map[GinkgoFlagSection]GinkgoFlags{} + ungroupedFlags := GinkgoFlags{} + managedFlags := map[string]bool{} + extraGoFlags := []*flag.Flag{} + + for _, flag := range f.flags { + managedFlags[flag.Name] = true + managedFlags[flag.DeprecatedName] = true + + if flag.Name == "" { + continue + } + + section, ok := f.sections.Lookup(flag.SectionKey) + if ok { + groupedFlags[section] = append(groupedFlags[section], flag) + } else { + ungroupedFlags = append(ungroupedFlags, flag) + } + } + + f.flagSet.VisitAll(func(flag *flag.Flag) { + if !managedFlags[flag.Name] { + extraGoFlags = append(extraGoFlags, flag) + } + }) + + out := "" + for _, section := range f.sections { + flags := groupedFlags[section] + if len(flags) == 0 { + continue + } + out += f.usageForSection(section) + if section.Succinct { + succinctFlags := []string{} + for _, flag := range flags { + if flag.Name != "" { + succinctFlags = append(succinctFlags, fmt.Sprintf("--%s", flag.Name)) + } + } + out += formatter.Fiw(1, formatter.COLS, section.Style+strings.Join(succinctFlags, ", ")+"{{/}}\n") + } else { + for _, flag := range flags { + out += f.usageForFlag(flag, section.Style) + } + } + out += "\n" + } + if len(ungroupedFlags) > 0 { + for _, flag := range ungroupedFlags { + out += f.usageForFlag(flag, "") + } + out += "\n" + } + if len(extraGoFlags) > 0 { + out += f.usageForSection(f.extraGoFlagsSection) + for _, goFlag := range extraGoFlags { + out += f.usageForGoFlag(goFlag) + } + } + + return out +} + +func (f GinkgoFlagSet) substituteUsage() { + fmt.Fprintln(f.flagSet.Output(), f.Usage()) +} + +func valueAtKeyPath(root interface{}, keyPath string) (reflect.Value, bool) { + if len(keyPath) == 0 { + return reflect.Value{}, false + } + + val := reflect.ValueOf(root) + components := strings.Split(keyPath, ".") + for _, component := range components { + val = reflect.Indirect(val) + switch val.Kind() { + case reflect.Map: + val = val.MapIndex(reflect.ValueOf(component)) + if val.Kind() == reflect.Interface { + val = reflect.ValueOf(val.Interface()) + } + case reflect.Struct: + val = val.FieldByName(component) + default: + return reflect.Value{}, false + } + if (val == reflect.Value{}) { + return reflect.Value{}, false + } + } + + return val, true +} + +func (f GinkgoFlagSet) usageForSection(section GinkgoFlagSection) string { + out := formatter.F(section.Style + "{{bold}}{{underline}}" + section.Heading + "{{/}}\n") + if section.Description != "" { + out += formatter.Fiw(0, formatter.COLS, section.Description+"\n") + } + return out +} + +func (f GinkgoFlagSet) usageForFlag(flag GinkgoFlag, style string) string { + argument := flag.UsageArgument + defValue := flag.UsageDefaultValue + if argument == "" { + value, _ := valueAtKeyPath(f.bindings, flag.KeyPath) + switch value.Type() { + case reflect.TypeOf(string("")): + argument = "string" + case reflect.TypeOf(int64(0)), reflect.TypeOf(int(0)): + argument = "int" + case reflect.TypeOf(time.Duration(0)): + argument = "duration" + case reflect.TypeOf(float64(0)): + argument = "float" + case reflect.TypeOf([]string{}): + argument = "string" + } + } + if argument != "" { + argument = "[" + argument + "] " + } + if defValue != "" { + defValue = fmt.Sprintf("(default: %s)", defValue) + } + hyphens := "--" + if len(flag.Name) == 1 { + hyphens = "-" + } + + out := formatter.Fi(1, style+"%s%s{{/}} %s{{gray}}%s{{/}}\n", hyphens, flag.Name, argument, defValue) + out += formatter.Fiw(2, formatter.COLS, "{{light-gray}}%s{{/}}\n", flag.Usage) + return out +} + +func (f GinkgoFlagSet) usageForGoFlag(goFlag *flag.Flag) string { + //Taken directly from the flag package + out := fmt.Sprintf(" -%s", goFlag.Name) + name, usage := flag.UnquoteUsage(goFlag) + if len(name) > 0 { + out += " " + name + } + if len(out) <= 4 { + out += "\t" + } else { + out += "\n \t" + } + out += strings.ReplaceAll(usage, "\n", "\n \t") + out += "\n" + return out +} + +type stringSliceVar struct { + slice reflect.Value +} + +func (ssv stringSliceVar) String() string { return "" } +func (ssv stringSliceVar) Set(s string) error { + ssv.slice.Set(reflect.AppendSlice(ssv.slice, reflect.ValueOf([]string{s}))) + return nil +} + +//given a set of GinkgoFlags and bindings, generate flag arguments suitable to be passed to an application with that set of flags configured. +func GenerateFlagArgs(flags GinkgoFlags, bindings interface{}) ([]string, error) { + result := []string{} + for _, flag := range flags { + name := flag.ExportAs + if name == "" { + name = flag.Name + } + if name == "" { + continue + } + + value, ok := valueAtKeyPath(bindings, flag.KeyPath) + if !ok { + return []string{}, fmt.Errorf("could not load KeyPath: %s", flag.KeyPath) + } + + iface := value.Interface() + switch value.Type() { + case reflect.TypeOf(string("")): + if iface.(string) != "" { + result = append(result, fmt.Sprintf("--%s=%s", name, iface)) + } + case reflect.TypeOf(int64(0)): + if iface.(int64) != 0 { + result = append(result, fmt.Sprintf("--%s=%d", name, iface)) + } + case reflect.TypeOf(float64(0)): + if iface.(float64) != 0 { + result = append(result, fmt.Sprintf("--%s=%f", name, iface)) + } + case reflect.TypeOf(int(0)): + if iface.(int) != 0 { + result = append(result, fmt.Sprintf("--%s=%d", name, iface)) + } + case reflect.TypeOf(bool(true)): + if iface.(bool) { + result = append(result, fmt.Sprintf("--%s", name)) + } + case reflect.TypeOf(time.Duration(0)): + if iface.(time.Duration) != time.Duration(0) { + result = append(result, fmt.Sprintf("--%s=%s", name, iface)) + } + + case reflect.TypeOf([]string{}): + strings := iface.([]string) + for _, s := range strings { + result = append(result, fmt.Sprintf("--%s=%s", name, s)) + } + default: + return []string{}, fmt.Errorf("unsupported type %T", iface) + } + } + + return result, nil +} diff --git a/config/flags_test.go b/config/flags_test.go new file mode 100644 index 0000000000..b768fc5804 --- /dev/null +++ b/config/flags_test.go @@ -0,0 +1,460 @@ +package config_test + +import ( + "flag" + "strings" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("Flags", func() { + BeforeEach(func() { + format.CharactersAroundMismatchToInclude = 1000 + formatter.SingletonFormatter.ColorMode = formatter.ColorModePassthrough + }) + + Describe("GinkgoFlags", func() { + Describe("CopyAppend", func() { + It("concatenates the flags together, making a copy as it does so", func() { + A := config.GinkgoFlags{{Name: "A"}, {Name: "B"}, {Name: "C"}} + B := config.GinkgoFlags{{Name: "1"}, {Name: "2"}, {Name: "3"}} + + Ω(A.CopyAppend(B...)).Should(Equal(config.GinkgoFlags{{Name: "A"}, {Name: "B"}, {Name: "C"}, {Name: "1"}, {Name: "2"}, {Name: "3"}})) + Ω(A).Should(HaveLen(3)) + Ω(B).Should(HaveLen(3)) + }) + }) + + Describe("WithPrefix", func() { + It("attaches the passed in prefi to the Name, DeprecatedName and ExportAs fields", func() { + flags := config.GinkgoFlags{ + {Name: "A"}, + {DeprecatedName: "B"}, + {ExportAs: "C"}, + } + prefixed := flags.WithPrefix("hello") + Ω(prefixed).Should(Equal(config.GinkgoFlags{ + {Name: "hello.A"}, + {DeprecatedName: "hello.B"}, + {ExportAs: "hello.C"}, + })) + }) + }) + }) + + Describe("GinkgoFlagSections", func() { + Describe("Lookup", func() { + var sections config.GinkgoFlagSections + BeforeEach(func() { + sections = config.GinkgoFlagSections{ + {Key: "A", Heading: "Aft"}, + {Key: "B", Heading: "Starboard"}, + } + }) + + It("looks up the flag section with the passed in key", func() { + section, found := sections.Lookup("A") + Ω(found).Should(BeTrue()) + Ω(section).Should(Equal(sections[0])) + }) + + It("returns an empty flag section when the key is not found", func() { + section, found := sections.Lookup("C") + Ω(found).Should(BeFalse()) + Ω(section).Should(BeZero()) + }) + }) + }) + + Describe("The zero GinkgoFlagSet", func() { + It("returns true for IsZero", func() { + Ω(config.GinkgoFlagSet{}.IsZero()).Should(BeTrue()) + }) + + It("returns the passed in args when asked to parse", func() { + args := []string{"-a=1", "-b=2", "-c=3"} + Ω(config.GinkgoFlagSet{}.Parse(args)).Should(Equal(args)) + }) + + It("does not validate any deprecations", func() { + deprecationTracker := types.NewDeprecationTracker() + config.GinkgoFlagSet{}.ValidateDeprecations(deprecationTracker) + Ω(deprecationTracker.DidTrackDeprecations()).Should(BeFalse()) + }) + + It("emits an empty string for usage", func() { + Ω(config.GinkgoFlagSet{}.Usage()).Should(Equal("")) + }) + }) + + Describe("GinkgoFlagSet", func() { + type StructA struct { + StringProperty string + Int64Property int64 + Float64Property float64 + } + type StructB struct { + IntProperty int + BoolProperty bool + StringSliceProperty []string + DeprecatedProperty string + } + var A StructA + var B StructB + var flags config.GinkgoFlags + var bindings map[string]interface{} + var sections config.GinkgoFlagSections + var flagSet config.GinkgoFlagSet + + BeforeEach(func() { + A = StructA{ + StringProperty: "the default string", + Int64Property: 1138, + Float64Property: 3.141, + } + B = StructB{ + IntProperty: 2009, + BoolProperty: true, + StringSliceProperty: []string{"once", "upon", "a time"}, + DeprecatedProperty: "n/a", + } + bindings = map[string]interface{}{ + "A": &A, + "B": &B, + } + sections = config.GinkgoFlagSections{ + {Key: "candy", Style: "{{red}}", Heading: "Candy Section", Description: "So sweet."}, + {Key: "dairy", Style: "{{blue}}", Heading: "Dairy Section", Description: "Abbreviated section", Succinct: true}, + } + flags = config.GinkgoFlags{ + {Name: "string-flag", SectionKey: "candy", Usage: "string-usage", UsageArgument: "name", UsageDefaultValue: "Gerald", KeyPath: "A.StringProperty", DeprecatedName: "stringFlag"}, + {Name: "int-64-flag", SectionKey: "candy", Usage: "int-64-usage", KeyPath: "A.Int64Property", DeprecatedName: "int64Flag", DeprecatedDocLink: "no-more-camel-case"}, + {Name: "float-64-flag", SectionKey: "dairy", Usage: "float-64-usage", KeyPath: "A.Float64Property"}, + {Name: "int-flag", SectionKey: "invalid", Usage: "int-usage", KeyPath: "B.IntProperty"}, + {Name: "bool-flag", SectionKey: "candy", Usage: "bool-usage", KeyPath: "B.BoolProperty"}, + {Name: "string-slice-flag", SectionKey: "dairy", Usage: "string-slice-usage", KeyPath: "B.StringSliceProperty"}, + {SectionKey: "candy", DeprecatedName: "deprecated-flag", KeyPath: "B.DeprecatedProperty"}, + } + }) + + Describe("Creation Failure Cases", func() { + Context("when passed an unsuppoted type in the map", func() { + BeforeEach(func() { + type UnsupportedStructB struct { + IntProperty int + BoolProperty bool + StringSliceProperty []string + DeprecatedProperty int32 //not supported + } + + bindings = map[string]interface{}{ + "A": &A, + "B": &UnsupportedStructB{}, + } + }) + + It("errors", func() { + flagSet, err := config.NewGinkgoFlagSet(flags, bindings, sections) + Ω(flagSet.IsZero()).Should(BeTrue()) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when the flags point to an invalid keypath in the map", func() { + BeforeEach(func() { + flags = append(flags, config.GinkgoFlag{Name: "welp-flag", Usage: "welp-usage", KeyPath: "A.WelpProperty"}) + }) + + It("errors", func() { + flagSet, err := config.NewGinkgoFlagSet(flags, bindings, sections) + Ω(flagSet.IsZero()).Should(BeTrue()) + Ω(err).Should(HaveOccurred()) + }) + }) + }) + + Describe("A stand-alone GinkgoFlagSet", func() { + BeforeEach(func() { + var err error + flagSet, err = config.NewGinkgoFlagSet(flags, bindings, sections) + Ω(flagSet.IsZero()).Should(BeFalse()) + Ω(err).ShouldNot(HaveOccurred()) + }) + + Describe("Parsing flags", func() { + It("maintains default values when no flags are parsed", func() { + args, err := flagSet.Parse([]string{}) + Ω(err).ShouldNot(HaveOccurred()) + Ω(args).Should(Equal([]string{})) + + Ω(A.StringProperty).Should(Equal("the default string")) + Ω(B.IntProperty).Should(Equal(2009)) + }) + + It("updates the bindings when flags are parsed, returning any additional arguments", func() { + args, err := flagSet.Parse([]string{ + "-string-flag", "a new string", + "-int-64-flag=1139", + "--float-64-flag", "2.71", + "-int-flag=1984", + "-bool-flag=false", + "-string-slice-flag", "there lived", + "-string-slice-flag", "three dragons", + "extra-1", + "extra-2", + }) + Ω(err).ShouldNot(HaveOccurred()) + Ω(args).Should(Equal([]string{"extra-1", "extra-2"})) + + Ω(A.StringProperty).Should(Equal("a new string")) + Ω(A.Int64Property).Should(Equal(int64(1139))) + Ω(A.Float64Property).Should(Equal(2.71)) + Ω(B.IntProperty).Should(Equal(1984)) + Ω(B.BoolProperty).Should(Equal(false)) + Ω(B.StringSliceProperty).Should(Equal([]string{"once", "upon", "a time", "there lived", "three dragons"})) + }) + + It("updates the bindings when deprecated flags are set", func() { + _, err := flagSet.Parse([]string{ + "-stringFlag", "deprecated but works", + "-int64Flag=1234", + "-deprecated-flag", "does not fail", + }) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(A.StringProperty).Should(Equal("deprecated but works")) + Ω(A.Int64Property).Should(Equal(int64(1234))) + Ω(B.DeprecatedProperty).Should(Equal("does not fail")) + }) + + It("reports accurately on flags that were set", func() { + _, err := flagSet.Parse([]string{ + "-string-flag", "a new string", + "--float-64-flag", "2.71", + }) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(flagSet.WasSet("string-flag")).Should(BeTrue()) + Ω(flagSet.WasSet("int-64-flag")).Should(BeFalse()) + Ω(flagSet.WasSet("float-64-flag")).Should(BeTrue()) + }) + }) + + Describe("Validating Deprecations", func() { + var deprecationTracker *types.DeprecationTracker + BeforeEach(func() { + deprecationTracker = types.NewDeprecationTracker() + }) + + Context("when no deprecated flags were invoked", func() { + It("doesn't track any deprecations", func() { + flagSet.Parse([]string{ + "--string-flag", "ok", + "--int-flag", "1983", + }) + flagSet.ValidateDeprecations(deprecationTracker) + + Ω(deprecationTracker.DidTrackDeprecations()).Should(BeFalse()) + }) + }) + + Context("when deprecated flags were invoked", func() { + It("tracks any detected deprecations with the passed in deprecation tracker", func() { + flagSet.Parse([]string{ + "--stringFlag", "deprecated version", + "--string-flag", "ok", + "--int64Flag", "427", + }) + flagSet.ValidateDeprecations(deprecationTracker) + + Ω(deprecationTracker.DidTrackDeprecations()).Should(BeTrue()) + report := deprecationTracker.DeprecationsReport() + Ω(report).Should(ContainSubstring("--int64Flag is deprecated, use --int-64-flag instead")) + Ω(report).Should(ContainSubstring("https://github.com/onsi/ginkgo/blob/v2/docs/MIGRATING_TO_V2.md#no-more-camel-case")) + Ω(report).Should(ContainSubstring("--stringFlag is deprecated, use --string-flag instead")) + }) + }) + }) + + Describe("Emitting Usage information", func() { + It("emits information by section", func() { + expectedUsage := []string{ + "{{red}}{{bold}}{{underline}}Candy Section{{/}}", //Candy section + "So sweet.", //with heading + " {{red}}--string-flag{{/}} [name] {{gray}}(default: Gerald){{/}}", //flag with usage argument and default value + " {{light-gray}}string-usage{{/}}", + " {{red}}--int-64-flag{{/}} [int] {{gray}}{{/}}", + " {{light-gray}}int-64-usage{{/}}", + " {{red}}--bool-flag{{/}} {{gray}}{{/}}", + " {{light-gray}}bool-usage{{/}}", + "", + "{{blue}}{{bold}}{{underline}}Dairy Section{{/}}", //Dairy section is Succinct... + "Abbreviated section", + " {{blue}}--float-64-flag, --string-slice-flag{{/}}", //so flags are just enumerated, without documentation + "", + " --int-flag{{/}} [int] {{gray}}{{/}}", + " {{light-gray}}int-usage{{/}}", + "", + "", + } + Ω(flagSet.Usage()).Should(Equal(strings.Join(expectedUsage, "\n"))) + }) + }) + }) + + Describe("A GinkgoFlagSet attached to an existing golang flagset", func() { + var goFlagSet *flag.FlagSet + + BeforeEach(func() { + var err error + goFlagSet = flag.NewFlagSet("go-set", flag.ContinueOnError) + goStringFlag := "" + goIntFlag := 0 + goFlagSet.StringVar(&goStringFlag, "go-string-flag", "bob", "sets via `go`") + goFlagSet.IntVar(&goIntFlag, "go-int-flag", 0, "an integer, please") + flagSet, err = config.NewAttachedGinkgoFlagSet(goFlagSet, flags, bindings, sections, config.GinkgoFlagSection{ + Heading: "The go flags...", + }) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("attaches its flags to go flag set, including deprecated flags", func() { + registeredFlags := map[string]*flag.Flag{} + goFlagSet.VisitAll(func(flag *flag.Flag) { + registeredFlags[flag.Name] = flag + }) + + Ω(registeredFlags["string-flag"].Usage).Should(Equal("string-usage")) + Ω(registeredFlags["int-64-flag"].Usage).Should(Equal("int-64-usage")) + Ω(registeredFlags["float-64-flag"].Usage).Should(Equal("float-64-usage")) + Ω(registeredFlags["int-flag"].Usage).Should(Equal("int-usage")) + Ω(registeredFlags["bool-flag"].Usage).Should(Equal("bool-usage")) + Ω(registeredFlags["string-slice-flag"].Usage).Should(Equal("string-slice-usage")) + Ω(registeredFlags["string-flag"].DefValue).Should(Equal("the default string")) + Ω(registeredFlags["int-64-flag"].DefValue).Should(Equal("1138")) + Ω(registeredFlags["float-64-flag"].DefValue).Should(Equal("3.141")) + Ω(registeredFlags["int-flag"].DefValue).Should(Equal("2009")) + Ω(registeredFlags["bool-flag"].DefValue).Should(Equal("true")) + Ω(registeredFlags["stringFlag"].Usage).Should(Equal("[DEPRECATED] use --string-flag instead")) + Ω(registeredFlags["int64Flag"].Usage).Should(Equal("[DEPRECATED] use --int-64-flag instead")) + Ω(registeredFlags["deprecated-flag"].Usage).Should(Equal("[DEPRECATED]")) + }) + + It("overrides the goFlagSet's usage", func() { + buf := gbytes.NewBuffer() + goFlagSet.SetOutput(buf) + goFlagSet.Parse([]string{"--oops"}) + + Ω(string(buf.Contents())).Should(Equal("flag provided but not defined: -oops\n" + flagSet.Usage() + "\n")) + }) + + It("includes the go FlagSets flags in their own section", func() { + expectedUsage := []string{ + "{{red}}{{bold}}{{underline}}Candy Section{{/}}", + "So sweet.", + " {{red}}--string-flag{{/}} [name] {{gray}}(default: Gerald){{/}}", + " {{light-gray}}string-usage{{/}}", + " {{red}}--int-64-flag{{/}} [int] {{gray}}{{/}}", + " {{light-gray}}int-64-usage{{/}}", + " {{red}}--bool-flag{{/}} {{gray}}{{/}}", + " {{light-gray}}bool-usage{{/}}", + "", + "{{blue}}{{bold}}{{underline}}Dairy Section{{/}}", + "Abbreviated section", + " {{blue}}--float-64-flag, --string-slice-flag{{/}}", + "", + " --int-flag{{/}} [int] {{gray}}{{/}}", + " {{light-gray}}int-usage{{/}}", + "", + "{{bold}}{{underline}}The go flags...{{/}}", //separate go flags section at the bottom, includes flags that are in the go FlagSet but not in the GinkgoFLagSet + " -go-int-flag int", + " an integer, please", + " -go-string-flag go", //Note the processing of `go` using flag.UnquoteUsage() + " sets via go", + "", + } + Ω(flagSet.Usage()).Should(Equal(strings.Join(expectedUsage, "\n"))) + }) + }) + }) + + Describe("GenerateFlagArgs", func() { + type StructA struct { + StringProperty string + Int64Property int64 + Float64Property float64 + UnsupportedInt32 int32 + } + type StructB struct { + IntProperty int + BoolProperty bool + StringSliceProperty []string + DeprecatedProperty string + } + var A StructA + var B StructB + var flags config.GinkgoFlags + var bindings map[string]interface{} + + BeforeEach(func() { + A = StructA{ + StringProperty: "the default string", + Int64Property: 1138, + Float64Property: 3.141, + } + B = StructB{ + IntProperty: 2009, + BoolProperty: true, + StringSliceProperty: []string{"once", "upon", "a time"}, + DeprecatedProperty: "n/a", + } + bindings = map[string]interface{}{ + "A": &A, + "B": &B, + } + flags = config.GinkgoFlags{ + {Name: "string-flag", KeyPath: "A.StringProperty", DeprecatedName: "stringFlag"}, + {Name: "int-64-flag", KeyPath: "A.Int64Property"}, + {Name: "float-64-flag", KeyPath: "A.Float64Property"}, + {Name: "int-flag", KeyPath: "B.IntProperty", ExportAs: "alias-int-flag"}, + {Name: "bool-flag", KeyPath: "B.BoolProperty", ExportAs: "alias-bool-flag"}, + {Name: "string-slice-flag", KeyPath: "B.StringSliceProperty"}, + {DeprecatedName: "deprecated-flag", KeyPath: "B.DeprecatedProperty"}, + } + }) + + It("generates an array of flag arguments that, if parsed, reproduce the values in the passed-in bindings", func() { + args, err := config.GenerateFlagArgs(flags, bindings) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(args).Should(Equal([]string{ + "--string-flag=the default string", + "--int-64-flag=1138", + "--float-64-flag=3.141000", + "--alias-int-flag=2009", + "--alias-bool-flag", + "--string-slice-flag=once", + "--string-slice-flag=upon", + "--string-slice-flag=a time", + })) + }) + + It("errors if there is a keypath issue", func() { + flags[0] = config.GinkgoFlag{Name: "unsupported-type", KeyPath: "A.UnsupportedInt32"} + args, err := config.GenerateFlagArgs(flags, bindings) + Ω(err).Should(MatchError("unsupported type int32")) + Ω(args).Should(BeEmpty()) + + flags[0] = config.GinkgoFlag{Name: "bad-keypath", KeyPath: "A.StringProoperty"} + args, err = config.GenerateFlagArgs(flags, bindings) + Ω(err).Should(MatchError("could not load KeyPath: A.StringProoperty")) + Ω(args).Should(BeEmpty()) + }) + }) +}) diff --git a/docs/MIGRATING_TO_V2.md b/docs/MIGRATING_TO_V2.md new file mode 100644 index 0000000000..8192a1d3ca --- /dev/null +++ b/docs/MIGRATING_TO_V2.md @@ -0,0 +1,168 @@ +--- +layout: default +title: Migrating to Ginkgo V2 +--- + +[Ginkgo 2.0](#711) is a major release that adds substantial new functionality and removes/moves existing functionality. + +This document serves as a changelog and migration guide for users migrating from Ginkgo 1.x to 2.0. The intent is that the migration will take minimal effort. + +# Additions and Improvements + +## Major Additions and Improvements + +### Interrupt Behavior +Interrupt behavior is substantially improved, sending an interrupt signal will now: + - immediately cause the current test to unwind. Ginkgo will run any `AfterEach` blocks, then immediately skip all remaining tests, then run the `AfterSuite` block. + - emit information about which node Ginkgo was running when the interrupt signal was recevied. + - emit as much information as possible about the interrupted test (e.g. `GinkgoWriter` contents, `stdout` and `stderr` context). + +Previously, sending a second interrupt signal would cause Ginkgo to exit immediately. With the improved interrupt behavior this is no longer necessary. + +## Minor Additions and Improvements +- `BeforeSuite` and `AfterSuite` no longer run if all tests in a suite are skipped. +- Ginkgo can now catch several common user gotchas and emit a helpful error. +- Error output is clearer and consistently links to relevant sections in the documentation. +- Test randomization is now more stable as tests are now sorted deterministcally on file_name:line_number first (previously they were sorted on test text which could not guarantee a stable sort). + +# Changes + +## Major Changes +These are major changes that will need user intervention to migrate succesfully. + +### Removed: Async Testing +As described in the [Ginkgo 2.0 Proposal](https://docs.google.com/document/d/1h28ZknXRsTLPNNiOjdHIO-F2toCzq4xoZDXbfYaBdoQ/edit#heading=h.mzgqmkg24xoo) the Ginkgo 1.x implementation of asynchronous testing using a `Done` channel was a confusing source of test-pollution. It is removed in Ginkgo 2.0. + +In Ginkgo 2.0 tests of the form: + +``` +It("...", func(done Done) { + // user test code to run asynchronously + close(done) //signifies the test is done +}, timeout) +``` + +will emit a deprecation warning and will run **synchronously**. This means the `timeout` will not be enforced and the status of the `Done` channel will be ignored - a test that hangs will hang indefinitely. + +#### Migration Strategy: +We recommend users make targeted use of Gomega's [Asynchronous Assertions](https://onsi.github.io/gomega/#making-asynchronous-assertions) to better test asynchronous behavior. + +As a first migration pass that produces **equivalent behavior** users can replace asynchronous tests with: + +``` +It("...", func() { + done := make(chan interface{}) + go func() { + // user test code to run asynchronously + close(done) //signifies the code is done + }() + Eventually(done, timeout).Should(BeClosed()) +}) +``` + +### Removed: Measure +As described in the [Ginkgo 2.0 Proposal](https://docs.google.com/document/d/1h28ZknXRsTLPNNiOjdHIO-F2toCzq4xoZDXbfYaBdoQ/edit#heading=h.2ezhpn4gmcgs) the Ginkgo 1.x implementation of benchmarking using `Measure` nodes was a source of tightly-coupled complexity. It is removed in Ginkgo 2.0. + +In Ginkgo 2.0 tests of the form: +``` +Measure(..., func(b Benchmarker) { + // user benchmark code +}) +``` + +will emit a deprecation warning and **will no longer run**. + +#### Migration Strategy: +A new Gomega benchmarking subpackage is being developed to replace Ginkgo's benchmarking capabilities with a more mature, decoupled, and useful implementation. This section will be updated once the Gomega package is ready. + +### Changed: Reporter Interface +Objects satisfying the `Reporter` interface can be passed to Ginkgo to report information about tests. The `Reporter` interface has changed in Ginkgo 2.0. + +The Ginkgo 1.x `Reporter` interface: +``` +type V1Reporter interface { + SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) + BeforeSuiteDidRun(setupSummary *types.SetupSummary) + SpecWillRun(specSummary *types.SpecSummary) + SpecDidComplete(specSummary *types.SpecSummary) + AfterSuiteDidRun(setupSummary *types.SetupSummary) + SpecSuiteDidEnd(summary *types.SuiteSummary) +} +``` + +is now simpler in Ginkgo 2.0: +``` +type Reporter interface { + SpecSuiteWillBegin(config config.GinkgoConfigType, summary types.SuiteSummary) + WillRun(specSummary types.Summary) + DidRun(specSummary types.Summary) + SpecSuiteDidEnd(summary types.SuiteSummary) +} +``` + +In addition, there have been changes to the data types passed into these methods. The original types are preserved in [http://github.com/onsi/ginkgo/blob/v2/types/deprecation_support.go] and aliased to their original names. + +#### Migration Strategy: +Most users will not need to worry about this change. For users with custom reporters you will see a compilation failure when you try to pass your `V1Reporter` to Ginkgo. This can be fixed immediately by wrapping your custom reporter with `reporters.ReporterFromV1Reporter()`: + +``` +import "github.com/onsi/ginkgo/reporters" + +func TestXXX(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, "XXX", []Reporter{ + reporters.ReporterFromV1Reporter(customV1Reporter) + }) +} +``` + +Your test suite will now compile and Ginkgo will send appropriately formatted data to your reporter. However, Ginkgo will emit a deprecation warning that you are using a V1 reporter. To silence the warning you will need to rewrite your reporter to match the new interface. You can look at the implementation of `reporters.ReporterFromV1Reporter` and `reporters.DefaultReporter` for guidance, open an issue for help, or reach out for help on the Ginkgo slack channel. + +Alternatively, you can avoid using a custom reporter and instead ask Ginkgo to emit a JSON formatted report (TODO: update with link once implemented) that you can then post-process with your tooling. + +### Changed: Profiling Support +Ginkgo V1 was incorrectly handling Go test's various profiling flags (e.g. -cpuprofile, -memprofile). This has been fixed in V2. In fact, V2 can capture profiles for multiple packages (e.g. ginkgo -r -cpuprofile=profile.out will work). + +When generating profiles for `-cpuprofile=FILE`, `-blockprofile=FILE`, `-memprofile=FILE`, `-mutexprofile=FILE`, and `-execution-trace=FILE` (Ginkgo's alias for `go test -test.trace`) the following rules apply: + +- If `-output-dir` is not set: each profile generates a file named `$FILE` in the directory of each package under test. +- If `-output-dir` is set: each profile generates a file in the specified `-output-dir` directory. named `PACKAGE_NAME_$FILE` + +When generating cover profiles using `-cover` and `-coverprofile=FILE`, the following rules apply: + +- By default, a single cover profile is generated at $FILE (or `coverprofile.out` if `-cover-profile` is not set but `-cover` is set). This includes the merged results of all the cover profiels reported by each suite. +- If `-output-dir` is set: the $FILE is placed in the specified `-output-dir` directory. +- If `-keep-separate-coverprofiles` is set, the individual coverprofiles generated by each package are not merged and, instead, a file named $FILE will appear in each package directory. If `-output-dir` is set these files are copied into the `-output-dir` directory and namespaced with `PACKAGE_NAME_$FILE`. + + +### Changed: Command Line Flags +All camel case flags (e.g. `-randomizeAllSpecs`) are replaced with kebab case flags (e.g. `-randomize-all-specs`) in Ginkgo 2.0. The camel case versions continue to work but emit a deprecation warning. + +#### Migration Strategy: +Users should update any scripts they have that invoke the `ginkgo` cli from camel case to kebab case (:camel: :arrow_right: :oden:). + +### Removed: `-stream` +`-stream` was originally introduce in Ginkgo 1.x to force parallel test processes to emit output simultaneously in order to help debug hanging test issues. With improvements to Ginkgo's interrupt handling and parallel test reporting this behavior is no longer necessary and has been removed. + +### Removed: `-notify` +`-notify` instructed Ginkgo to emit desktop notifications on linux and MacOS. This feature was rarely used and has been removed. + +### Removed: `-noisyPendings` and `-noisySkippings` +Both these flags tweaked the reporter's behavior for pending and skipped tests. A similar, and more intuitive, outcome is possible using `--succinct` and `-v`. + +#### Migration Strategy: +Users should remove -stream from any scripts they have that invoke the `ginkgo` cli. + +### Removed: `ginkgo convert` +The `ginkgo convert` subcommand in V1 could convert an existing set of Go tests into a Ginkgo test suite, wrapping each `TestX` function in an `It`. This subcommand added complexity to the codebase and was infrequently used. It has been removed. Users who want to convert tests suites over to Ginkgo will need to do so by hand. + +## Minor Changes +These are minor changes that will be transparent for most users. + +- `"top level"` is no longer the first element in `types.Summary.NodeTexts`. This will only affect users who write custom reporters. + +- The output format of Ginkgo's Default Reporter has changed in numerous subtle ways to improve readability and the user experience. Users who were scraping Ginkgo output programatically may need to change their scripts or use the new JSON formatted report option (TODO: update with link once JSON reporting is implemented). + +- Removed `ginkgo blur` alias. Use `ginkgo unfocus` instead. + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..db91b65f81 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,2169 @@ +--- +layout: default +title: Ginkgo +--- +[Ginkgo](https://github.com/onsi/ginkgo) is a Go testing framework built to help you efficiently write expressive and comprehensive tests using [Behavior-Driven Development](https://en.wikipedia.org/wiki/Behavior-driven_development) ("BDD") style. It is best paired with the [Gomega](https://github.com/onsi/gomega) matcher library but is designed to be matcher-agnostic. + +These docs are written assuming you'll be using Gomega with Ginkgo. They also assume you know your way around Go and have a good mental model for how Go organizes packages under `$GOPATH`. + +--- + +## Support Policy + +Ginkgo provides support for versions of Go that are noted by the [Go release policy](https://golang.org/doc/devel/release.html#policy) i.e. N and N-1 major versions. + +--- + +## Getting Ginkgo + +Just `go get` it: + + $ go get github.com/onsi/ginkgo/ginkgo + $ go get github.com/onsi/gomega/... + +This fetches ginkgo and installs the `ginkgo` executable under `$GOPATH/bin` -- you'll want that on your `$PATH`. + +**Ginkgo is tested against Go v1.6 and newer** +To install Go, follow the [installation instructions](https://golang.org/doc/install) + +The above commands also install the entire gomega library. If you want to fetch only the packages needed by your tests, import the packages you need and use `go get -t`. + +For example, import the gomega package in your test code: + + import "github.com/onsi/gomega" + +Use `go get -t` to retrieve the packages referenced in your test code: + + $ cd /path/to/my/app + $ go get -t ./... + +--- + +## Getting Started: Writing Your First Test +Ginkgo hooks into Go's existing `testing` infrastructure. This allows you to run a Ginkgo suite using `go test`. + +> This also means that Ginkgo tests can live alongside traditional Go `testing` tests. Both `go test` and `ginkgo` will run all the tests in your suite. + +### Bootstrapping a Suite +To write Ginkgo tests for a package you must first bootstrap a Ginkgo test suite. Say you have a package named `books`: + + $ cd path/to/books + $ ginkgo bootstrap + +This will generate a file named `books_suite_test.go` containing: + +```go +package books_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "testing" +) + +func TestBooks(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Books Suite") +} +``` + +Let's break this down: + +- Go allows us to specify the `books_test` package alongside the `books` package. Using `books_test` instead of `books` allows us to respect the encapsulation of the `books` package: your tests will need to import `books` and access it from the outside, like any other package. This is preferred to reaching into the package and testing its internals and leads to more behavioral tests. You can, of course, opt out of this -- just change `package books_test` to `package books` +- We import the `ginkgo` and `gomega` packages into the test's top-level namespace by performing a dot-import. If you'd rather not do this, check out the [Avoiding Dot Imports](#avoiding-dot-imports) section below. +- `TestBooks` is a `testing` test. The Go test runner will run this function when you run `go test` or `ginkgo`. +- `RegisterFailHandler(Fail)`: A Ginkgo test signals failure by calling Ginkgo's `Fail(description string)` function. We pass this function to Gomega using `RegisterFailHandler`. This is the sole connection point between Ginkgo and Gomega. +- `RunSpecs(t *testing.T, suiteDescription string)` tells Ginkgo to start the test suite. Ginkgo will automatically fail the `testing.T` if any of your specs fail. + +At this point you can run your suite: + + $ ginkgo #or go test + + === RUN TestBootstrap + + Running Suite: Books Suite + ========================== + Random Seed: 1378936983 + + Will run 0 of 0 specs + + + Ran 0 of 0 Specs in 0.000 seconds + SUCCESS! -- 0 Passed | 0 Failed | 0 Pending | 0 Skipped + + --- PASS: TestBootstrap (0.00 seconds) + PASS + ok books 0.019s + +### Adding Specs to a Suite +An empty test suite is not very interesting. While you can start to add tests directly into `books_suite_test.go` you'll probably prefer to separate your tests into separate files (especially for packages with multiple files). Let's add a test file for our `book.go` model: + + $ ginkgo generate book + +This will generate a file named `book_test.go` containing: + +```go +package books_test + +import ( + "/path/to/books" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Book", func() { + +}) +``` + +Let's break this down: + +- We import the `ginkgo` and `gomega` packages into the top-level namespace. While incredibly convenient, this is not - strictly speaking - necessary. If youd like to avoid this check out the [Avoiding Dot Imports](#avoiding-dot-imports) section below. +- Similarly, we import the `books` package since we are using the special `books_test` package to isolate our tests from our code. For convenience we import the `books` package into the namespace. You can opt out of either these decisions by editing the generated test file. +- We add a *top-level* describe container using Ginkgo's `Describe(text string, body func()) bool` function. The `var _ = ...` trick allows us to evaluate the Describe at the top level without having to wrap it in a `func init() {}` + +The function in the `Describe` will contain our specs. Let's add a few now to test loading books from JSON: + +```go +var _ = Describe("Book", func() { + var ( + longBook Book + shortBook Book + ) + + BeforeEach(func() { + longBook = Book{ + Title: "Les Miserables", + Author: "Victor Hugo", + Pages: 1488, + } + + shortBook = Book{ + Title: "Fox In Socks", + Author: "Dr. Seuss", + Pages: 24, + } + }) + + Describe("Categorizing book length", func() { + Context("With more than 300 pages", func() { + It("should be a novel", func() { + Expect(longBook.CategoryByLength()).To(Equal("NOVEL")) + }) + }) + + Context("With fewer than 300 pages", func() { + It("should be a short story", func() { + Expect(shortBook.CategoryByLength()).To(Equal("SHORT STORY")) + }) + }) + }) +}) +``` + +Let's break this down: + +- Ginkgo makes extensive use of closures to allow you to build descriptive test suites. +- You should make use of `Describe` and `Context` containers to expressively organize the behavior of your code. +- You can use `BeforeEach` to set up state for your specs. You use `It` to specify a single spec. +- In order to share state between a `BeforeEach` and an `It` you use closure variables, typically defined at the top of the most relevant `Describe` or `Context` container. +- We use Gomega's `Expect` syntax to make expectations on the `CategoryByLength()` method. + +Assuming a `Book` model with this behavior, running the tests will yield: + + $ ginkgo # or go test + === RUN TestBootstrap + + Running Suite: Books Suite + ========================== + Random Seed: 1378938274 + + Will run 2 of 2 specs + + •• + Ran 2 of 2 Specs in 0.000 seconds + SUCCESS! -- 2 Passed | 0 Failed | 0 Pending | 0 Skipped + + --- PASS: TestBootstrap (0.00 seconds) + PASS + ok books 0.025s + +Success! + +### Marking Specs as Failed + +While you typically want to use a matcher library, like [Gomega](https://github.com/onsi/gomega), to make assertions in your specs, Ginkgo provides a simple, global, `Fail` function that allows you to mark a spec as failed. Just call: + +```go +Fail("Failure reason") +``` + +and Ginkgo will take care of the rest. + +`Fail` (and therefore Gomega, since it uses fail) will record a failure for the current space *and* panic. This allows Ginkgo to stop the current spec in its tracks - no subsequent assertions (or any code for that matter) will be called. Ordinarily Ginkgo will rescue this panic itself and move on to the next test. + +However, if your test launches a *goroutine* that calls `Fail` (or, equivalently, invokes a failing Gomega assertion), there's no way for Ginkgo to rescue the panic that `Fail` invokes. This will cause the test suite to panic and no subsequent tests will run. To get around this you must rescue the panic using `GinkgoRecover`. Here's an example: + +```go +It("panics in a goroutine", func(done Done) { + go func() { + defer GinkgoRecover() + + Ω(doSomething()).Should(BeTrue()) + + close(done) + }() +}) +``` + +Now, if `doSomething()` returns false, Gomega will call `Fail` which will panic but the `defer`red `GinkgoRecover()` will recover said panic and prevent the test suite from exploding. + +More details about `Fail` and about using matcher libraries other than Gomega can be found in the [Using Other Matcher Libraries](#using-other-matcher-libraries) section. + +### Logging Output + +Ginkgo provides a globally available `io.Writer` called `GinkgoWriter` that you can write to. `GinkgoWriter` aggregates input while a test is running and only dumps it to stdout if the test fails. When running in verbose mode (`ginkgo -v` or `go test -ginkgo.v`) `GinkgoWriter` always immediately redirects its input to stdout. + +When a Ginkgo test suite is interrupted (via `^C`) Ginkgo will emit any content written to the `GinkgoWriter`. This makes it easier to debug stuck tests. This is particularly useful when paired with `--progress` which instruct Ginkgo to emit notifications to the `GinkgoWriter` as it runs through your `BeforeEach`es, `It`s, `AfterEach`es, etc... + +### IDE Support + +Ginkgo works best from the command-line, and [`ginkgo watch`](#watching-for-changes) makes it easy to rerun tests on the command line whenever changes are detected. + +There are a set of [completions](https://github.com/onsi/ginkgo-sublime-completions) available for [Sublime Text](https://www.sublimetext.com/) (just use [Package Control](https://sublime.wbond.net/) to install `Ginkgo Completions`) and for [VSCode](https://code.visualstudio.com/) (use the extensions installer and install vscode-ginkgo). + +IDE authors can set the `GINKGO_EDITOR_INTEGRATION` environment variable to any non-empty value to enable coverage to be displayed for focused specs. By default, Ginkgo will fail with a non-zero exit code if specs are focused to ensure they do not pass in CI. + +--- + +## Structuring Your Specs + +Ginkgo makes it easy to write expressive specs that describe the behavior of your code in an organized manner. You use `Describe` and `Context` containers to organize your `It` specs and you use `BeforeEach` and `AfterEach` to build up and tear down common set up amongst your tests. + +### Individual Specs: `It` +You can add a single spec by placing an `It` block within a `Describe` or `Context` container block: + +```go +var _ = Describe("Book", func() { + It("can be loaded from JSON", func() { + book := NewBookFromJSON(`{ + "title":"Les Miserables", + "author":"Victor Hugo", + "pages":1488 + }`) + + Expect(book.Title).To(Equal("Les Miserables")) + Expect(book.Author).To(Equal("Victor Hugo")) + Expect(book.Pages).To(Equal(1488)) + }) +}) +``` + +> `It`s may also be placed at the top-level though this is uncommon. + +#### The `Specify` Alias + +In order to ensure that your specs read naturally, the `Specify`, `PSpecify`, `XSpecify`, and `FSpecify` blocks are available as aliases to use in situations where the corresponding `It` alternatives do not seem to read as natural language. `Specify` blocks behave identically to `It` blocks and can be used wherever `It` blocks (and `PIt`, `XIt`, and `FIt` blocks) are used. + +An example of a good substitution of `Specify` for `It` would be the following: + +```go +Describe("The foobar service", func() { + Context("when calling Foo()", func() { + Context("when no ID is provided", func() { + Specify("an ErrNoID error is returned", func() { + }) + }) + }) +}) +``` + +### Extracting Common Setup: `BeforeEach` +You can remove duplication and share common setup across tests using `BeforeEach` blocks: + +```go +var _ = Describe("Book", func() { + var book Book + + BeforeEach(func() { + book = NewBookFromJSON(`{ + "title":"Les Miserables", + "author":"Victor Hugo", + "pages":1488 + }`) + }) + + It("can be loaded from JSON", func() { + Expect(book.Title).To(Equal("Les Miserables")) + Expect(book.Author).To(Equal("Victor Hugo")) + Expect(book.Pages).To(Equal(1488)) + }) + + It("can extract the author's last name", func() { + Expect(book.AuthorLastName()).To(Equal("Hugo")) + }) +}) +``` + +The `BeforeEach` is run before each spec thereby ensuring that each spec has a pristine copy of the state. Common state is shared using closure variables (`var book Book` in this case). You can also perform clean up in `AfterEach` blocks. + +It is also common to place assertions within `BeforeEach` and `AfterEach` blocks. These assertions can, for example, assert that no errors occured while preparing the state for the spec. + +### Organizing Specs With Containers: `Describe` and `Context` + +Ginkgo allows you to expressively organize the specs in your suite using `Describe` and `Context` containers: + +```go +var _ = Describe("Book", func() { + var ( + book Book + err error + ) + + BeforeEach(func() { + book, err = NewBookFromJSON(`{ + "title":"Les Miserables", + "author":"Victor Hugo", + "pages":1488 + }`) + }) + + Describe("loading from JSON", func() { + Context("when the JSON parses succesfully", func() { + It("should populate the fields correctly", func() { + Expect(book.Title).To(Equal("Les Miserables")) + Expect(book.Author).To(Equal("Victor Hugo")) + Expect(book.Pages).To(Equal(1488)) + }) + + It("should not error", func() { + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when the JSON fails to parse", func() { + BeforeEach(func() { + book, err = NewBookFromJSON(`{ + "title":"Les Miserables", + "author":"Victor Hugo", + "pages":1488oops + }`) + }) + + It("should return the zero-value for the book", func() { + Expect(book).To(BeZero()) + }) + + It("should error", func() { + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Describe("Extracting the author's last name", func() { + It("should correctly identify and return the last name", func() { + Expect(book.AuthorLastName()).To(Equal("Hugo")) + }) + }) +}) +``` + +You use `Describe` blocks to describe the individual behaviors of your code and `Context` blocks to exercise those behaviors under different circumstances. In this example we `Describe` loading a book from JSON and specify two `Context`s: when the JSON parses succesfully and when the JSON fails to parse. Semantic differences aside, the two container types have identical behavior. + +When nesting `Describe`/`Context` blocks the `BeforeEach` blocks for all the container nodes surrounding an `It` are run from outermost to innermost when the `It` is executed. The same is true for `AfterEach` block though they run from innermost to outermost. Note: the `BeforeEach` and `AfterEach` blocks run for **each** `It` block. This ensures a pristine state for each spec. + +> In general, the only code within a container block should be an `It` block or a `BeforeEach`/`JustBeforeEach`/`JustAfterEach`/`AfterEach` block, or closure variable declarations. It is generally a mistake to make an assertion in a container block. + +> It is also a mistake to *initialize* a closure variable in a container block. If one of your `It`s mutates that variable, subsequent `It`s will receive the mutated value. This is a case of test pollution and can be hard to track down. **Always initialize your variables in `BeforeEach` blocks.** + +If you'd like to get information, at runtime about the current test, you can use `CurrentGinkgoTestDescription()` from within any `It` or `BeforeEach`/`JustBeforeEach`/`JustAfterEach`/`AfterEach` block. The `CurrentGinkgoTestDescription` returned by this call has a variety of information about the currently running test including the filename, line number, text in the `It` block, and text in the surrounding container blocks. + +### Separating Creation and Configuration: `JustBeforeEach` + +The above example illustrates a common antipattern in BDD-style testing. Our top level `BeforeEach` creates a new book using valid JSON, but a lower level `Context` exercises the case where a book is created with *invalid* JSON. This causes us to recreate and override the original book. Thankfully, with Ginkgo's `JustBeforeEach` blocks, this code duplication is unnecessary. + +`JustBeforeEach` blocks are guaranteed to be run *after* all the `BeforeEach` blocks have run and *just before* the `It` block has run. We can use this fact to clean up the Book specs: + +```go +var _ = Describe("Book", func() { + var ( + book Book + err error + json string + ) + + BeforeEach(func() { + json = `{ + "title":"Les Miserables", + "author":"Victor Hugo", + "pages":1488 + }` + }) + + JustBeforeEach(func() { + book, err = NewBookFromJSON(json) + }) + + Describe("loading from JSON", func() { + Context("when the JSON parses succesfully", func() { + It("should populate the fields correctly", func() { + Expect(book.Title).To(Equal("Les Miserables")) + Expect(book.Author).To(Equal("Victor Hugo")) + Expect(book.Pages).To(Equal(1488)) + }) + + It("should not error", func() { + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when the JSON fails to parse", func() { + BeforeEach(func() { + json = `{ + "title":"Les Miserables", + "author":"Victor Hugo", + "pages":1488oops + }` + }) + + It("should return the zero-value for the book", func() { + Expect(book).To(BeZero()) + }) + + It("should error", func() { + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Describe("Extracting the author's last name", func() { + It("should correctly identify and return the last name", func() { + Expect(book.AuthorLastName()).To(Equal("Hugo")) + }) + }) +}) +``` + +Now the actual book creation only occurs once for every `It`, and the failing JSON context can simply assign invalid json to the `json` variable in a `BeforeEach`. + +Abstractly, `JustBeforeEach` allows you to decouple **creation** from **configuration**. Creation occurs in the `JustBeforeEach` using configuration specified and modified by a chain of `BeforeEach`s. + +> You can have multiple `JustBeforeEach`es at different levels of nesting. Ginkgo will first run all the `BeforeEach`es from the outside in, then it will run the `JustBeforeEach`es from the outside in. While powerful, this can lead to confusing test suites -- so use nested `JustBeforeEach`es judiciously. +> +> Some parting words: `JustBeforeEach` is a powerful tool that can be easily abused. Use it well. + +### Separating Diagnostics Collection and Teardown: `JustAfterEach` + +It is sometimes useful to have some code which is executed just after each `It` block, but **before** Teardown (which might destroy useful state) - for example to to perform diagnostic operations if the test failed. + +We can use this in the example above to check if the test failed and if so output the actual book: + +```go + JustAfterEach(func() { + if CurrentGinkgoTestDescription().Failed { + fmt.Printf("Collecting diags just after failed test in %s\n", CurrentGinkgoTestDescription().TestText) + fmt.Printf("Actual book was %v\n", book) + } + }) +``` + +> You can have multiple `JustAfterEach`es at different levels of nesting. Ginkgo will first run all the `JustAfterEach`es from the inside out, then it will run the `AfterEach`es from the inside out. While powerful, this can lead to confusing test suites -- so use nested `JustAfterEach`es judiciously. +> +> Like `JustBeforeEach`, `JustAfterEach` is a powerful tool that can be easily abused. Use it well. + +### Global Setup and Teardown: `BeforeSuite` and `AfterSuite` + +Sometimes you want to run some set up code once before the entire test suite and some clean up code once after the entire test suite. For example, perhaps you need to spin up and tear down an external database. + +Ginkgo provides `BeforeSuite` and `AfterSuite` to accomplish this. You typically define these at the top-level in the bootstrap file. For example, say you need to set up an external database: + +```go +package books_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "your/db" + + "testing" +) + +var dbRunner *db.Runner +var dbClient *db.Client + +func TestBooks(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Books Suite") +} + +var _ = BeforeSuite(func() { + dbRunner = db.NewRunner() + err := dbRunner.Start() + Expect(err).NotTo(HaveOccurred()) + + dbClient = db.NewClient() + err = dbClient.Connect(dbRunner.Address()) + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + dbClient.Cleanup() + dbRunner.Stop() +}) +``` + +The `BeforeSuite` function is run before any specs are run. If a failure occurs in the `BeforeSuite` then none of the specs are run and the test suite ends. + +The `AfterSuite` function is run after all the specs have run, regardless of whether any tests have failed. Since the `AfterSuite` typically includes code to clean up persistent state ginkgo will *also* run `AfterSuite` when you send the running test suite an interrupt signal (`^C`). To abort the `AfterSuite` send another interrupt signal. + +Both `BeforeSuite` and `AfterSuite` can be run asynchronously by passing a function that takes a `Done` parameter. + +You are only allowed to define `BeforeSuite` and `AfterSuite` *once* in a test suite (you shouldn't need more than one!) + +Finally, when running in parallel, each parallel process will run `BeforeSuite` and `AfterSuite` functions. [Look here](#parallel-specs) for more on running tests in parallel. + +### Documenting Complex `It`s: `By` + +As a rule, you should try to keep your `It`s, `BeforeEach`es, etc. short and to the point. Sometimes this is not possible, particularly when testing complex workflows in integration-style tests. In these cases your test blocks begin to hide a narrative that is hard to glean by looking at code alone. Ginkgo provides `by` to help in these situations. Here's a hokey example: + +```go +var _ = Describe("Browsing the library", func() { + BeforeEach(func() { + By("Fetching a token and logging in") + + authToken, err := authClient.GetToken("gopher", "literati") + Exepect(err).NotTo(HaveOccurred()) + + err := libraryClient.Login(authToken) + Exepect(err).NotTo(HaveOccurred()) + }) + + It("should be a pleasant experience", func() { + By("Entering an aisle") + + aisle, err := libraryClient.EnterAisle() + Expect(err).NotTo(HaveOccurred()) + + By("Browsing for books") + + books, err := aisle.GetBooks() + Expect(err).NotTo(HaveOccurred()) + Expect(books).To(HaveLen(7)) + + By("Finding a particular book") + + book, err := books.FindByTitle("Les Miserables") + Expect(err).NotTo(HaveOccurred()) + Expect(book.Title).To(Equal("Les Miserables")) + + By("Check the book out") + + err := libraryClient.CheckOut(book) + Expect(err).NotTo(HaveOccurred()) + books, err := aisle.GetBooks() + Expect(books).To(HaveLen(6)) + Expect(books).NotTo(ContainElement(book)) + }) +}) +``` + +The string passed to `By` is emitted via the [`GinkgoWriter`](#logging-output). If a test succeeds you won't see any output beyond Ginkgo's green dot. If a test fails, however, you will see each step printed out up to the step immediately preceding the failure. Running with `ginkgo -v` always emits all steps. + +`By` takes an optional function of type `func()`. When passed such a function `By` will immediately call the function. This allows you to organize your `It`s into groups of steps but is purely optional. In practice the fact that each `By` function is a separate callback limits the usefulness of this approach. + +--- + +## The Spec Runner + +### Pending Specs + +You can mark an individual spec or container as Pending. This will prevent the spec (or specs within the container) from running. You do this by adding a `P` or an `X` in front of your `Describe`, `Context`, `It`, and `Measure`: + +```go +PDescribe("some behavior", func() { ... }) +PContext("some scenario", func() { ... }) +PIt("some assertion") +PMeasure("some measurement") + +XDescribe("some behavior", func() { ... }) +XContext("some scenario", func() { ... }) +XIt("some assertion") +XMeasure("some measurement") +``` + +> You don't need to remove the `func() { ... }` when you mark an `It` or `Measure` as pending. Ginkgo will happily ignore any arguments after the string. + +> By default, Ginkgo will print out a description for each pending spec. You can suppress this by setting the `--noisyPendings=false` flag. + +> By default, Ginkgo will not fail a suite for having pending specs. You can pass the `--failOnPending` flag to reverse this behavior. + +Using the `P` and `X` prefixes marks specs as pending at compile time. If you need to skip a spec at *runtime* (perhaps due to a constraint that can only be known at runtime) you may call `Skip` in your test: + +```go +It("should do something, if it can", func() { + if !someCondition { + Skip("special condition wasn't met") + } + + // assertions go here +}) +``` + +> By default, Ginkgo will print out a description for each skipped spec. You can suppress this by setting the `--noisySkippings=false` flag. + +Note that `Skip(...)` causes the closure to exit so there is no need to return. + +### Focused Specs + +It is often convenient, when developing to be able to run a subset of specs. Ginkgo has two mechanisms for allowing you to focus specs: + +1. You can focus individual specs or whole containers of specs *programatically* by adding an `F` in front of your `Describe`, `Context`, and `It`: + ```go + FDescribe("some behavior", func() { ... }) + FContext("some scenario", func() { ... }) + FIt("some assertion", func() { ... }) + ``` + + doing so instructs Ginkgo to only run those specs. To run all specs, you'll need to go back and remove all the `F`s. + +2. You can pass in a regular expression with the `--focus=REGEXP` and/or `--skip=REGEXP` flags. Ginkgo will only run specs that match the focus regular expression and don't match the skip regular expression. + +3. In cases where specs dont provide enough hierarchichal distinction between groups of tests, directories can be included in the matching of `focus` and `skip`, via the `--regexScansFilePath` option. That is, if the original code location for a test is `test/a/b/c/my_test.go`, one can combine `--focus=/b/` along with `--regexScansFilePath=true` to focus on tests including the path `/b/`. This feature is useful for filtering tests in binary artifacts along the lines of the original directory where those tests were created - but ideally your specs should be organized in such a way as to minimize the need for using this feature. + +When Ginkgo detects that a passing test suite has a programmatically focused test it causes the suite to exit with a non-zero status code. This is to help detect erroneously committed focused tests on CI systems. When passed a command-line focus/skip flag Ginkgo exits with status code 0 - if you want to focus tests on your CI system you should explicitly pass in a -focus or -skip flag. + +Nested programmatically focused specs follow a simple rule: if a leaf-node is marked focused, any of its ancestor nodes that are marked focus will be unfocused. With this rule, sibling leaf nodes (regardless of relative-depth) that are focused will run regardless of the focus of a shared ancestor; and non-focused siblings will not run regardless of the focus of the shared ancestor or the relative depths of the siblings. More simply: + +```go +FDescribe("outer describe", func() { + It("A", func() { ... }) + It("B", func() { ... }) +}) +``` + +will run both `It`s but + +```go +FDescribe("outer describe", func() { + It("A", func() { ... }) + FIt("B", func() { ... }) +}) +``` + +will only run `B`. This behavior tends to map more closely to what the developer actually intends when iterating on a test suite. + +> The programatic approach and the `--focus=REGEXP`/`--skip=REGEXP` approach are mutually exclusive. Using the command line flags will override the programmatic focus. + +> Focusing a container with no `It` or `Measure` leaf nodes has no effect. Since there is nothing to run in the container, Ginkgo effectively ignores it. + +> When using the command line flags you can specify one or both of `--focus` and `--skip`. If both are specified the constraints will be `AND`ed together. + +> You can unfocus programatically focused tests by running `ginkgo unfocus`. This will strip the `F`s off of any `FDescribe`, `FContext`, and `FIt`s that your tests in the current directory may have. + +> If you want to skip entire packages (when running `ginkgo` recursively with the `-r` flag) you can pass a comma-separated list to `--skipPackage=PACKAGES,TO,SKIP`. Any packages with *paths* that contain one of the entries in this comma separated list will be skipped. + +### Spec Permutation + +By default, Ginkgo will randomize the order in which your specs are run. This can help suss out test pollution early on in a suite's development. + +Ginkgo's default behavior is to only permute the order of top-level containers -- the specs *within* those containers continue to run in the order in which they are specified in the test file. This is helpful when developing specs as it mitigates the coginitive overload of having specs continuously change the order in which they run. + +To randomize *all* specs in a suite, you can pass the `--randomizeAllSpecs` flag. This is useful on CI and can greatly help fight the scourge of test pollution. + +Ginkgo uses the current time to seed the randomization. It prints out the seed near the beginning of the test output. If you notice test intermittent test failures that you think may be due to test pollution, you can use the seed from a failing suite to exactly reproduce the spec order for that suite. To do this pass the `--seed=SEED` flag. + +When running multiple spec suites Ginkgo defaults to running the suites in the order they would be listed on the file-system. You can permute the suites by passing `ginkgo --randomizeSuites` + +### Parallel Specs + +Ginkgo has support for running specs in parallel. It does this by spawning separate `go test` processes and serving specs to each process off of a shared queue. This is important for a BDD test framework, as the shared context of the closures does not parallelize well in-process. + +To run a Ginkgo suite in parallel you *must* use the `ginkgo` CLI. Simply pass in the `-p` flag: + + ginkgo -p + +this will automatically detect the optimal number of test nodes to spawn (see the note below). + +To specify the number of nodes to spawn, use `-nodes`: + + ginkgo -nodes=N + +> You do not need to specify both `-p` and `-nodes`. Setting `-nodes` to anything greater than 1 implies a parallelized test run. + +> The number of nodes used with `-p` is `runtime.NumCPU()` if `runtime.NumCPU() <= 4`, otherwise it is `runtime.NumCPU() - 1` based on a rigorous science based heuristic best characterized as "my gut sense based on a few months of experience" + +The test runner collates output from the running processes into one coherent output. This is done, under the hood, via a client-server model: as each client suite completes a test, the test output and status is sent to the server which then prints to screen. This collates the output of simultaneous test runners to one coherent (i.e. non-interleaved), aggregated, output. + +It is sometimes necessary/preferable to view the output of the individual parallel test suites in real-time. To do this you can set `-stream`: + + ginkgo -nodes=N -stream + +When run with the `-stream` flag the test runner simply pipes the output from each individual node as it runs (it prepends each line of output with the node # that the output came from). This results in less coherent output (lines from different nodes will be interleaved) but can be valuable when debugging flakey/hanging test suites. + +> On windows, parallel tests default to `-stream` because Ginkgo can't capture logging to stdout/stderr (necessary for aggregation) on windows. + +#### Managing External Processes in Parallel Test Suites + +If your tests spin up or connect to external processes you'll need to make sure that those connections are safe in a parallel context. One way to ensure this would be, for example, to spin up a separate instance of an external resource for each Ginkgo process. For example, let's say your tests spin up and hit a database. You could bring up a different database server bound to a different port for each of your parallel processes: + +```go +package books_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/ginkgo/config" + + "your/db" + + "testing" +) + +var dbRunner *db.Runner +var dbClient *db.Client + + +func TestBooks(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Books Suite") +} + +var _ = BeforeSuite(func() { + port := 4000 + config.GinkgoConfig.ParallelNode + + dbRunner = db.NewRunner() + err := dbRunner.Start(port) + Expect(err).NotTo(HaveOccurred()) + + dbClient = db.NewClient() + err = dbClient.Connect(dbRunner.Address()) + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + dbClient.Cleanup() + dbRunner.Stop() +}) +``` + + +The `github.com/onsi/ginkgo/config` package provides your suite with access to the command line configuration parameters passed into Ginkgo. The `config.GinkgoConfig.ParallelNode` parameter is the index for the current node (starts with `1`, goes up to `N`). Similarly `config.GinkgoConfig.ParallelTotal` is the total number of nodes running in parallel. + +#### Managing *Singleton* External Processes in Parallel Test Suites + +When possible, you should make every effort to start up a new instance of an external resource for every parallel node. This helps avoid test-pollution by strictly separating each parallel node. + +Sometimes (rarely) this is not possible. Perhaps, for reasons beyond your control, you can only start one instance of a service on your machine. Ginkgo provides a workaround for this with `SynchronizedBeforeSuite` and `SynchronizedAfterSuite`. + +The idea here is simple. With `SynchronizedBeforeSuite` Ginkgo gives you a way to run some preliminary setup code on just one parallel node (Node 1) and other setup code on all nodes. Ginkgo synchronizes these functions and guarantees that node 1 will complete its preliminary setup before the other nodes run their setup code. Moreover, Ginkgo makes it possible for the preliminary setup code on the first node to pass information on to the setup code on the other nodes. + +Here's what our earlier database example looks like using `SynchronizedBeforeSuite`: + +```go +var _ = SynchronizedBeforeSuite(func() []byte { + port := 4000 + config.GinkgoConfig.ParallelNode + + dbRunner = db.NewRunner() + err := dbRunner.Start(port) + Expect(err).NotTo(HaveOccurred()) + + return []byte(dbRunner.Address()) +}, func(data []byte) { + dbAddress := string(data) + + dbClient = db.NewClient() + err = dbClient.Connect(dbAddress) + Expect(err).NotTo(HaveOccurred()) +}) +``` + +`SynchronizedBeforeSuite` must be passed two functions. The first must return `[]byte` and the second must accept `[]byte`. When running with multiple nodes the *first* function is only run on node 1. When this function completes, all nodes (including node 1) proceed to run the *second* function and will receive the data returned by the first function. In this example, we use this data-passing mechanism to forward the database's address (set up on node 1) to all nodes. + +To clean up correctly, you should use `SynchronizedAfterSuite`. Continuing our example: + +```go +var _ = SynchronizedAfterSuite(func() { + dbClient.Cleanup() +}, func() { + dbRunner.Stop() +}) +``` + +With `SynchronizedAfterSuite` the *first* function is run on *all* nodes (including node 1). The *second* function is only run on node 1. Moreover, the second function is only run when all other nodes have finished running. This is important, since node 1 is responsible for setting up and tearing down the singleton resources it must wait for the other nodes to end before tearing down the resources they depend on. + +Finally, all of these function can be passed an additional `Done` parameter to run asynchronously. When running asynchronously, an optional timeout can be provided as a third parameter to `SynchronizedBeforeSuite` and `SynchronizedAfterSuite`. The same timeout is applied to both functions. + +> Note an important subtelty: The `dbRunner` variable is *only* populated on Node 1. No other node should attempt to touch the data in that variable (it will be nil on the other nodes). The `dbClient` variable, which is populated in the second `SynchronizedBeforeSuite` function is, of course, available across all nodes. + +--- + +## Understanding Ginkgo's Lifecycle + +Users of Ginkgo sometimes get tripped up by Ginkgo's lifecycle. This section provides a mental model to help you reason about what code runs when. + +Ginkgo endeavors to carefully control the order in which specs run and provides seamless support for running a given test suite in parallel across [multiple processes](#parallel-specs). To accomplish this, Ginkgo needs to know a suite's entire testing tree (i.e. the nested set of `Describe`s, `Context`s, `BeforeEach`es, `It`s, etc.) **up front**. Ginkgo uses this tree to construct an ordered, ([deterministically randomized](#spec-permutation)), list of tests to run. + +This means that all the tests must be defined _before_ Ginkgo can run the suite. Once the suite is running it is an error to attempt to define a new test (e.g. calling `It` within an existing `It` block). + +Of course, it is still possible (in fact, common) to dynamically generate test suites based on configuration. However you must generate these tests _at the right time_ in the Ginkgo lifecycle. This nuance sometimes trips users up. + +Let's look at a typical Ginkgo test suite. What follows is a test suite for a `books` package that spans multiple files: + + +```go +// books_suite_test.go + +package books_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/books" + + "testing" +) + +func TestBooks(t *testing.T) { // L1 + RegisterFailHandler(Fail) // L2 + RunSpecs(t, "Books Suite") // L3 +} // L4 +``` + +```go +// reading_test.go + +package books_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/books" + + "testing" +) + +var _ = Describe("When reading a book", func() { // L5 + var book *books.Book // L6 + + BeforeEach(func() { // L7 + book = books.New("The Chronicles of Narnia", 300) // L8 + Expect(book.CurrentPage()).To(Equal(1)) // L9 + Expect(book.NumPages()).To(Equal(300)) // L10 + }) // L11 + + It("should increment the page number", func() { // L12 + err := book.Read(3) // L13 + Expect(err).NotTo(HaveOccurred()) // L14 + Expect(book.CurrentPage()).To(Equal(4)) // L15 + }) // L16 + + Context("when the reader finishes the book", func() { // L17 + It("should not allow them to read more pages", func() { // L18 + err := book.Read(300) // L19 + Expect(err).NotTo(HaveOccurred()) // L20 + Expect(book.IsFinished()).To(BeTrue()) // L21 + err = book.Read(1) // L22 + Expect(err).To(HaveOccurred()) // L23 + }) // L24 + }) // L25 +}) // L26 +``` + +```go +// isbn_test.go + +package books_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/books" + + "testing" +) + +var _ = Describe("Looking up ISBN numbers", func() { // L27 + Context("When the book can be found", func() { // L28 + It("returns the correct ISBN number", func() { // L29 + Expect(books.ISBNFor("The Chronicles of Narnia", "C.S. Lewis")).To(Equal("9780060598242")) // L30 + }) // L31 + }) // L32 + + Context("When the book can't be found", func() { // L33 + It("returns an error", func() { // L34 + isbn, err := books.ISBNFor("The Chronicles of Blarnia", "C.S. Lewis") // L35 + Expect(isbn).To(BeZero()) // L36 + Expect(err).To(HaveOccurred()) // L37 + }) // L38 + }) // L39 +}) // L40 +``` + +Here's what happens when this test is run via the `ginkgo` cli, in order: + +1. `ginkgo` runs `go test -c` to compile the test binary +2. `ginkgo` launches the test binary (this is equivalent to simply running `go test` but gives Ginkgo the ability to launch multiple test processes without paying the cost of repeat compilation) +3. The test binary loads into memory and all top-level functions are defined and invoked. Specifically, that means: + 1. The `TestBooks` function is defined (Line `L1`) + 2. The `Describe` at `L5` is invoked and passed in the string "When reading a book", and the anonymous function that contains the tests nested in this describe. + 3. The `Describe` at `L27` is invoked and passed in the string "Looking up ISBN numbers", and the anonymous function that contains the tests nested in this describe. + + Note that the anonymous functions passed into the `Describe`s are *not* invoked at this time. At this point, Ginkgo simply knows that there are two top-level containers in this suite. +4. The `go test` runtime starts running tests by invoking `TestBooks()` (`L1`) +5. Ginkgo's `Fail` handler is registered with `gomega` via `RegisterFailHandler` (`L2`) - this is necessary because `ginkgo` and `gomega` are not tightly coupled and alternative matcher libraries can be used with Ginkgo. +6. `RunSpecs` is called (`L3`). This does a number of things: + + **Test Tree Construction Phase**: + 1. Ginkgo iterates through every top-level container(i.e. the two `Describe`s at `L5` and `L27`) and invokes their anonymous functions. + 2. When the function at `L5` is invoked it: + - Defines a closure variable named `book` (`L6`) + - Registers a `BeforeEach` passing it an anonymous function (`L7`). This function is registered and saved as part of the testing tree and **is not run yet**. + - Registers an `It` with a description and anonymous function. (`L12`). This function is registered and saved as part of the testing tree and **is not run yet**. + - Adds a nested `Context` (`L17`). The anonymous function passed into the `Context` is **immediately** invoked to continue building the tree. This registers the `It` at line `L18`. + - At this point the anonymous function at `L5` exists **and is never called again**. + 3. The function for the next top-level describe at `L27` is also invoked, and behaves similarly. + 4. At this point all top-level containers have been invoked and the testing tree looks like: + ``` + [ + ["When Reading A Book", BeforeEach, It "should increment the page number"], + ["When Reading A Book", BeforeEach, "When the reader finishes the book", It "should not allow them to read more pages"], + ["Looking up ISBN numbers", "When the book can be found", It "returns the correct ISBN number"], + ["Looking up ISBN numbers", "When the book can't be found", It "returns an error"], + ] + ``` + Here, the `BeforeEach` and `It` nodes in the tree all contain references to their respective anonymous functions. Note that there are four tests, each corresponding to one of the four `It`s defined in the test. + + Having constructed the testing tree, Ginkgo can now randomize it deterministically based on the random seed. This simply amounts to shuffling the list of tests shown above. + + **Test Tree Invocation Phase**: + To run the tests, Ginkgo now simply walks the shuffled testing tree. Invoking the anonymous functions attached to any registered `BeforeEach` and `It` in order. For example, when running the test defined at `L18` Ginkgo will first invoke the anonymous function passed into the `BeforeEach` at `L7` and then the anonymous function passed into the `It` at `L18`. + + > Note, again, that the parent closure defined at `L5` is _not_ reinvoked. The functions passed into `Describe`s and `Context`s are _only_ invoked during the **Tree Construction Phase** + + > It is an error to define new `It`s, `BeforeEach`es, etc. during the Test Tree Invocation Phase. + +7. `RunSpecs` keeps track of running tests and test failures, updating any attached reporters as the test runs. When the test completes `RunSpecs` exits and the `TestBooks` function exits. + + +That was a lot of detail but it all boils down to a fairly simple flow. To summarize: + +There are two phases during a Ginkgo test run. + +In the **Test Tree Construction Phase** the anonymous functions passed into any containers (i.e. `Describe`s and `Context`s) are invoked. These functions define closure variables and call child nodes (`It`s, `BeforeEach`es, and `AfterEach`es etc.) to definte the testing tree. The anonymous functions passed into child nodes are **not** called during the Test Tree Construction Phase. Once constructed the tree is randomized. + +In the **Test Tree Invocation Phase** the child node functions are invoked in the correct order. Note that the container functions are not invoked during this phase. + +### Avoiding test pollution + +Because the anonymous functions passed into container functions are _not_ reinvoked during the Test Tree Invocation Phase you should not rely on variable initializations in container functions to be called repeatedly. You *must*, instead, reinitialize any variables that can be mutated by tests in your `BeforeEach` functions. + +Consider, for example, this variation of the `"When reading a book"` `Describe` container defined in `L5` above: + +``` +var _ = Describe("When reading a book", func() { //L1' + var book *books.Book //L2' + book = books.New("The Chronicles of Narnia", 300) // create book in parent closure //L3' + + It("should increment the page number", func() { //L4' + err := book.Read(3) //L5' + Expect(err).NotTo(HaveOccurred()) //L6' + Expect(book.CurrentPage()).To(Equal(4)) //L7' + }) //L8' + + Context("when the reader finishes the book", func() { //L9' + It("should not allow them to read more pages", func() { //L10' + err := book.Read(300) //L11' + Expect(err).NotTo(HaveOccurred()) //L12' + Expect(book.IsFinished()).To(BeTrue()) //L13' + err = book.Read(1) //L14' + Expect(err).To(HaveOccurred()) //L15' + }) //L16' + }) //L17' +}) //L18' +``` + +In this variation not only is the book variable shared between both `It`s. The exact same book instance is shared between both `It`s. This will lead to confusing test pollution behavior that will vary depending on which order the `It`s are called in. For example, if the `"should not allow them to read more pages"` test (`L10'`)is invoked first, then the book will already be finished (`L13'`)when the `"should increment the page number"` (`L4'`) test is called resulting in an aberrant test failure. + +The correct solution to test pollution like this is to always initialize variables in `BeforeEach` blocks. This ensures test state is clean between each test run. + +### Do not make assertions in container node functions + +A related, common, error is to make assertions in the anonymous functions passed into container node. Assertions must _only_ be made in the functions of child nodes as only those functions run during the Test Tree Invocation Phase. + +So, avoid code like this: + +``` +var _ = Describe("When reading a book", func() { + var book *books.Book + book = books.New("The Chronicles of Narnia", 300) + Expect(book.CurrentPage()).To(Equal(1)) + Expect(book.NumPages()).To(Equal(300)) + + It("...") +}) +``` + +If those assertions fail, they will do so during the Test Tree Construction Phase - not the Test Tree Invocation Phase when Ginkgo is tracking and reporting failures. Instead, initialize variables and make correctness assertions like these inside a `BeforeEach` block. + +### Patterns for dynamically generating tests + +A common pattern (and one closely related to the [shared example patterns outlined below](#shared-example-patterns)) is to dynamically generate a test suite based on external input (e.g. a file or an environment variable). + +Imagine, for example, a file named `isbn.json` that includes a set of known ISBN lookups: + +```json +// isbn.json +[ + {"title": "The Chronicles of Narnia", "author": "C.S. Lewis", "isbn": "9780060598242"}, + {"title": "Ender's Game", "author": "Orson Scott Card", "isbn": "9780765378484"}, + {"title": "Ender's Game", "author": "Victor Hugo", "isbn": "9780140444308"}, +] +``` + +You might want to generate a collection of tests, one for each book. A recommended pattern for this is: + +```go +// isbn_test.go + +package books_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/books" + + "testing" +) + +var _ = Describe("Looking up ISBN numbers", func() { + testConfigData := loadTestISBNs("isbn.json") + + Context("When the book can be found", func() { + for _, d := range testConfigData { + d := d //necessary to ensure the correct value is passed to the closure + It("returns the correct ISBN number for " + d.Title, func() { + Expect(books.ISBNFor(d.Title, d.Author)).To(Equal(d.ISBN)) + }) + } + }) +}) +``` + +Here `data` is defined and initialized with the contents of the `isbn.json` file during the Test Tree Construction Phase and is then used to define a set of `It`s. + +If you have test configuration data like this that you want to share across multiple top-level `Describes` or test files you can either load it in each `Describe` (as shown here) or load it once into a globally shared variable. The recommended pattern for the latter is to load such variables just prior to the invocation of `RunSpecs`: + + +```go +// books_suite_test.go + +package books_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/books" + + "testing" +) + +var testConfigData TestConfigData + +func TestBooks(t *testing.T) { + RegisterFailHandler(Fail) + testConfigData = loadTestISBNs("isbn.json") + RunSpecs(t, "Books Suite") +} +``` + +Here, the `testConfigData` can be referenced in any subsequent `Describe` or `Context` closure and is guaranteed to be initialized as the Test Tree Construction Phase does not begin until `RunSpecs` is invoked. + +If you must make an assertion on the `testConfigData` you can do so in a `BeforeSuite` as follows: + +```go +func TestBooks(t *testing.T) { + RegisterFailHandler(Fail) + testConfigData = loadTestISBNs("isbn.json") + RunSpecs(t, "Books Suite") +} + +var _ = BeforeSuite(func() { + Expect(testConfigData).NotTo(BeEmpty()) +}) +``` + +This works because `BeforeSuite` functions run during the Test Tree Invocation phase and only run once. + +Lastly - the most common mistake folks encounter when dynamically generating tests is to set up test configuration data in a node that only runs during the Test Tree Invocation Phase. For example: + +```go + +var _ = Describe("Looking up ISBN numbers", func() { + var testConfigData TestConfigData + + BeforeEach(func() { + testConfigData = loadTestISBNs("isbn.json") // WRONG! + }) + + Context("When the book can be found", func() { + for _, d := range testConfigData { + d := d //necessary to ensure the correct value is passed to the closure + It("returns the correct ISBN number for " + d.Title, func() { + Expect(books.ISBNFor(d.Title, d.Author)).To(Equal(d.ISBN)) + }) + } + }) +}) +``` + +This will generate zero tests as `testConfigData` will be zero during the Test Tree Construction Phase. + +--- + +## Asynchronous Tests + +Go does concurrency well. Ginkgo provides support for testing asynchronicity effectively. + +Consider this example: + +```go +It("should post to the channel, eventually", func() { + c := make(chan string, 0) + + go DoSomething(c) + Expect(<-c).To(ContainSubstring("Done!")) +}) +``` +This test will block until a response is received over the channel `c`. A deadlock or timeout is a common failure mode for tests like this, a common pattern in such situations is to add a select statement at the bottom of the function and include a `<-time.After(X)` channel to specify a timeout. + +Ginkgo has this pattern built in. The `body` functions in all non-container blocks (`It`s, `BeforeEach`es, `AfterEach`es, `JustBeforeEach`es, `JustAfterEach`es, and `Benchmark`s) can take an optional `done Done` argument: + +```go +It("should post to the channel, eventually", func(done Done) { + c := make(chan string, 0) + + go DoSomething(c) + Expect(<-c).To(ContainSubstring("Done!")) + close(done) +}, 0.2) +``` + +`Done` is a `chan interface{}`. When Ginkgo detects that the `done Done` argument has been requested it runs the `body` function as a goroutine, wrapping it with the necessary logic to apply a timeout assertion. You must either close the `done` channel, or send something (anything) to it to tell Ginkgo that your test has ended. If your test doesn't end after a timeout period, Ginkgo will fail the test and move on the next one. + +The default timeout is 1 second. You can modify this timeout by passing a `float64` (in seconds) after the `body` function. In this example we set the timeout to 0.2 seconds. + +> Gomega has additional support for making rich assertions on asynchronous code. Make sure to check out how `Eventually` works in Gomega. + +--- + +## The Ginkgo CLI + +The Ginkgo CLI can be installed by running + + $ go install github.com/onsi/ginkgo/ginkgo + +It offers a number of conveniences beyond what `go test` provides out of the box and is recommended, but not necessary. + +### Running Tests + +To run the suite in the current directory, simply run: + + $ ginkgo #or go test + +To run the suites in other directories, simply run: + + $ ginkgo /path/to/package /path/to/other/package ... + +To pass arguments/custom flags down to your test suite: + + $ ginkgo -- + +Note: the `--` is important! Only arguments following `--` will be passed to your suite. To parse arguments/custom flags in your test suite, declare a variable and initialize it at the package-level: + +```go +var myFlag string +func init() { + flag.StringVar(&myFlag, "myFlag", "defaultvalue", "myFlag is used to control my behavior") +} +``` + +Of course, ginkgo takes a number of flags. These must be specified *before* you specify the packages to run. Here's a summary of the call syntax: + + $ ginkgo -- + +Here are the flags that Ginkgo accepts: + +**Controlling which test suites run:** + +- `-r` + + Set `-r` to have the `ginkgo` CLI recursively run all test suites under the target directories. Useful for running all the tests across all your packages. + +- `-skipPackage=PACKAGES,TO,SKIP` + + When running with `-r` you can pass `-skipPackage` a comma-separated list of entries. Any packages with *paths* that contain one of the entries in this comma separated list will be skipped. + +**Running in parallel:** + +- `-p` + + Set `-p` to parallelize the test suite across an auto-detected number of nodes. + +- `--nodes=NODE_TOTAL` + + Use this to parallelize the suite across NODE_TOTAL processes. You don't need to specify `-p` as well (though you can!) + +- `-stream` + + By default, when parallelizing a suite, the test runner aggregates data from each parallel node and produces coherent output as the tests run. Setting `stream` to `true` will, instead, stream output from all parallel nodes in real-time, prepending each log line with the node # that emitted it. This leads to incoherent (interleaved) output, but is useful when debugging flakey/hanging test suites. + +**Modifying output:** + +- `--noColor` + + If provided, Ginkgo's default reporter will not print out in color. + +- `--succinct` + + Succinct silences much of Ginkgo's more verbose output. Test suites that succeed basically get printed out on just one line! Succinct is turned off, by default, when running tests for one package. It is turned on by default when Ginkgo runs multiple test packages. + +- `--v` + + If present, Ginkgo's default reporter will print out the text and location for each spec before running it. Also, the GinkgoWriter will flush its output to stdout in realtime. + +- `--noisyPendings=false` + + By default, Ginkgo's default reporter will provide detailed output for pending specs. You can set --noisyPendings=false to suppress this behavior. + +- `--noisySkippings=false` + + By default, Ginkgo's default reporter will provide detailed output for skipped specs. You can set --noisySkippings=false to suppress this behavior. + +- `--reportPassed` + + If present, Ginkgo's default reporter will print detailed output for passed specs. + +- `--reportFile=` + + Create report output file in specified path (relative or absolute). It will also override a pre-defined path of `ginkgo.Reporter`, and parent directories will be created, if not exist. + +- `--trace` + + If present, Ginkgo will print out full stack traces for each failure, not just the line number at which the failure occurs. + +- `--progress` + + If present, Ginkgo will emit the progress to the `GinkgoWriter` as it enters and runs each `BeforeEach`, `AfterEach`, `It`, etc... node. This is useful for debugging stuck tests (e.g. where exactly is the test getting stuck?) and for making tests that emit many logs to the `GinkgoWriter` more readable (e.g. which logs were emitted in the `BeforeEach`? Which were emitted by the `It`?). Combine with `--v` to emit the `--progress` output to stdout. + +**Controlling randomization:** + +- `--seed=SEED` + + The random seed to use when permuting the specs. + +- `--randomizeAllSpecs` + + If present, all specs will be permuted. By default Ginkgo will only permute the order of the top level containers. + +- `--randomizeSuites` + + If present and running multiple spec suites, the order in which the specs run will be randomized. + +**Focusing and Skipping specs:** + +- `--skipMeasurements` + + If present, Ginkgo will skip any `Measure` specs you've defined. + +- `--focus=REGEXP` + + If provided, Ginkgo will only run specs with descriptions that match the regular expression REGEXP. + +- `--skip=REGEXP` + + If provided, Ginkgo will only run specs with descriptions that do not match the regular expression REGEXP. + +**Running the race detector and code coverage tools:** + +- `-race` + + Set `-race` to have the `ginkgo` CLI run your tests with the race detector on. + +- `-cover` + + Set `-cover` to have the `ginkgo` CLI run your tests with coverage analysis turned on (a Go 1.2+ feature). Ginkgo will generate coverage profiles under the current directory named `PACKAGE.coverprofile` for each set of package tests that is run. + +- `-coverpkg=,` + + Like `-cover`, `-coverpkg` runs your tests with coverage analysis turned on. However, `-coverpkg` allows you to specify the packages to run the analysis on. This allows you to get coverage on packages outside of the current package, which is useful for integration tests. Note that it will not run coverage on the current package by default, you always need to specify all packages you want coverage for. + The package name should be fully resolved, eg `github.com/onsi/ginkgo/reporters/stenographer` + +- `-coverprofile=` + + Renames coverage results file to a provided filename + +- `-outputdir=` + + Moves coverage results to a specified directory
+ When combined with `-coverprofile` will also append them together + +**Build flags:** + +- `-tags` + + Set `-tags` to pass in build tags down to the compilation step. + +- `-compilers` + + When compiling multiple test suites (e.g. with `ginkgo -r`) Ginkgo will use `runtime.NumCPU()` to determine the number of compile processes to spin up. On some environments this is a bad idea. You can specify th enumber of compilers manually with this flag. + +**Failure behavior:** + +- `--failOnPending` + + If present, Ginkgo will mark a test suite as failed if it has any pending specs. + +- `--failFast` + + If present, Ginkgo will stop the suite right after the first spec failure. + +**Watch flags:** + +- `--depth=DEPTH` + + When watching packages, Ginkgo also watches those package's dependencies for changes. The default value for `--depth` is `1` meaning that only the immediate dependencies of a package are monitored. You can adjust this up to monitor dependencies-of-dependencies, or set it to `0` to only monitor the package itself, not its dependencies. + +- `--watchRegExp=WATCH_REG_EXP` + + When watching packages, Ginkgo only monitors files matching the watch regular expression for changes. The default value is `\.go$` meaning only go files are watched for changes. + +**Flaky test mitigation:** + +- `--flakeAttempts=ATTEMPTS` + + If a test fails, Gingko can rerun it immediately. Set this flag to a value + greater than 1 to enable retries. As long as one of the retries succeeds, + Ginkgo will not consider the test suite to have been failed. + + This flag is dangerous! Don't be tempted to use it to cover up bad tests! + +**Miscellaneous:** + +- `-dryRun` + + If present, Ginkgo will walk your test suite and report output *without* actually running your tests. This is best paired with `-v` to preview which tests will run. Ther ordering of the tests honors the randomization strategy specified by `--seed` and `--randomizeAllSpecs`. Note that `-dryRun` does not work with parallel tests and Ginkgo will emit an error if you try to use `-dryRun` with `-p` or `-nodes`. + +- `-keepGoing` + + By default, when running multiple tests (with -r or a list of packages) Ginkgo will abort when a test fails. To have Ginkgo run subsequent test suites after a failure you can set -keepGoing. + +- `-untilItFails` + + If set to `true`, Ginkgo will keep running your tests until a failure occurs. This can be useful to help suss out race conditions or flakey tests. It's best to run this with `--randomizeAllSpecs` and `--randomizeSuites` to permute test order between iterations. + +- `--slowSpecThreshold=TIME_IN_SECONDS` + + By default, Ginkgo's default reporter will flag tests that take longer than 5 seconds to run -- this does not fail the suite, it simply notifies you of slow running specs. You can change this threshold using this flag. + +- `-timeout=DURATION` + + Ginkgo will fail the test suite if it takes longer than `DURATION` to run. The default value is 24 hours. + +- `--afterSuiteHook=HOOK_COMMAND` + + Ginko has the ability to run a command hook after a suite test completes. You simply give it the command to run and it will do string replacement to pass data into the command. Example: --afterSuiteHook=”echo (ginkgo-suite-name) suite tests have [(ginkgo-suite-passed)]” This suite hook will replace (ginkgo-suite-name) and (ginkgo-suite-passed) with the suite name and pass/fail status respectively, then echo that to the terminal. + +- `-requireSuite` + + If you create a package with Ginkgo test files, but forget to run `ginkgo bootstrap` your tests will never run and the suite will always pass. Ginkgo does notify with the message `Found no test suites, did you forget to run "ginkgo bootstrap"?` but doesn't fail. This flag causes Ginkgo to mark the suite as failed if there are test files but nothing that references `RunSpecs.` + +### Watching For Changes + +The Ginkgo CLI provides a `watch` subcommand that takes (almost) all the flags that the main `ginkgo` command takes. With `ginkgo watch` ginkgo will monitor the package in the current directory and trigger tests when changes are detected. + +You can also run `ginkgo watch -r` to monitor all packages recursively. + +For each monitored packaged, Ginkgo will also monitor that package's dependencies and trigger the monitored package's test suite when a change in a dependency is detected. By default, `ginkgo watch` monitors a package's immediate dependencies. You can adjust this using the `-depth` flag. Set `-depth` to `0` to disable monitoring dependencies and set `-depth` to something greater than `1` to monitor deeper down the dependency graph. + +### Precompiling Tests + +Ginkgo has strong support for writing integration-style acceptance tests. These tests are useful tools to validate that (for example) a complex distributed system is functioning correctly. It is often convenient to distribute these acceptance tests as standalone binaries. + +Ginkgo allows you to build such binaries with: + + ginkgo build path/to/package + +This will produce a precompiled binary called `package.test`. You can then invoke `package.test` directly to run the test suite. Under the hood `ginkgo` is simply calling `go test -c -o` to compile the `package.test` binary. + +Calling `package.test` directly will run the tests in *series*. To run the tests in parallel you'll need the `ginkgo` cli to orchestrate the parallel nodes. You can run: + + ginkgo -p path/to/package.test + +to do this. Since the ginkgo CLI is a single binary you can provide a parallelizable (and therefore *fast*) set of integration-style acceptance tests simply by distributing two binaries. + +> The `build` subcommand accepts a subset of the flags that `ginkgo` and `ginkgo watch` take. These flags are constrained to compile-time concerns such as `--cover` and `--race`. You can learn more via `ginkgo help build`. + +> You can cross-compile and target different platforms using the standard `GOOS` and `GOARCH` environment variables. So `GOOS=linux GOARCH=amd64 ginkgo build path/to/package` run on OS X will create a `package.test` binary that runs on linux. + +### Generators + +- To bootstrap a Ginkgo test suite for the package in the current directory, run: + + $ ginkgo bootstrap + + This will generate a file named `PACKAGE_suite_test.go` where PACKAGE is the name of the current directory. + +- To add a test file to a package, run: + + $ ginkgo generate + + This will generate a file named `SUBJECT_test.go`. If you don't specify SUBJECT, it will generate a file named `PACKAGE_test.go` where PACKAGE is the name of the current directory. + +By default, these generators will dot-import both Ginkgo and Gomega. To avoid dot imports, you can pass `--nodot` to both subcommands. This is discussed more fully in the [next section](#avoiding-dot-imports). + +> Note that you don't have to use either of these generators. They're just convenient helpers to get you up and running quickly. + +### Avoiding Dot Imports + +Ginkgo and Gomega provide a DSL and, by default, the `ginkgo bootstrap` and `ginkgo generate` commands import both packages into the top-level namespace using dot imports. + +There are certain, rare, cases where you need to avoid this. For example, your code may define methods with names that conflict with the methods defined in Ginkgo and/or Gomega. In such cases you can either import your code into its own namespace (i.e. drop the `.` in front of your package import). Or, you can drop the `.` in front of Ginkgo and/or Gomega. The latter comes at the cost of constantly having to preface your `Describe`s and `It`s with `ginkgo.` and your `Expect`s and `ContainSubstring`s with `gomega.`. + +There is a *third* option that the ginkgo CLI provides, however. If you need to (or simply want to!) avoid dot imports you can: + + ginkgo bootstrap --nodot + +and + + ginkgo generate --nodot + +This will create a bootstrap file that *explicitly* imports all the exported identifiers in Ginkgo and Gomega into the top level namespace. This happens at the bottom of your bootstrap file and generates code that looks something like: + +```go +import ( + github.com/onsi/ginkgo + ... +) + +... + +// Declarations for Ginkgo DSL +var Describe = ginkgo.Describe +var Context = ginkgo.Context +var It = ginkgo.It +// etc... +``` + +This allows you to write tests using `Describe`, `Context`, and `It` without dot imports and without the `ginkgo.` prefix. Crucially, it also allows you to redefine any conflicting identifiers (or even cook up your own semantics!). For example: + +```go +var _ = ginkgo.Describe +var When = ginkgo.Context +var Then = ginkgo.It +``` + +This will avoid importing `Describe` and will rename `Context` to `When` and `It` to `Then`. + +As new matchers are added to Gomega you may need to update the set of imports identifiers. You can do this by entering the directory containing your bootstrap file and running: + + ginkgo nodot + +this will update the imports, preserving any renames that you've provided. + +### Creating an Outline of Tests + +If you want to see an outline of the Ginkgo tests in a file, you can use the `ginkgo outline` command. The following uses the `book_test.go` example from [Getting Started: Writing Your First Test](#getting-started-writing-your-first-test): + + ginkgo outline book_test.go + +This generates an outline in a comma-separated-values (CSV) format. Column headers are on the first line, followed by Ginkgo containers, specs, and other identifiers, in the order they appear in the file: + + Name,Text,Start,End,Spec,Focused,Pending + Describe,Book,124,973,false,false,false + BeforeEach,,217,507,false,false,false + Describe,Categorizing book length,513,970,false,false,false + Context,With more than 300 pages,567,753,false,false,false + It,should be a novel,624,742,true,false,false + Context,With fewer than 300 pages,763,963,false,false,false + It,should be a short story,821,952,true,false,false + +The columns are: + +- Name (string): The name of a container, spec, or other identifier in the core DSL. +- Text (string): The description of a container or spec. (If it is not a literal, it is undefined in the outline.) +- Start (int): Position of the first character in the container or spec. +- End (int): Position of the character immediately after the container or spec. +- Spec (bool): True, if the identifier is a spec. +- Focused (bool): True, if focused. (Conforms to the rules in [Focused Specs](#focused-specs).) +- Pending (bool): True, if pending. (Conforms to the rules in [Pending Specs](#pending-specs).) + +You can set a different output format with the `-format` flag. Accepted formats are `csv`, `indent`, and `json`. The `ident` format is like `csv`, but uses identation to show the nesting of containers and specs. Both the `csv` and `json` formats can be read by another program, e.g., an editor plugin that displays a tree view of Ginkgo tests in a file, or presents a menu for the user to quickly navigate to a container or spec. +### Other Subcommands + +- To unfocus any programmatically focused tests in the current directory (and subdirectories): + + $ ginkgo unfocus + +- For help: + + $ ginkgo help + + For help on a particular subcommand: + + $ ginkgo help + +- To get the current version of Ginkgo: + + $ ginkgo version + +--- + +## Benchmark Tests + +Ginkgo allows you to measure the performance of your code using `Measure` blocks. `Measure` blocks can go wherever an `It` block can go -- each `Measure` generates a new spec. The closure function passed to `Measure` must take a `Benchmarker` argument. The `Benchmarker` is used to measure runtimes and record arbitrary numerical values. You must also pass `Measure` an integer after your closure function, this represents the number of samples of your code `Measure` will perform. + +For example: + +```go +Measure("it should do something hard efficiently", func(b Benchmarker) { + runtime := b.Time("runtime", func() { + output := SomethingHard() + Expect(output).To(Equal(17)) + }) + + Ω(runtime.Seconds()).Should(BeNumerically("<", 0.2), "SomethingHard() shouldn't take too long.") + + b.RecordValue("disk usage (in MB)", HowMuchDiskSpaceDidYouUse()) +}, 10) +``` + +will run the closure function 10 times, aggregating data for "runtime" and "disk usage". Ginkgo's reporter will then print out a summary of each of these metrics containing some simple statistics: + + • [MEASUREMENT] + Suite + it should do something hard efficiently + + Ran 10 samples: + runtime: + Fastest Time: 0.01s + Slowest Time: 0.08s + Average Time: 0.05s ± 0.02s + + disk usage (in MB): + Smallest: 3.0 + Largest: 5.2 + Average: 3.9 ± 0.4 + +With `Measure` you can write expressive, exploratory, specs to measure the performance of various parts of your code (or external components, if you use Ginkgo to write integration tests). As you collect your data, you can leave the `Measure` specs in place to monitor performance and fail the suite should components start growing slow and bloated. + +`Measure`s can live alongside `It`s within a test suite. If you want to run just the `It`s you can pass the `--skipMeasurements` flag to `ginkgo`. + +> You can also mark `Measure`s as pending with `PMeasure` and `XMeasure` or focus them with `FMeasure`. + +### Measuring Time + +The `Benchmarker` passed into your closure function provides the + +```go +Time(name string, body func(), info ...Interface{}) time.Duration +``` + + method. `Time` runs the passed in `body` function and records, and returns, its runtime. The resulting measurements for each sample are aggregated and some simple statistics are computed. These stats appear in the spec output under the `name` you pass in. Note that `name` must be unique within the scope of the `Measure` node. + + You can also pass arbitrary information via the optional `info` argument. This will be passed along to the reporter along with the agreggated runtimes that `Time` measures. The default reporter presents a string representation of `info`, but you can write a custom reporter to perform something more structured. For example, you might run several measurements of the same code, but vary some parameter between runs. You could encode the value of that parameter in `info`, and then have a custom reporter that uses `info` and the statistics provided by Ginkgo to generate a CSV file - or perhaps even a plot. + + If you want to assert that `body` ran within some threshold time, you can make an assertion against `Time`'s return value. + +### Recording Arbitrary Values + +The `Benchmarker` also provides the + +```go +RecordValue(name string, value float64, info ...Interface{}) +``` + +method. `RecordValue` allows you to record arbitrary numerical data. These results are aggregated and some simple statistics are computed. These stats appear in the spec output under the `name` you pass in. Note that `name` must be unique within the scope of the `Measure` node. + +The optional `info` parameter can be used to pass structured data to a custom reporter. See [Measuring Time](#measuring-time) above for more details. + +--- + +## Shared Example Patterns + +Ginkgo doesn't have any explicit support for Shared Examples (also known as Shared Behaviors) but there are a few patterns that you can use to reuse tests across your suite. + +### Locally-scoped Shared Behaviors + +It is often the case that within a particular suite, there will be a number of different `Context`s that assert the exact same behavior, in that they have identical `It`s within them. The only difference between these `Context`s is the set up done in their respective `BeforeEach`s. Rather than repeat the `It`s for these `Context`s, here are two ways to extract the code and avoid repeating yourself. + +#### Pattern 1: Extract a function that defines the shared `It`s + +Here, we will pull out a function that lives within the same closure that `Context`s live in, that defines the `It`s that are common to those `Context`s. For example: + +```go +Describe("my api client", func() { + var client APIClient + var fakeServer FakeServer + var response chan APIResponse + + BeforeEach(func() { + response = make(chan APIResponse, 1) + fakeServer = NewFakeServer() + client = NewAPIClient(fakeServer) + client.Get("/some/endpoint", response) + }) + + Describe("failure modes", func() { + AssertFailedBehavior := func() { + It("should not include JSON in the response", func() { + Ω((<-response).JSON).Should(BeZero()) + }) + + It("should not report success", func() { + Ω((<-response).Success).Should(BeFalse()) + }) + } + + Context("when the server does not return a 200", func() { + BeforeEach(func() { + fakeServer.Respond(404) + }) + + AssertFailedBehavior() + }) + + Context("when the server returns unparseable JSON", func() { + BeforeEach(func() { + fakeServer.Succeed("{I'm not JSON!") + }) + + AssertFailedBehavior() + }) + + Context("when the request errors", func() { + BeforeEach(func() { + fakeServer.Error(errors.New("oops!")) + }) + + AssertFailedBehavior() + }) + }) +}) +``` + +Note that the `AssertFailedBehavior` function is called within the body of the `Context` container block. The `It`s defined by this function get added to the enclosing container. Since the function shares the same closure scope we don't need to pass the `response` channel in. + +You can put as many `It`s as you wanted into the shared behavior `AssertFailedBehavior` above, and can even nest `It`s within `Context`s inside of `AssertFailedBehavior`. Although it may not always be the best idea to DRY your test suites excessively, this pattern gives you the ability do so as you see fit. One drawback of this approach, however, is that you cannot focus or pend a shared behavior group, or examples/contexts within the group. In other words, you don't get `FAssertFailedBehavior` or `XAssertFailedBehavior` for free. + +#### Pattern 2: Extract functions that return closures, and pass the results to `It`s + +To understand this pattern, let's just redo the above example with this pattern: + +```go +Describe("my api client", func() { + var client APIClient + var fakeServer FakeServer + var response chan APIResponse + + BeforeEach(func() { + response = make(chan APIResponse, 1) + fakeServer = NewFakeServer() + client = NewAPIClient(fakeServer) + client.Get("/some/endpoint", response) + }) + + Describe("failure modes", func() { + AssertNoJSONInResponse := func() func() { + return func() { + Ω((<-response).JSON).Should(BeZero()) + } + } + + AssertDoesNotReportSuccess := func() func() { + return func() { + Ω((<-response).Success).Should(BeFalse()) + } + } + Context("when the server does not return a 200", func() { + BeforeEach(func() { + fakeServer.Respond(404) + }) + + It("should not include JSON in the response", AssertNoJSONInResponse()) + It("should not report success", AssertDoesNotReportSuccess()) + }) + + Context("when the server returns unparseable JSON", func() { + BeforeEach(func() { + fakeServer.Succeed("{I'm not JSON!") + }) + + It("should not include JSON in the response", AssertNoJSONInResponse()) + It("should not report success", AssertDoesNotReportSuccess()) + }) + + Context("when the request errors", func() { + BeforeEach(func() { + fakeServer.Error(errors.New("oops!")) + }) + + It("should not include JSON in the response", AssertNoJSONInResponse()) + It("should not report success", AssertDoesNotReportSuccess()) + }) + }) +}) +``` + +Note that this solution is still very compact, especially because there are only two shared `It`s for each `Context`. There is slightly more repetition here, but it's also slightly more explicit. The main benefit of this pattern is you can focus and pend individual `It`s in individual `Context`s. + +### Global Shared Behaviors + +The patterns outlined above work well when the shared behavior is intended to be used within a fixed scope. If you want to build shared behavior that can be used across different test files (or even different packages) you'll need to tweak the pattern to make it possible to pass inputs in. We can extend both examples outlined above to illustrate how this might work: + +#### Pattern 1 + +```go +package sharedbehaviors + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type FailedResponseBehaviorInputs struct { + response chan APIResponse +} + +func SharedFailedResponseBehavior(inputs *FailedResponseBehaviorInputs) { + It("should not include JSON in the response", func() { + Ω((<-(inputs.response)).JSON).Should(BeZero()) + }) + + It("should not report success", func() { + Ω((<-(inputs.response)).Success).Should(BeFalse()) + }) +} +``` + +#### Pattern 2 + +```go +package sharedbehaviors + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type FailedResponseBehaviorInputs struct { + response chan APIResponse +} + +func AssertNoJSONInResponese(inputs *FailedResponseBehaviorInputs) func() { + return func() { + Ω((<-(inputs.response)).JSON).Should(BeZero()) + } +} + +func AssertDoesNotReportSuccess(inputs *FailedResponseBehaviorInputs) func() { + return func() { + Ω((<-(inputs.response)).Success).Should(BeFalse()) + } +} +``` + +Users of the shared behavior must generate and populate a `FailedResponseBehaviorInputs` and pass it in to either `SharedFailedResponseBehavior` or `AssertNoJSONInResponese` and `AssertDoesNotReportSuccess`. Why do things this way? Two reasons: + +1. Having a stuct to encapsulate the input variables (like `FailedResponseBehaviorInputs`) allows you to clearly stipulate the contract between the the specs and the shared behavior. The shared behavior *needs* these inputs in order to function correctly. + +2. More importantly, inputs like the `response` channel are generally created and/or set in `BeforeEach` blocks. However the shared behavior functions must be called within a container block and will not have access to any variables specified in a `BeforeEach` as the `BeforeEach` hasn't run yet. To get around this, we instantiate a `FailedResponseBehaviorInputs` and pass a pointer to it to the shared behavior functions -- in the `BeforeEach` we manipulate the fields of the `FailedResponseBehaviorInputs`, ensuring that their values get communicated to the `It`s generated by the shared behavior. + +Here's what the calling test would look like after dot-importing the `sharedbehaviors` package (for brevity we'll combine patterns 1 and 2 in this example): + +```go +Describe("my api client", func() { + var client APIClient + var fakeServer FakeServer + var response chan APIResponse + sharedInputs := FailedResponseBehaviorInputs{} + + BeforeEach(func() { + sharedInputs.response = make(chan APIResponse, 1) + fakeServer = NewFakeServer() + client = NewAPIClient(fakeServer) + client.Get("/some/endpoint", sharedInputs.response) + }) + + Describe("failure modes", func() { + Context("when the server does not return a 200", func() { + BeforeEach(func() { + fakeServer.Respond(404) + }) + + // Pattern 1 + SharedFailedResponseBehavior(&sharedInputs) + }) + + Context("when the server returns unparseable JSON", func() { + BeforeEach(func() { + fakeServer.Succeed("{I'm not JSON!") + }) + + // Pattern 2 + It("should not include JSON in the response", AssertNoJSONInResponse(&sharedInputs)) + It("should not report success", AssertDoesNotReportSuccess(&sharedInputs)) + }) + }) +}) +``` + +--- + +## Ginkgo and Continuous Integration + +Ginkgo comes with a number of [flags](#running-tests) that you probably want to turn on when running in a Continuous Integration environment. The following is recommended: + + ginkgo -r --randomizeAllSpecs --randomizeSuites --failOnPending --cover --trace --race --progress + +- `-r` will recursively find and run all spec suites in the current directory +- `--randomizeAllSpecs` and `--randomizeSuites` will shuffle both the order in which specs within a suite run, and the order in which different suites run. This can be *great* for identifying test pollution. You can always rerun a given ordering later by passing the `--seed` flag a matching seed. +- `--failOnPending` causes the test suite to fail if there are any pending tests (typically these should not be committed but should signify work in progress). +- `--cover` generates `.coverprofile`s and coverage statistics for each test suite. +- `--trace` prints out a full stack trace when failures occur. This makes debugging based on CI logs easier. +- `--race` runs the tests with the race detector turned on. +- `--progress` emits test progress to the GinkgoWriter. Makes identifying where failures occur a little easier. + +It is *not* recommended that you run tests in parallel on CI with `-p`. Many CI systems run on multi-core machines that report very many (e.g. 32 nodes). Parallelizing on such a high scale typically yields *longer* test run times (particularly since your tests are probably running inside some sort of cpu-share limited container: you don't actually have free reign of all 32 cores). To run tests in parallel on CI you're probably better off providing an explicit number of parallel nodes with `-nodes`. + +### Sample .travis.yml + +For Travis CI, you could use something like this: + + language: go + go: + - 1.9 + - tip + + install: + - go get -v github.com/onsi/ginkgo/ginkgo + - go get -v github.com/onsi/gomega + - go get -v -t ./... + - export PATH=$PATH:$HOME/gopath/bin + + script: ginkgo -r --randomizeAllSpecs --randomizeSuites --failOnPending --cover --trace --race --compilers=2 + +Note that we've added `--compilers=2` -- this resolves an issue where Travis kills the Ginkgo process early for being too greedy. By default, Ginkgo will run `runtime.NumCPU()` compilers which, on Travis, can be as many as `32` compilers! Similarly, if you want to run your tests in parallel on Travis, make sure to specify `--nodes=N` instead of `-p`. + +--- + +## Extensions + +Ginkgo ships with extensions to the core DSL. These can be (optionally) dot imported to augment Ginkgo's default DSL. + +Currently there is only one extension: the table extension. + +### Table Driven Tests + +The [table](https://godoc.org/github.com/onsi/ginkgo/extensions/table) provides an expressive DSL for writing table driven tests. + +| Attention: if you have ginkgo in your `vendor` directory, be sure to add the package `github.com/onsi/ginkgo/extensions/table` to `vendor`. See [issue 234](https://github.com/onsi/ginkgo/issues/234#issuecomment-196645747) for details. | +| :-------------------- | + +While it's easy to roll your own table driven tests using simple data structures and a for loop, this layer of DSL makes it particularly easy to write and manage table driven tests. + +For example: + +```go +package table_test + +import ( + . "github.com/onsi/ginkgo/extensions/table" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Math", func() { + DescribeTable("the > inequality", + func(x int, y int, expected bool) { + Expect(x > y).To(Equal(expected)) + }, + Entry("x > y", 1, 0, true), + Entry("x == y", 0, 0, false), + Entry("x < y", 0, 1, false), + ) +}) +``` + +> In this example we dot import the table extension. This isn't strictly necessary but makes the DSL easier to interact with. + +Let's break this down `DescribeTable` takes a description, a function to run for each test case, and a set of table entries. + +The function you pass in to `DescribeTable` can accept arbitrary arguments. The parameters passed in to the individual `Entry` calls will be passed in to the function (type mismatches will result in a runtime panic). + +The indiviudal `Entry` calls construct a `TableEntry` that is passed into `DescribeTable`. A `TableEntry` consists of a description (the first call to `Entry`) and an arbitrary set of parameters to be passed into the function registered with `DescribeTable`. + +It's important to understand the life-cycle of the table. The `table` package is a thin wrapper around Ginkgo's DSL. `DescribeTable` generates a single Ginkgo `Describe`, within this `Describe` each `Entry` generates a Ginkgo `It`. This all happens *before* the tests run (at "testing tree construction time"). The result is that the table expands into a number of `It`s (one for each `Entry`) that are subject to all of Ginkgo's test-running semantics: `It`s can be randomized and parallelized across multiple nodes. + +To be clear, the above test is *exactly* equivalent to: + +```go +package table_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Math", func() { + Describe("the > inequality", + It("x > y", func() { + Expect(1 > 0).To(Equal(true)) + }) + + It("x == y", func() { + Expect(0 > 0).To(Equal(false)) + }) + + It("x < y", func() { + Expect(0 > 1).To(Equal(false)) + }) + ) +}) +``` + +You should be aware of the Ginkgo test lifecycle - particularly around [dynamically generating tests](#patterns-for-dynamically-generating-tests) - when using `DescribeTable`. + +#### Focusing and Pending Tables and Entries + +Here's the cool part. Entire tables can be focused or marked pending by simply swapping out `DescribeTable` with `FDescribeTable` (to focus) or `PDescribeTable` (to mark pending). + +Similarly, individual entries can be focused/pended out with `FEntry` and `PEntry`. This is particularly useful when debugging tests. + +#### Managing Complex Parameters + +While passing arbitrary parameters to `Entry` is convenient it can make the test cases difficult to parse at a glance. For more complex tables it may make more sense to define a new type and pass it around instead. For example: + +```go +package table_test + +import ( + . "github.com/onsi/ginkgo/extensions/table" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Substring matching", func() { + type SubstringCase struct { + String string + Substring string + Count int + } + + DescribeTable("counting substring matches", + func(c SubstringCase) { + Ω(strings.Count(c.String, c.Substring)).Should(BeNumerically("==", c.Count)) + }, + Entry("with no matching substring", SubstringCase{ + String: "the sixth sheikh's sixth sheep's sick", + Substring: "emir", + Count: 0, + }), + Entry("with one matching substring", SubstringCase{ + String: "the sixth sheikh's sixth sheep's sick", + Substring: "sheep", + Count: 1, + }), + Entry("with many matching substring", SubstringCase{ + String: "the sixth sheikh's sixth sheep's sick", + Substring: "si", + Count: 3, + }), + ) +}) +``` + +Note that this pattern uses the same DSL, it's simply a way to manage the parameters flowing between the `Entry` cases and the callback registered with `DescribeTable`. + +#### Custom Entry Description + +There are some scenarios where having the parameters as part of the description helps in understanding what a given test is about. +Instead of needing to add the parameters to each different description, `Entry` support passing it a helper function that will be fed with the `Entry` parameters and should return a description related to those parameters. + +For example: + +```go +package table_test + +import ( + . "github.com/onsi/ginkgo/extensions/table" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) +var _ = Describe("TableWithParametricDescription", func() { + describe := func(desc string) func(int, int, bool) string { + return func(x, y int, expected bool) string { + return fmt.Sprintf("%s x=%d y=%d expected:%t", desc, x, y, expected) + } + } + + DescribeTable("a simple table", + func(x int, y int, expected bool) { + Ω(x > y).Should(Equal(expected)) + }, + Entry(describe("x > y"), 1, 0, true), + Entry(describe("x == y"), 0, 0, false), + Entry(describe("x < y"), 0, 1, false), + ) +} + +``` + +In this case, the description of each `It` the entries are translated to is generated by the `describe` function passed to each `Entry`. + +--- + +## Writing Custom Reporters + +While Ginkgo's default reporter offers a comprehensive set of features, Ginkgo makes it easy to write and run multiple custom reporters at once. There are many usecases for this - you might implement a custom reporter to support a special output format for your CI setup, or you might implement a custom reporter to [aggregate data](#measuring-time) from Ginkgo's `Measure` nodes and produce HTML or CSV reports (or even plots!) + +In Ginkgo a reporter must satisfy the `Reporter` interface: + +```go +type Reporter interface { + SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) + BeforeSuiteDidRun(setupSummary *types.SetupSummary) + SpecWillRun(specSummary *types.SpecSummary) + SpecDidComplete(specSummary *types.SpecSummary) + AfterSuiteDidRun(setupSummary *types.SetupSummary) + SpecSuiteDidEnd(summary *types.SuiteSummary) +} +``` + +The method names should be self-explanatory. Be sure to dig into the `SuiteSummary` and `SpecSummary` objects to get a sense of what data is available to your reporter. If you're writing a custom reporter to ingest benchmarking data generated by `Measure` nodes you'll want to look at the `ExampleMeasurement` struct that is provided by `ExampleSummary.Measurements`. + +Once you've created your custom reporter you may pass an instance of it to Ginkgo by replacing the `RunSpecs` command in your test suite bootstrap with either: + +```go +RunSpecsWithDefaultAndCustomReporters(t *testing.T, description string, reporters []Reporter) +``` + +or + +```go +RunSpecsWithCustomReporters(t *testing.T, description string, reporters []Reporter) +``` + +`RunSpecsWithDefaultAndCustomReporters` will run your custom reporters alongside Ginkgo's default reporter. `RunSpecsWithCustomReporters` will only run the custom reporters you pass in. + +If you wish to run your tests in parallel you should not use `RunSpecsWithCustomReporters` as the default reporter plays an important role in streaming test output to the ginkgo CLI. + +--- + +## Third Party Integrations + +### Using Other Matcher Libraries + +Most matcher library accept the `*testing.T` object. Unfortunately, since this is a concrete type is can be tricky to pass in an equivalent that will work with Ginkgo. + +It is, typically, not difficult to replace `*testing.T` in such libraries with an interface that `*testing.T` satisfies. For example [testify](https://github.com/stretchr/testify) accepts `t` via an interface. In such cases you can pass `GinkgoT()`. This generates an object that mimics `*testing.T` and communicates to Ginkgo directly. + +For example, to get testify working: + +```go +package foo_test + +import ( + . "github.com/onsi/ginkgo" + + "github.com/stretchr/testify/assert" +) + +var _ = Describe(func("foo") { + It("should testify to its correctness", func(){ + assert.Equal(GinkgoT(), foo{}.Name(), "foo") + }) +}) +``` + +> Note that passing the `*testing.T` from Ginkgo's bootstrap `Test...()` function will cause the suite to abort as soon as the first failure is encountered. Don't do this. You need to communicate failure to Ginkgo's single (global) `Fail` function + +### Integrating with Gomock + +Ginkgo does not provide a mocking/stubbing framework. It's the author's opinion that mocks and stubs can be avoided completely by embracing dependency injection and always injecting Go interfaces. Real dependencies are then injected in production code, and fake dependencies are injected under test. Building and maintaining such fakes tends to be straightforward and can allow for clearer and more expressive tests than mocks. + +With that said, it is relatively straightforward to use a mocking framework such as [Gomock](https://code.google.com/p/gomock/). `GinkgoT()` implements Gomock's `TestReporter` interface. Here's how you use it (for example): + +```go +import ( + "code.google.com/p/gomock/gomock" + + . github.com/onsi/ginkgo + . github.com/onsi/gomega +) + +var _ = Describe("Consumer", func() { + var ( + mockCtrl *gomock.Controller + mockThing *mockthing.MockThing + consumer *Consumer + ) + + BeforeEach(func() { + mockCtrl = gomock.NewController(GinkgoT()) + mockThing = mockthing.NewMockThing(mockCtrl) + consumer = NewConsumer(mockThing) + }) + + AfterEach(func() { + mockCtrl.Finish() + }) + + It("should consume things", func() { + mockThing.EXPECT().OmNom() + consumer.Consume() + }) +}) +``` + +When using Gomock you may want to run `ginkgo` with the `-trace` flag to print out stack traces for failures which will help you trace down where, in your code, invalid calls occured. + +### Generating JUnit XML Output + +Ginkgo provides a [custom reporter](#writing-custom-reporters) for generating JUnit compatible XML output. Here's a sample bootstrap file that instantiates a JUnit reporter and passes it to the test runner: + +```go +package foo_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/reporters" + "testing" +) + +func TestFoo(t *testing.T) { + RegisterFailHandler(Fail) + junitReporter := reporters.NewJUnitReporter("junit.xml") + RunSpecsWithDefaultAndCustomReporters(t, "Foo Suite", []Reporter{junitReporter}) +} +``` + +This will generate a file name "junit.xml" in the directory containing your test. This xml file is compatible with the latest version of the Jenkins JUnit plugin. + +If you want to run your tests in parallel you'll need to make your JUnit xml filename a function of the parallel node number. You can do this like so: + + junitReporter := reporters.NewJUnitReporter(fmt.Sprintf("junit_%d.xml", config.GinkgoConfig.ParallelNode)) + +Note that you'll need to import `fmt` and `github.com/onsi/ginkgo/config` to get this to work. This will generate an xml file for each parallel node. The Jenkins JUnit plugin (for example) automatically aggregates data from across all these files. \ No newline at end of file diff --git a/extensions/table/table.go b/extensions/table/table.go index 4b00278073..9420105c44 100644 --- a/extensions/table/table.go +++ b/extensions/table/table.go @@ -12,7 +12,7 @@ import ( "fmt" "reflect" - "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/internal" "github.com/onsi/ginkgo/internal/global" "github.com/onsi/ginkgo/types" ) @@ -63,7 +63,7 @@ For example: ) */ func DescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { - describeTable(description, itBody, entries, types.FlagTypeNone) + describeTable(description, itBody, entries, false, false) return true } @@ -71,7 +71,7 @@ func DescribeTable(description string, itBody interface{}, entries ...TableEntry You can focus a table with `FDescribeTable`. This is equivalent to `FDescribe`. */ func FDescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { - describeTable(description, itBody, entries, types.FlagTypeFocused) + describeTable(description, itBody, entries, true, false) return true } @@ -79,7 +79,7 @@ func FDescribeTable(description string, itBody interface{}, entries ...TableEntr You can mark a table as pending with `PDescribeTable`. This is equivalent to `PDescribe`. */ func PDescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { - describeTable(description, itBody, entries, types.FlagTypePending) + describeTable(description, itBody, entries, false, true) return true } @@ -87,24 +87,19 @@ func PDescribeTable(description string, itBody interface{}, entries ...TableEntr You can mark a table as pending with `XDescribeTable`. This is equivalent to `XDescribe`. */ func XDescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { - describeTable(description, itBody, entries, types.FlagTypePending) + describeTable(description, itBody, entries, false, true) return true } -func describeTable(description string, itBody interface{}, entries []TableEntry, flag types.FlagType) { +func describeTable(description string, itBody interface{}, entries []TableEntry, markedFocus bool, markedPending bool) { itBodyValue := reflect.ValueOf(itBody) if itBodyValue.Kind() != reflect.Func { panic(fmt.Sprintf("DescribeTable expects a function, got %#v", itBody)) } - global.Suite.PushContainerNode( - description, - func() { - for _, entry := range entries { - entry.generateIt(itBodyValue) - } - }, - flag, - codelocation.New(2), - ) + global.Suite.PushNode(internal.NewNode(types.NodeTypeContainer, description, func() { + for _, entry := range entries { + entry.generateIt(itBodyValue) + } + }, types.NewCodeLocation(2), markedFocus, markedPending)) } diff --git a/extensions/table/table_entry.go b/extensions/table/table_entry.go index 4d9c237ad7..70e27cf1fc 100644 --- a/extensions/table/table_entry.go +++ b/extensions/table/table_entry.go @@ -4,7 +4,7 @@ import ( "fmt" "reflect" - "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/internal" "github.com/onsi/ginkgo/internal/global" "github.com/onsi/ginkgo/types" ) @@ -41,7 +41,7 @@ func (t TableEntry) generateIt(itBody reflect.Value) { } if t.Pending { - global.Suite.PushItNode(description, func() {}, types.FlagTypePending, t.codeLocation, 0) + global.Suite.PushNode(internal.NewNode(types.NodeTypeIt, description, func() {}, t.codeLocation, false, true)) return } @@ -50,11 +50,7 @@ func (t TableEntry) generateIt(itBody reflect.Value) { itBody.Call(values) } - if t.Focused { - global.Suite.PushItNode(description, body, types.FlagTypeFocused, t.codeLocation, global.DefaultTimeout) - } else { - global.Suite.PushItNode(description, body, types.FlagTypeNone, t.codeLocation, global.DefaultTimeout) - } + global.Suite.PushNode(internal.NewNode(types.NodeTypeIt, description, body, t.codeLocation, t.Focused, false)) } func castParameters(function reflect.Value, parameters []interface{}) []reflect.Value { @@ -85,7 +81,7 @@ func Entry(description interface{}, parameters ...interface{}) TableEntry { Parameters: parameters, Pending: false, Focused: false, - codeLocation: codelocation.New(1), + codeLocation: types.NewCodeLocation(1), } } @@ -98,7 +94,7 @@ func FEntry(description interface{}, parameters ...interface{}) TableEntry { Parameters: parameters, Pending: false, Focused: true, - codeLocation: codelocation.New(1), + codeLocation: types.NewCodeLocation(1), } } @@ -111,7 +107,7 @@ func PEntry(description interface{}, parameters ...interface{}) TableEntry { Parameters: parameters, Pending: true, Focused: false, - codeLocation: codelocation.New(1), + codeLocation: types.NewCodeLocation(1), } } @@ -124,6 +120,6 @@ func XEntry(description interface{}, parameters ...interface{}) TableEntry { Parameters: parameters, Pending: true, Focused: false, - codeLocation: codelocation.New(1), + codeLocation: types.NewCodeLocation(1), } } diff --git a/reporters/stenographer/support/go-colorable/LICENSE b/formatter/colorable_others.go similarity index 76% rename from reporters/stenographer/support/go-colorable/LICENSE rename to formatter/colorable_others.go index 91b5cef30e..778bfd7c7c 100644 --- a/reporters/stenographer/support/go-colorable/LICENSE +++ b/formatter/colorable_others.go @@ -1,3 +1,11 @@ +// +build !windows + +/* +These packages are used for colorize on Windows and contributed by mattn.jp@gmail.com + + * go-colorable: + * go-isatty: + The MIT License (MIT) Copyright (c) 2016 Yasuhiro Matsumoto @@ -19,3 +27,15 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package formatter + +import ( + "io" + "os" +) + +func newColorable(file *os.File) io.Writer { + return file +} diff --git a/reporters/stenographer/support/go-colorable/colorable_windows.go b/formatter/colorable_windows.go similarity index 90% rename from reporters/stenographer/support/go-colorable/colorable_windows.go rename to formatter/colorable_windows.go index 1088009230..80a3e66573 100644 --- a/reporters/stenographer/support/go-colorable/colorable_windows.go +++ b/formatter/colorable_windows.go @@ -1,4 +1,33 @@ -package colorable +/* +These packages are used for colorize on Windows and contributed by mattn.jp@gmail.com + + * go-colorable: + * go-isatty: + +The MIT License (MIT) + +Copyright (c) 2016 Yasuhiro Matsumoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package formatter import ( "bytes" @@ -11,9 +40,26 @@ import ( "syscall" "unsafe" - "github.com/onsi/ginkgo/reporters/stenographer/support/go-isatty" + "syscall" + "unsafe" ) +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") + procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") + procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") + procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") + procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") +) + +func isTerminal(fd uintptr) bool { + var st uint32 + r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) + return r != 0 && e == 0 +} + const ( foregroundBlue = 0x1 foregroundGreen = 0x2 @@ -52,45 +98,28 @@ type consoleScreenBufferInfo struct { maximumWindowSize coord } -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") - procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") - procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") - procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") - procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") -) - -type Writer struct { +type writer struct { out io.Writer handle syscall.Handle lastbuf bytes.Buffer oldattr word } -func NewColorable(file *os.File) io.Writer { +func newColorable(file *os.File) io.Writer { if file == nil { panic("nil passed instead of *os.File to NewColorable()") } - if isatty.IsTerminal(file.Fd()) { + if isTerminal(file.Fd()) { var csbi consoleScreenBufferInfo handle := syscall.Handle(file.Fd()) procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - return &Writer{out: file, handle: handle, oldattr: csbi.attributes} + return &writer{out: file, handle: handle, oldattr: csbi.attributes} } else { return file } } -func NewColorableStdout() io.Writer { - return NewColorable(os.Stdout) -} - -func NewColorableStderr() io.Writer { - return NewColorable(os.Stderr) -} - var color256 = map[int]int{ 0: 0x000000, 1: 0x800000, @@ -350,7 +379,7 @@ var color256 = map[int]int{ 255: 0xeeeeee, } -func (w *Writer) Write(data []byte) (n int, err error) { +func (w *writer) Write(data []byte) (n int, err error) { var csbi consoleScreenBufferInfo procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) diff --git a/formatter/formatter.go b/formatter/formatter.go index 30d7cbe129..237dd9b178 100644 --- a/formatter/formatter.go +++ b/formatter/formatter.go @@ -2,10 +2,15 @@ package formatter import ( "fmt" + "os" "regexp" "strings" ) +// ColorableStdOut and ColorableStdErr enable color output support on Windows +var ColorableStdOut = newColorable(os.Stdout) +var ColorableStdErr = newColorable(os.Stderr) + const COLS = 80 type ColorMode uint8 diff --git a/ginkgo/testsuite/testsuite_suite_test.go b/formatter/formatter_suite_test.go similarity index 55% rename from ginkgo/testsuite/testsuite_suite_test.go rename to formatter/formatter_suite_test.go index d1e8b21d37..d67654339c 100644 --- a/ginkgo/testsuite/testsuite_suite_test.go +++ b/formatter/formatter_suite_test.go @@ -1,13 +1,13 @@ -package testsuite_test +package formatter_test import ( + "testing" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - - "testing" ) -func TestTestsuite(t *testing.T) { +func TestFormatter(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Testsuite Suite") + RunSpecs(t, "Formatter Suite") } diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go new file mode 100644 index 0000000000..934f398721 --- /dev/null +++ b/formatter/formatter_test.go @@ -0,0 +1,88 @@ +package formatter_test + +import ( + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/formatter" + . "github.com/onsi/gomega" +) + +var _ = Describe("Formatter", func() { + var colorMode formatter.ColorMode + var f formatter.Formatter + + BeforeEach(func() { + colorMode = formatter.ColorModeTerminal + }) + + JustBeforeEach(func() { + f = formatter.New(colorMode) + }) + + Context("with ColorModeNone", func() { + BeforeEach(func() { + colorMode = formatter.ColorModeNone + }) + + It("strips out color information", func() { + Ω(f.F("{{green}}{{bold}}hi there{{/}}")).Should(Equal("hi there")) + }) + }) + + Context("with ColorModeTerminal", func() { + BeforeEach(func() { + colorMode = formatter.ColorModeTerminal + }) + + It("renders the color information using terminal escape codes", func() { + Ω(f.F("{{green}}{{bold}}hi there{{/}}")).Should(Equal("\x1b[38;5;10m\x1b[1mhi there\x1b[0m")) + }) + }) + + Context("with ColorModePassthrough", func() { + BeforeEach(func() { + colorMode = formatter.ColorModePassthrough + }) + + It("leaves the color information as is, allowing us to test statements more easily", func() { + Ω(f.F("{{green}}{{bold}}hi there{{/}}")).Should(Equal("{{green}}{{bold}}hi there{{/}}")) + }) + }) + + Describe("NewWithNoColorBool", func() { + Context("when the noColor bool is true", func() { + It("strips out color information", func() { + f = formatter.NewWithNoColorBool(true) + Ω(f.F("{{green}}{{bold}}hi there{{/}}")).Should(Equal("hi there")) + }) + }) + + Context("when the noColor bool is false", func() { + It("renders the color information using terminal escape codes", func() { + f = formatter.NewWithNoColorBool(false) + Ω(f.F("{{green}}{{bold}}hi there{{/}}")).Should(Equal("\x1b[38;5;10m\x1b[1mhi there\x1b[0m")) + }) + }) + }) + + Describe("F", func() { + It("transforms the color information and sprintfs", func() { + Ω(f.F("{{green}}hi there {{cyan}}%d {{yellow}}%s{{/}}", 3, "wise men")).Should(Equal("\x1b[38;5;10mhi there \x1b[38;5;14m3 \x1b[38;5;11mwise men\x1b[0m")) + }) + }) + + Describe("Fi", func() { + It("transforms the color information, sprintfs, and applies an indentation", func() { + Ω(f.Fi(2, "{{green}}hi there\n{{cyan}}%d {{yellow}}%s{{/}}", 3, "wise men")).Should(Equal( + " \x1b[38;5;10mhi there\n \x1b[38;5;14m3 \x1b[38;5;11mwise men\x1b[0m", + )) + }) + }) + + Describe("CycleJoin", func() { + It("combines elements, cycling through styles as it goes", func() { + Ω(f.CycleJoin([]string{"a", "b", "c"}, "|", []string{"{{red}}", "{{green}}"})).Should(Equal( + "\x1b[38;5;9ma|\x1b[38;5;10mb|\x1b[38;5;9mc\x1b[0m", + )) + }) + }) +}) diff --git a/ginkgo/bootstrap_command.go b/ginkgo/bootstrap_command.go deleted file mode 100644 index 6f5af39134..0000000000 --- a/ginkgo/bootstrap_command.go +++ /dev/null @@ -1,200 +0,0 @@ -package main - -import ( - "bytes" - "flag" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "text/template" - - "go/build" - - sprig "github.com/go-task/slim-sprig" - "github.com/onsi/ginkgo/ginkgo/nodot" -) - -func BuildBootstrapCommand() *Command { - var ( - agouti, noDot, internal bool - customBootstrapFile string - ) - flagSet := flag.NewFlagSet("bootstrap", flag.ExitOnError) - flagSet.BoolVar(&agouti, "agouti", false, "If set, bootstrap will generate a bootstrap file for writing Agouti tests") - flagSet.BoolVar(&noDot, "nodot", false, "If set, bootstrap will generate a bootstrap file that does not . import ginkgo and gomega") - flagSet.BoolVar(&internal, "internal", false, "If set, generate will generate a test file that uses the regular package name") - flagSet.StringVar(&customBootstrapFile, "template", "", "If specified, generate will use the contents of the file passed as the bootstrap template") - - return &Command{ - Name: "bootstrap", - FlagSet: flagSet, - UsageCommand: "ginkgo bootstrap ", - Usage: []string{ - "Bootstrap a test suite for the current package", - "Accepts the following flags:", - }, - Command: func(args []string, additionalArgs []string) { - generateBootstrap(agouti, noDot, internal, customBootstrapFile) - }, - } -} - -var bootstrapText = `package {{.Package}} - -import ( - "testing" - - {{.GinkgoImport}} - {{.GomegaImport}} -) - -func Test{{.FormattedName}}(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "{{.FormattedName}} Suite") -} -` - -var agoutiBootstrapText = `package {{.Package}} - -import ( - "testing" - - {{.GinkgoImport}} - {{.GomegaImport}} - "github.com/sclevine/agouti" -) - -func Test{{.FormattedName}}(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "{{.FormattedName}} Suite") -} - -var agoutiDriver *agouti.WebDriver - -var _ = BeforeSuite(func() { - // Choose a WebDriver: - - agoutiDriver = agouti.PhantomJS() - // agoutiDriver = agouti.Selenium() - // agoutiDriver = agouti.ChromeDriver() - - Expect(agoutiDriver.Start()).To(Succeed()) -}) - -var _ = AfterSuite(func() { - Expect(agoutiDriver.Stop()).To(Succeed()) -}) -` - -type bootstrapData struct { - Package string - FormattedName string - GinkgoImport string - GomegaImport string -} - -func getPackageAndFormattedName() (string, string, string) { - path, err := os.Getwd() - if err != nil { - complainAndQuit("Could not get current working directory: \n" + err.Error()) - } - - dirName := strings.Replace(filepath.Base(path), "-", "_", -1) - dirName = strings.Replace(dirName, " ", "_", -1) - - pkg, err := build.ImportDir(path, 0) - packageName := pkg.Name - if err != nil { - packageName = dirName - } - - formattedName := prettifyPackageName(filepath.Base(path)) - return packageName, dirName, formattedName -} - -func prettifyPackageName(name string) string { - name = strings.Replace(name, "-", " ", -1) - name = strings.Replace(name, "_", " ", -1) - name = strings.Title(name) - name = strings.Replace(name, " ", "", -1) - return name -} - -func determinePackageName(name string, internal bool) string { - if internal { - return name - } - - return name + "_test" -} - -func fileExists(path string) bool { - _, err := os.Stat(path) - return err == nil -} - -func generateBootstrap(agouti, noDot, internal bool, customBootstrapFile string) { - packageName, bootstrapFilePrefix, formattedName := getPackageAndFormattedName() - data := bootstrapData{ - Package: determinePackageName(packageName, internal), - FormattedName: formattedName, - GinkgoImport: `. "github.com/onsi/ginkgo"`, - GomegaImport: `. "github.com/onsi/gomega"`, - } - - if noDot { - data.GinkgoImport = `"github.com/onsi/ginkgo"` - data.GomegaImport = `"github.com/onsi/gomega"` - } - - targetFile := fmt.Sprintf("%s_suite_test.go", bootstrapFilePrefix) - if fileExists(targetFile) { - fmt.Printf("%s already exists.\n\n", targetFile) - os.Exit(1) - } else { - fmt.Printf("Generating ginkgo test suite bootstrap for %s in:\n\t%s\n", packageName, targetFile) - } - - f, err := os.Create(targetFile) - if err != nil { - complainAndQuit("Could not create file: " + err.Error()) - panic(err.Error()) - } - defer f.Close() - - var templateText string - if customBootstrapFile != "" { - tpl, err := ioutil.ReadFile(customBootstrapFile) - if err != nil { - panic(err.Error()) - } - templateText = string(tpl) - } else if agouti { - templateText = agoutiBootstrapText - } else { - templateText = bootstrapText - } - - bootstrapTemplate, err := template.New("bootstrap").Funcs(sprig.TxtFuncMap()).Parse(templateText) - if err != nil { - panic(err.Error()) - } - - buf := &bytes.Buffer{} - bootstrapTemplate.Execute(buf, data) - - if noDot { - contents, err := nodot.ApplyNoDot(buf.Bytes()) - if err != nil { - complainAndQuit("Failed to import nodot declarations: " + err.Error()) - } - fmt.Println("To update the nodot declarations in the future, switch to this directory and run:\n\tginkgo nodot") - buf = bytes.NewBuffer(contents) - } - - buf.WriteTo(f) - - goFmt(targetFile) -} diff --git a/ginkgo/build/build_command.go b/ginkgo/build/build_command.go new file mode 100644 index 0000000000..a75a25170f --- /dev/null +++ b/ginkgo/build/build_command.go @@ -0,0 +1,57 @@ +package build + +import ( + "fmt" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/command" + "github.com/onsi/ginkgo/ginkgo/internal" +) + +func BuildBuildCommand() command.Command { + var cliConfig = config.NewDefaultGinkgoCLIConfig() + var goFlagsConfig = config.NewDefaultGoFlagsConfig() + + flags, err := config.BuildBuildCommandFlagSet(&cliConfig, &goFlagsConfig) + if err != nil { + panic(err) + } + + return command.Command{ + Name: "build", + Flags: flags, + Usage: "ginkgo build ", + ShortDoc: "Build the passed in (or the package in the current directory if left blank).", + DocLink: "precompiling-tests", + Command: func(args []string, _ []string) { + var errors []error + cliConfig, goFlagsConfig, errors = config.VetAndInitializeCLIAndGoConfig(cliConfig, goFlagsConfig) + command.AbortIfErrors("Ginkgo detected configuraiotn issues:", errors) + + buildSpecs(args, cliConfig, goFlagsConfig) + }, + } +} + +func buildSpecs(args []string, cliConfig config.GinkgoCLIConfigType, goFlagsConfig config.GoFlagsConfigType) { + suites, _ := internal.FindSuites(args, cliConfig, false) + if len(suites) == 0 { + command.AbortWith("Found no test suites") + } + + passed := true + for _, suite := range suites { + fmt.Printf("Compiling %s...\n", suite.PackageName) + suite = internal.CompileSuite(suite, goFlagsConfig) + if suite.CompilationError != nil { + fmt.Println(suite.CompilationError.Error()) + passed = false + } else { + fmt.Printf(" compiled %s.test\n", suite.PackageName) + } + } + + if !passed { + command.AbortWith("Failed to compile all tests") + } +} diff --git a/ginkgo/build_command.go b/ginkgo/build_command.go deleted file mode 100644 index 2fddef0f7b..0000000000 --- a/ginkgo/build_command.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - "path/filepath" - - "github.com/onsi/ginkgo/ginkgo/interrupthandler" - "github.com/onsi/ginkgo/ginkgo/testrunner" -) - -func BuildBuildCommand() *Command { - commandFlags := NewBuildCommandFlags(flag.NewFlagSet("build", flag.ExitOnError)) - interruptHandler := interrupthandler.NewInterruptHandler() - builder := &SpecBuilder{ - commandFlags: commandFlags, - interruptHandler: interruptHandler, - } - - return &Command{ - Name: "build", - FlagSet: commandFlags.FlagSet, - UsageCommand: "ginkgo build ", - Usage: []string{ - "Build the passed in (or the package in the current directory if left blank).", - "Accepts the following flags:", - }, - Command: builder.BuildSpecs, - } -} - -type SpecBuilder struct { - commandFlags *RunWatchAndBuildCommandFlags - interruptHandler *interrupthandler.InterruptHandler -} - -func (r *SpecBuilder) BuildSpecs(args []string, additionalArgs []string) { - r.commandFlags.computeNodes() - - suites, _ := findSuites(args, r.commandFlags.Recurse, r.commandFlags.SkipPackage, false) - - if len(suites) == 0 { - complainAndQuit("Found no test suites") - } - - passed := true - for _, suite := range suites { - runner := testrunner.New(suite, 1, false, 0, r.commandFlags.GoOpts, nil) - fmt.Printf("Compiling %s...\n", suite.PackageName) - - path, _ := filepath.Abs(filepath.Join(suite.Path, fmt.Sprintf("%s.test", suite.PackageName))) - err := runner.CompileTo(path) - if err != nil { - fmt.Println(err.Error()) - passed = false - } else { - fmt.Printf(" compiled %s.test\n", suite.PackageName) - } - } - - if passed { - os.Exit(0) - } - os.Exit(1) -} diff --git a/ginkgo/command/abort.go b/ginkgo/command/abort.go new file mode 100644 index 0000000000..6f3542b8e7 --- /dev/null +++ b/ginkgo/command/abort.go @@ -0,0 +1,53 @@ +package command + +import "fmt" + +type AbortDetails struct { + ExitCode int + Error error + EmitUsage bool +} + +func Abort(details AbortDetails) { + panic(details) +} + +func AbortWith(format string, args ...interface{}) { + Abort(AbortDetails{ + ExitCode: 1, + Error: fmt.Errorf(format, args...), + EmitUsage: false, + }) +} + +func AbortWithUsage(format string, args ...interface{}) { + Abort(AbortDetails{ + ExitCode: 1, + Error: fmt.Errorf(format, args...), + EmitUsage: true, + }) +} + +func AbortIfError(preamble string, err error) { + if err != nil { + Abort(AbortDetails{ + ExitCode: 1, + Error: fmt.Errorf("%s\n%s", preamble, err.Error()), + EmitUsage: false, + }) + } +} + +func AbortIfErrors(preamble string, errors []error) { + if len(errors) > 0 { + out := "" + for _, err := range errors { + out += err.Error() + } + Abort(AbortDetails{ + ExitCode: 1, + Error: fmt.Errorf("%s\n%s", preamble, out), + EmitUsage: false, + }) + } +} diff --git a/ginkgo/command/abort_test.go b/ginkgo/command/abort_test.go new file mode 100644 index 0000000000..5b229bd36a --- /dev/null +++ b/ginkgo/command/abort_test.go @@ -0,0 +1,91 @@ +package command_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/ginkgo/command" +) + +var _ = Describe("Abort", func() { + It("panics when called", func() { + details := command.AbortDetails{ + ExitCode: 1, + Error: fmt.Errorf("boom"), + EmitUsage: true, + } + Ω(func() { + command.Abort(details) + }).Should(PanicWith(details)) + }) + + Describe("AbortWith", func() { + It("aborts with a formatted error", func() { + Ω(func() { + command.AbortWith("boom %d %s", 17, "bam!") + }).Should(PanicWith(command.AbortDetails{ + ExitCode: 1, + Error: fmt.Errorf("boom 17 bam!"), + EmitUsage: false, + })) + }) + }) + + Describe("AbortWithUsage", func() { + It("aborts with a formatted error and sets usage to true", func() { + Ω(func() { + command.AbortWithUsage("boom %d %s", 17, "bam!") + }).Should(PanicWith(command.AbortDetails{ + ExitCode: 1, + Error: fmt.Errorf("boom 17 bam!"), + EmitUsage: true, + })) + }) + }) + + Describe("AbortIfError", func() { + Context("with a nil error", func() { + It("does not abort", func() { + Ω(func() { + command.AbortIfError("boom boom?", nil) + }).ShouldNot(Panic()) + }) + }) + + Context("with a non-nil error", func() { + It("does aborts, tacking on the message", func() { + Ω(func() { + command.AbortIfError("boom boom?", fmt.Errorf("kaboom!")) + }).Should(PanicWith(command.AbortDetails{ + ExitCode: 1, + Error: fmt.Errorf("boom boom?\nkaboom!"), + EmitUsage: false, + })) + }) + }) + }) + + Describe("AbortIfErrors", func() { + Context("with an empty errors", func() { + It("does not abort", func() { + Ω(func() { + command.AbortIfErrors("boom boom?", []error{}) + }).ShouldNot(Panic()) + }) + }) + + Context("with non-nil errors", func() { + It("does aborts, tacking on the messages", func() { + Ω(func() { + command.AbortIfErrors("boom boom?", []error{fmt.Errorf("kaboom!\n"), fmt.Errorf("kababoom!!\n")}) + }).Should(PanicWith(command.AbortDetails{ + ExitCode: 1, + Error: fmt.Errorf("boom boom?\nkaboom!\nkababoom!!\n"), + EmitUsage: false, + })) + }) + }) + }) +}) diff --git a/ginkgo/command/command.go b/ginkgo/command/command.go new file mode 100644 index 0000000000..a1d8a55d7b --- /dev/null +++ b/ginkgo/command/command.go @@ -0,0 +1,50 @@ +package command + +import ( + "fmt" + "io" + "strings" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/formatter" +) + +type Command struct { + Name string + Flags config.GinkgoFlagSet + Usage string + ShortDoc string + Documentation string + DocLink string + Command func(args []string, additionalArgs []string) +} + +func (c Command) Run(args []string, additionalArgs []string) { + args, err := c.Flags.Parse(args) + if err != nil { + AbortWithUsage(err.Error()) + } + + c.Command(args, additionalArgs) +} + +func (c Command) EmitUsage(writer io.Writer) { + fmt.Fprintln(writer, formatter.F("{{bold}}"+c.Usage+"{{/}}")) + fmt.Fprintln(writer, formatter.F("{{gray}}%s{{/}}", strings.Repeat("-", len(c.Usage)))) + if c.ShortDoc != "" { + fmt.Fprintln(writer, formatter.Fiw(0, formatter.COLS, c.ShortDoc)) + fmt.Fprintln(writer, "") + } + if c.Documentation != "" { + fmt.Fprintln(writer, formatter.Fiw(0, formatter.COLS, c.Documentation)) + fmt.Fprintln(writer, "") + } + if c.DocLink != "" { + fmt.Fprintln(writer, formatter.Fi(0, "{{bold}}Learn more at:{{/}} {{cyan}}{{underline}}http://onsi.github.io/ginkgo/#%s{{/}}", c.DocLink)) + fmt.Fprintln(writer, "") + } + flagUsage := c.Flags.Usage() + if flagUsage != "" { + fmt.Fprintf(writer, formatter.F(flagUsage)) + } +} diff --git a/internal/failer/failer_suite_test.go b/ginkgo/command/command_suite_test.go similarity index 56% rename from internal/failer/failer_suite_test.go rename to ginkgo/command/command_suite_test.go index 8dce7be9ac..1b82d0e829 100644 --- a/internal/failer/failer_suite_test.go +++ b/ginkgo/command/command_suite_test.go @@ -1,13 +1,13 @@ -package failer_test +package command_test import ( + "testing" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - - "testing" ) -func TestFailer(t *testing.T) { +func TestCommand(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Failer Suite") + RunSpecs(t, "Command Suite") } diff --git a/ginkgo/command/command_test.go b/ginkgo/command/command_test.go new file mode 100644 index 0000000000..ab28cf617b --- /dev/null +++ b/ginkgo/command/command_test.go @@ -0,0 +1,94 @@ +package command_test + +import ( + "strings" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/ginkgo/command" + . "github.com/onsi/ginkgo/internal/test_helpers" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gstruct" +) + +var _ = Describe("Command", func() { + var c command.Command + var rt *RunTracker + + BeforeEach(func() { + rt = NewRunTracker() + fs, err := config.NewGinkgoFlagSet( + config.GinkgoFlags{ + {Name: "contrabulaturally", KeyPath: "C", Usage: "with irridiacy"}, + }, + &(struct{ C int }{C: 17}), + config.GinkgoFlagSections{}, + ) + Ω(err).ShouldNot(HaveOccurred()) + c = command.Command{ + Name: "enflabulate", + Flags: fs, + Usage: "flooper enflabulate ", + ShortDoc: "Enflabulate all the mandribles", + Documentation: "Coherent quasistatic protocols will be upended if contrabulaturally is greater than 23.", + DocLink: "fabulous-enflabulence", + Command: rt.C("enflabulate"), + } + }) + + Describe("Run", func() { + Context("when flags fails to parse", func() { + It("aborts with usage", func() { + Ω(func() { + c.Run([]string{"-not-a-flag=oops"}, []string{"additional", "args"}) + }).Should(PanicWith(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "ExitCode": Equal(1), + "Error": HaveOccurred(), + "EmitUsage": BeTrue(), + }))) + + Ω(rt).Should(HaveTrackedNothing()) + }) + }) + + Context("when flags parse", func() { + It("runs the command", func() { + c.Run([]string{"-contrabulaturally=16", "and-an-arg", "and-another"}, []string{"additional", "args"}) + Ω(rt).Should(HaveRun("enflabulate")) + + Ω(rt.DataFor("enflabulate")["Args"]).Should(Equal([]string{"and-an-arg", "and-another"})) + Ω(rt.DataFor("enflabulate")["AdditionalArgs"]).Should(Equal([]string{"additional", "args"})) + + }) + }) + }) + + Describe("Usage", func() { + BeforeEach(func() { + formatter.SingletonFormatter.ColorMode = formatter.ColorModePassthrough + }) + + It("emits a nicely formatted usage", func() { + buf := gbytes.NewBuffer() + c.EmitUsage(buf) + + expected := strings.Join([]string{ + "{{bold}}flooper enflabulate {{/}}", + "{{gray}}--------------------------{{/}}", + "Enflabulate all the mandribles", + "", + "Coherent quasistatic protocols will be upended if contrabulaturally is greater than 23.", + "", + "{{bold}}Learn more at:{{/}} {{cyan}}{{underline}}http://onsi.github.io/ginkgo/#fabulous-enflabulence{{/}}", + "", + " --contrabulaturally{{/}} [int] {{gray}}{{/}}", + " {{light-gray}}with irridiacy{{/}}", + "", "", + }, "\n") + + Ω(string(buf.Contents())).Should(Equal(expected)) + }) + }) +}) diff --git a/ginkgo/command/program.go b/ginkgo/command/program.go new file mode 100644 index 0000000000..3b83ab22ff --- /dev/null +++ b/ginkgo/command/program.go @@ -0,0 +1,179 @@ +package command + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/types" +) + +type Program struct { + Name string + Heading string + Commands []Command + DefaultCommand Command + DeprecatedCommands []DeprecatedCommand + + //For testing - leave as nil in production + OutWriter io.Writer + ErrWriter io.Writer + Exiter func(code int) +} + +type DeprecatedCommand struct { + Name string + Deprecation types.Deprecation +} + +func (p Program) RunAndExit(osArgs []string) { + var command Command + deprecationTracker := types.NewDeprecationTracker() + if p.Exiter == nil { + p.Exiter = os.Exit + } + if p.OutWriter == nil { + p.OutWriter = formatter.ColorableStdOut + } + if p.ErrWriter == nil { + p.ErrWriter = formatter.ColorableStdErr + } + + defer func() { + exitCode := 0 + + if r := recover(); r != nil { + details, ok := r.(AbortDetails) + if !ok { + panic(r) + } + + if details.Error != nil { + fmt.Fprintln(p.ErrWriter, formatter.F("{{red}}{{bold}}%s %s{{/}} {{red}}failed{{/}}", p.Name, command.Name)) + fmt.Fprintln(p.ErrWriter, formatter.Fi(1, details.Error.Error())) + } + if details.EmitUsage { + if details.Error != nil { + fmt.Fprintln(p.ErrWriter, "") + } + command.EmitUsage(p.ErrWriter) + } + exitCode = details.ExitCode + } + + command.Flags.ValidateDeprecations(deprecationTracker) + if deprecationTracker.DidTrackDeprecations() { + fmt.Fprintln(p.ErrWriter, deprecationTracker.DeprecationsReport()) + } + p.Exiter(exitCode) + return + }() + + args, additionalArgs := []string{}, []string{} + + foundDelimiter := false + for _, arg := range osArgs[1:] { + if !foundDelimiter { + if arg == "--" { + foundDelimiter = true + continue + } + } + + if foundDelimiter { + additionalArgs = append(additionalArgs, arg) + } else { + args = append(args, arg) + } + } + + command = p.DefaultCommand + if len(args) > 0 { + p.handleHelpRequestsAndExit(p.OutWriter, args) + if command.Name == args[0] { + args = args[1:] + } else { + for _, deprecatedCommand := range p.DeprecatedCommands { + if deprecatedCommand.Name == args[0] { + deprecationTracker.TrackDeprecation(deprecatedCommand.Deprecation) + return + } + } + for _, tryCommand := range p.Commands { + if tryCommand.Name == args[0] { + command, args = tryCommand, args[1:] + break + } + } + } + } + + command.Run(args, additionalArgs) +} + +func (p Program) handleHelpRequestsAndExit(writer io.Writer, args []string) { + if len(args) == 0 { + return + } + + matchesHelpFlag := func(args ...string) bool { + for _, arg := range args { + if arg == "--help" || arg == "-help" || arg == "-h" || arg == "--h" { + return true + } + } + return false + } + if len(args) == 1 { + if args[0] == "help" || matchesHelpFlag(args[0]) { + p.EmitUsage(writer) + Abort(AbortDetails{}) + } + } else { + var name string + if args[0] == "help" || matchesHelpFlag(args[0]) { + name = args[1] + } else if matchesHelpFlag(args[1:]...) { + name = args[0] + } else { + return + } + + if p.DefaultCommand.Name == name || p.Name == name { + p.DefaultCommand.EmitUsage(writer) + Abort(AbortDetails{}) + } + for _, command := range p.Commands { + if command.Name == name { + command.EmitUsage(writer) + Abort(AbortDetails{}) + } + } + + fmt.Fprintln(writer, formatter.F("{{red}}Unknown Command: {{bold}}%s{{/}}", name)) + fmt.Fprintln(writer, "") + p.EmitUsage(writer) + Abort(AbortDetails{ExitCode: 1}) + } + return +} + +func (p Program) EmitUsage(writer io.Writer) { + fmt.Fprintln(writer, formatter.F(p.Heading)) + fmt.Fprintln(writer, formatter.F("{{gray}}%s{{/}}", strings.Repeat("-", len(p.Heading)))) + fmt.Fprintln(writer, formatter.F("For usage information, run {{bold}}%s help {{/}}:", p.Name)) + + fmt.Fprintln(writer, formatter.Fi(1, "{{bold}}%s{{/}} or %s {{bold}}%s{{/}} - {{gray}}%s{{/}}", p.Name, p.Name, p.DefaultCommand.Name, p.DefaultCommand.Usage)) + if p.DefaultCommand.ShortDoc != "" { + fmt.Fprintln(writer, formatter.Fi(2, p.DefaultCommand.ShortDoc)) + } + + for _, command := range p.Commands { + fmt.Fprintln(writer, formatter.Fi(1, "{{bold}}%s{{/}} - {{gray}}%s{{/}}", command.Name, command.Usage)) + if command.ShortDoc != "" { + fmt.Fprintln(writer, formatter.Fi(2, command.ShortDoc)) + } + } +} diff --git a/ginkgo/command/program_test.go b/ginkgo/command/program_test.go new file mode 100644 index 0000000000..96661b039c --- /dev/null +++ b/ginkgo/command/program_test.go @@ -0,0 +1,303 @@ +package command_test + +import ( + "fmt" + "strings" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/extensions/table" + "github.com/onsi/ginkgo/formatter" + . "github.com/onsi/ginkgo/internal/test_helpers" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + + "github.com/onsi/ginkgo/ginkgo/command" +) + +var _ = Describe("Program", func() { + var program command.Program + var rt *RunTracker + var buf *gbytes.Buffer + + BeforeEach(func() { + rt = NewRunTracker() + defaultCommand := command.Command{Name: "alpha", Usage: "alpha usage", ShortDoc: "such usage!", Command: rt.C("alpha")} + + fs, err := config.NewGinkgoFlagSet( + config.GinkgoFlags{ + {Name: "decay-rate", KeyPath: "Rate", Usage: "set the decay rate, in years"}, + {DeprecatedName: "old", KeyPath: "Old"}, + }, + &(struct { + Rate float64 + Old bool + }{Rate: 17.0}), + config.GinkgoFlagSections{}, + ) + Ω(err).ShouldNot(HaveOccurred()) + commands := []command.Command{ + {Name: "beta", Flags: fs, Usage: "beta usage", ShortDoc: "such usage!", Command: rt.C("beta")}, + {Name: "gamma", Command: rt.C("gamma")}, + {Name: "zeta", Command: rt.C("zeta", func() { + command.Abort(command.AbortDetails{Error: fmt.Errorf("Kaboom!"), ExitCode: 17}) + })}, + } + + deprecatedCommands := []command.DeprecatedCommand{ + {Name: "delta", Deprecation: types.Deprecation{Message: "delta is for deprecated"}}, + } + + formatter.SingletonFormatter.ColorMode = formatter.ColorModePassthrough + + buf = gbytes.NewBuffer() + program = command.Program{ + Name: "omicron", + Heading: "Omicron v2.0.0", + Commands: commands, + DefaultCommand: defaultCommand, + DeprecatedCommands: deprecatedCommands, + + Exiter: func(code int) { + rt.RunWithData("exit", "Code", code) + }, + OutWriter: buf, + ErrWriter: buf, + } + }) + + Context("when called with no subcommand", func() { + BeforeEach(func() { + program.RunAndExit([]string{"omicron"}) //os.Args always includes the name of the program as the first element + }) + + It("runs the default command", func() { + Ω(rt).Should(HaveTracked("alpha", "exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + Ω(buf.Contents()).Should(BeEmpty()) + }) + }) + + Context("when called with the default command's name", func() { + BeforeEach(func() { + program.RunAndExit([]string{"omicron", "alpha", "args1", "args2"}) + }) + + It("runs the default command", func() { + Ω(rt).Should(HaveTracked("alpha", "exit")) + Ω(rt).Should(HaveRunWithData("alpha", "Args", []string{"args1", "args2"})) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + Ω(buf.Contents()).Should(BeEmpty()) + }) + }) + + Context("when called with a subcommand", func() { + BeforeEach(func() { + program.RunAndExit([]string{"omicron", "beta"}) + }) + + It("runs that subcommand", func() { + Ω(rt).Should(HaveTracked("beta", "exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + Ω(buf.Contents()).Should(BeEmpty()) + }) + }) + + Context("when called with an unkown subcommand", func() { + BeforeEach(func() { + program.RunAndExit([]string{"omicron", "xi"}) + }) + + It("calls the default command with arguments", func() { + Ω(rt).Should(HaveTracked("alpha", "exit")) + Ω(rt).Should(HaveRunWithData("alpha", "Args", []string{"xi"})) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + Ω(buf.Contents()).Should(BeEmpty()) + }) + }) + + Context("when passed arguments and additional arguments", func() { + BeforeEach(func() { + program.RunAndExit([]string{"omicron", "gamma", "arg1", "-arg2", "--", "addArg1", "addArg2"}) + }) + It("passes both in", func() { + Ω(rt).Should(HaveTracked("gamma", "exit")) + Ω(rt).Should(HaveRunWithData("gamma", "Args", []string{"arg1", "-arg2"}, "AdditionalArgs", []string{"addArg1", "addArg2"})) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + Ω(buf.Contents()).Should(BeEmpty()) + }) + }) + + DescribeTable("Emitting help when asked", func(args []string) { + program.RunAndExit(args) + Ω(rt).Should(HaveTracked("exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + //HavePrefix to avoid trailing whitespace causing failures + Ω(string(buf.Contents())).Should(HavePrefix(strings.Join([]string{ + "Omicron v2.0.0", + "{{gray}}--------------{{/}}", + "For usage information, run {{bold}}omicron help {{/}}:", + " {{bold}}omicron{{/}} or omicron {{bold}}alpha{{/}} - {{gray}}alpha usage{{/}}", + " such usage!", + " {{bold}}beta{{/}} - {{gray}}beta usage{{/}}", + " such usage!", + " {{bold}}gamma{{/}} - {{gray}}{{/}}", + " {{bold}}zeta{{/}} - {{gray}}{{/}}", + }, "\n"))) + }, + Entry("with help", []string{"omicron", "help"}), + Entry("with -help", []string{"omicron", "-help"}), + Entry("with --help", []string{"omicron", "--help"}), + Entry("with -h", []string{"omicron", "-h"}), + Entry("with --h", []string{"omicron", "--h"}), + ) + + DescribeTable("Emitting help for the default command", func(args []string) { + program.RunAndExit(args) + Ω(rt).Should(HaveTracked("exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + Ω(string(buf.Contents())).Should(HavePrefix(strings.Join([]string{ + "{{bold}}alpha usage{{/}}", + "{{gray}}-----------{{/}}", + "such usage!", + }, "\n"))) + }, + Entry("with help omicron", []string{"omicron", "help", "omicron"}), + Entry("with -help omicron", []string{"omicron", "-help", "omicron"}), + Entry("with --help omicron", []string{"omicron", "--help", "omicron"}), + Entry("with -h omicron", []string{"omicron", "-h", "omicron"}), + Entry("with --h omicron", []string{"omicron", "--h", "omicron"}), + Entry("with help alpha", []string{"omicron", "help", "alpha"}), + Entry("with -help alpha", []string{"omicron", "-help", "alpha"}), + Entry("with --help alpha", []string{"omicron", "--help", "alpha"}), + Entry("with -h alpha", []string{"omicron", "-h", "alpha"}), + Entry("with --h alpha", []string{"omicron", "--h", "alpha"}), + Entry("with alpha -help", []string{"omicron", "alpha", "-help"}), + Entry("with alpha --help", []string{"omicron", "alpha", "--help"}), + Entry("with alpha -h", []string{"omicron", "alpha", "-h"}), + Entry("with alpha --h", []string{"omicron", "alpha", "--h"}), + ) + + DescribeTable("Emitting help for a known subcommand", func(args []string) { + program.RunAndExit(args) + Ω(rt).Should(HaveTracked("exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + Ω(string(buf.Contents())).Should(HavePrefix(strings.Join([]string{ + "{{bold}}beta usage{{/}}", + "{{gray}}----------{{/}}", + "such usage!", + "", + " --decay-rate{{/}} [float] {{gray}}{{/}}", + " {{light-gray}}set the decay rate, in years{{/}}", + }, "\n"))) + }, + Entry("with help beta", []string{"omicron", "help", "beta"}), + Entry("with -help beta", []string{"omicron", "-help", "beta"}), + Entry("with --help beta", []string{"omicron", "--help", "beta"}), + Entry("with -h beta", []string{"omicron", "-h", "beta"}), + Entry("with --h beta", []string{"omicron", "--h", "beta"}), + Entry("with beta -help", []string{"omicron", "beta", "-help"}), + Entry("with beta --help", []string{"omicron", "beta", "--help"}), + Entry("with beta -h", []string{"omicron", "beta", "-h"}), + Entry("with beta --h", []string{"omicron", "beta", "--h"}), + ) + + DescribeTable("Emitting help for an unknown subcommand", func(args []string) { + program.RunAndExit(args) + Ω(rt).Should(HaveTracked("exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 1)) + Ω(string(buf.Contents())).Should(HavePrefix(strings.Join([]string{ + "{{red}}Unknown Command: {{bold}}xi{{/}}", + "", + "Omicron v2.0.0", + "{{gray}}--------------{{/}}", + "For usage information, run {{bold}}omicron help {{/}}:", + " {{bold}}omicron{{/}} or omicron {{bold}}alpha{{/}} - {{gray}}alpha usage{{/}}", + " such usage!", + " {{bold}}beta{{/}} - {{gray}}beta usage{{/}}", + " such usage!", + " {{bold}}gamma{{/}} - {{gray}}{{/}}", + " {{bold}}zeta{{/}} - {{gray}}{{/}}", + }, "\n"))) + }, + Entry("with help xi", []string{"omicron", "help", "xi"}), + Entry("with -help xi", []string{"omicron", "-help", "xi"}), + Entry("with --help xi", []string{"omicron", "--help", "xi"}), + Entry("with -h xi", []string{"omicron", "-h", "xi"}), + Entry("with --h xi", []string{"omicron", "--h", "xi"}), + Entry("with xi -help", []string{"omicron", "xi", "-help"}), + Entry("with xi --help", []string{"omicron", "xi", "--help"}), + Entry("with xi -h", []string{"omicron", "xi", "-h"}), + Entry("with xi --h", []string{"omicron", "xi", "--h"}), + ) + Context("when called with a deprecated command", func() { + BeforeEach(func() { + program.RunAndExit([]string{"omicron", "delta"}) + }) + + It("lets the user know the command is deprecated", func() { + Ω(rt).Should(HaveTracked("exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + Ω(string(buf.Contents())).Should(HavePrefix(strings.Join([]string{ + "{{light-yellow}}You're using deprecated Ginkgo functionality:{{/}}", + "{{light-yellow}}============================================={{/}}", + " {{yellow}}delta is for deprecated{{/}}", + }, "\n"))) + }) + }) + + Context("when a deprecated flag is used", func() { + BeforeEach(func() { + program.RunAndExit([]string{"omicron", "beta", "-old"}) + }) + + It("lets the user know a deprecated flag was used", func() { + Ω(rt).Should(HaveTracked("beta", "exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 0)) + Ω(string(buf.Contents())).Should(HavePrefix(strings.Join([]string{ + "{{light-yellow}}You're using deprecated Ginkgo functionality:{{/}}", + "{{light-yellow}}============================================={{/}}", + " {{yellow}}--old is deprecated{{/}}", + }, "\n"))) + }) + }) + + Context("when an unkown flag is used", func() { + BeforeEach(func() { + program.RunAndExit([]string{"omicron", "beta", "-zanzibar"}) + }) + + It("emits usage for the associated subcommand", func() { + Ω(rt).Should(HaveTracked("exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 1)) + Ω(string(buf.Contents())).Should(HavePrefix(strings.Join([]string{ + "{{red}}{{bold}}omicron beta{{/}} {{red}}failed{{/}}", + " flag provided but not defined: -zanzibar", + "", + "{{bold}}beta usage{{/}}", + "{{gray}}----------{{/}}", + "such usage!", + "", + " --decay-rate{{/}} [float] {{gray}}{{/}}", + " {{light-gray}}set the decay rate, in years{{/}}", + }, "\n"))) + }) + }) + + Context("when a subcommand aborts", func() { + BeforeEach(func() { + program.RunAndExit([]string{"omicron", "zeta"}) + }) + + It("emits information about the error", func() { + Ω(rt).Should(HaveTracked("zeta", "exit")) + Ω(rt).Should(HaveRunWithData("exit", "Code", 17)) + Ω(string(buf.Contents())).Should(HavePrefix(strings.Join([]string{ + "{{red}}{{bold}}omicron zeta{{/}} {{red}}failed{{/}}", + " Kaboom!", + }, "\n"))) + }) + }) + +}) diff --git a/ginkgo/convert/ginkgo_ast_nodes.go b/ginkgo/convert/ginkgo_ast_nodes.go deleted file mode 100644 index 02e2b3b328..0000000000 --- a/ginkgo/convert/ginkgo_ast_nodes.go +++ /dev/null @@ -1,123 +0,0 @@ -package convert - -import ( - "fmt" - "go/ast" - "strings" - "unicode" -) - -/* - * Creates a func init() node - */ -func createVarUnderscoreBlock() *ast.ValueSpec { - valueSpec := &ast.ValueSpec{} - object := &ast.Object{Kind: 4, Name: "_", Decl: valueSpec, Data: 0} - ident := &ast.Ident{Name: "_", Obj: object} - valueSpec.Names = append(valueSpec.Names, ident) - return valueSpec -} - -/* - * Creates a Describe("Testing with ginkgo", func() { }) node - */ -func createDescribeBlock() *ast.CallExpr { - blockStatement := &ast.BlockStmt{List: []ast.Stmt{}} - - fieldList := &ast.FieldList{} - funcType := &ast.FuncType{Params: fieldList} - funcLit := &ast.FuncLit{Type: funcType, Body: blockStatement} - basicLit := &ast.BasicLit{Kind: 9, Value: "\"Testing with Ginkgo\""} - describeIdent := &ast.Ident{Name: "Describe"} - return &ast.CallExpr{Fun: describeIdent, Args: []ast.Expr{basicLit, funcLit}} -} - -/* - * Convenience function to return the name of the *testing.T param - * for a Test function that will be rewritten. This is useful because - * we will want to replace the usage of this named *testing.T inside the - * body of the function with a GinktoT. - */ -func namedTestingTArg(node *ast.FuncDecl) string { - return node.Type.Params.List[0].Names[0].Name // *exhale* -} - -/* - * Convenience function to return the block statement node for a Describe statement - */ -func blockStatementFromDescribe(desc *ast.CallExpr) *ast.BlockStmt { - var funcLit *ast.FuncLit - var found = false - - for _, node := range desc.Args { - switch node := node.(type) { - case *ast.FuncLit: - found = true - funcLit = node - break - } - } - - if !found { - panic("Error finding ast.FuncLit inside describe statement. Somebody done goofed.") - } - - return funcLit.Body -} - -/* convenience function for creating an It("TestNameHere") - * with all the body of the test function inside the anonymous - * func passed to It() - */ -func createItStatementForTestFunc(testFunc *ast.FuncDecl) *ast.ExprStmt { - blockStatement := &ast.BlockStmt{List: testFunc.Body.List} - fieldList := &ast.FieldList{} - funcType := &ast.FuncType{Params: fieldList} - funcLit := &ast.FuncLit{Type: funcType, Body: blockStatement} - - testName := rewriteTestName(testFunc.Name.Name) - basicLit := &ast.BasicLit{Kind: 9, Value: fmt.Sprintf("\"%s\"", testName)} - itBlockIdent := &ast.Ident{Name: "It"} - callExpr := &ast.CallExpr{Fun: itBlockIdent, Args: []ast.Expr{basicLit, funcLit}} - return &ast.ExprStmt{X: callExpr} -} - -/* -* rewrite test names to be human readable -* eg: rewrites "TestSomethingAmazing" as "something amazing" - */ -func rewriteTestName(testName string) string { - nameComponents := []string{} - currentString := "" - indexOfTest := strings.Index(testName, "Test") - if indexOfTest != 0 { - return testName - } - - testName = strings.Replace(testName, "Test", "", 1) - first, rest := testName[0], testName[1:] - testName = string(unicode.ToLower(rune(first))) + rest - - for _, rune := range testName { - if unicode.IsUpper(rune) { - nameComponents = append(nameComponents, currentString) - currentString = string(unicode.ToLower(rune)) - } else { - currentString += string(rune) - } - } - - return strings.Join(append(nameComponents, currentString), " ") -} - -func newGinkgoTFromIdent(ident *ast.Ident) *ast.CallExpr { - return &ast.CallExpr{ - Lparen: ident.NamePos + 1, - Rparen: ident.NamePos + 2, - Fun: &ast.Ident{Name: "GinkgoT"}, - } -} - -func newGinkgoTInterface() *ast.Ident { - return &ast.Ident{Name: "GinkgoTInterface"} -} diff --git a/ginkgo/convert/import.go b/ginkgo/convert/import.go deleted file mode 100644 index 06c6ec94c9..0000000000 --- a/ginkgo/convert/import.go +++ /dev/null @@ -1,90 +0,0 @@ -package convert - -import ( - "fmt" - "go/ast" -) - -/* - * Given the root node of an AST, returns the node containing the - * import statements for the file. - */ -func importsForRootNode(rootNode *ast.File) (imports *ast.GenDecl, err error) { - for _, declaration := range rootNode.Decls { - decl, ok := declaration.(*ast.GenDecl) - if !ok || len(decl.Specs) == 0 { - continue - } - - _, ok = decl.Specs[0].(*ast.ImportSpec) - if ok { - imports = decl - return - } - } - - err = fmt.Errorf("Could not find imports for root node:\n\t%#v\n", rootNode) - return -} - -/* - * Removes "testing" import, if present - */ -func removeTestingImport(rootNode *ast.File) { - importDecl, err := importsForRootNode(rootNode) - if err != nil { - panic(err.Error()) - } - - var index int - for i, importSpec := range importDecl.Specs { - importSpec := importSpec.(*ast.ImportSpec) - if importSpec.Path.Value == "\"testing\"" { - index = i - break - } - } - - importDecl.Specs = append(importDecl.Specs[:index], importDecl.Specs[index+1:]...) -} - -/* - * Adds import statements for onsi/ginkgo, if missing - */ -func addGinkgoImports(rootNode *ast.File) { - importDecl, err := importsForRootNode(rootNode) - if err != nil { - panic(err.Error()) - } - - if len(importDecl.Specs) == 0 { - // TODO: might need to create a import decl here - panic("unimplemented : expected to find an imports block") - } - - needsGinkgo := true - for _, importSpec := range importDecl.Specs { - importSpec, ok := importSpec.(*ast.ImportSpec) - if !ok { - continue - } - - if importSpec.Path.Value == "\"github.com/onsi/ginkgo\"" { - needsGinkgo = false - } - } - - if needsGinkgo { - importDecl.Specs = append(importDecl.Specs, createImport(".", "\"github.com/onsi/ginkgo\"")) - } -} - -/* - * convenience function to create an import statement - */ -func createImport(name, path string) *ast.ImportSpec { - return &ast.ImportSpec{ - Name: &ast.Ident{Name: name}, - Path: &ast.BasicLit{Kind: 9, Value: path}, - } -} diff --git a/ginkgo/convert/package_rewriter.go b/ginkgo/convert/package_rewriter.go deleted file mode 100644 index 363e52fe2f..0000000000 --- a/ginkgo/convert/package_rewriter.go +++ /dev/null @@ -1,128 +0,0 @@ -package convert - -import ( - "fmt" - "go/build" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "regexp" -) - -/* - * RewritePackage takes a name (eg: my-package/tools), finds its test files using - * Go's build package, and then rewrites them. A ginkgo test suite file will - * also be added for this package, and all of its child packages. - */ -func RewritePackage(packageName string) { - pkg, err := packageWithName(packageName) - if err != nil { - panic(fmt.Sprintf("unexpected error reading package: '%s'\n%s\n", packageName, err.Error())) - } - - for _, filename := range findTestsInPackage(pkg) { - rewriteTestsInFile(filename) - } -} - -/* - * Given a package, findTestsInPackage reads the test files in the directory, - * and then recurses on each child package, returning a slice of all test files - * found in this process. - */ -func findTestsInPackage(pkg *build.Package) (testfiles []string) { - for _, file := range append(pkg.TestGoFiles, pkg.XTestGoFiles...) { - testfile, _ := filepath.Abs(filepath.Join(pkg.Dir, file)) - testfiles = append(testfiles, testfile) - } - - dirFiles, err := ioutil.ReadDir(pkg.Dir) - if err != nil { - panic(fmt.Sprintf("unexpected error reading dir: '%s'\n%s\n", pkg.Dir, err.Error())) - } - - re := regexp.MustCompile(`^[._]`) - - for _, file := range dirFiles { - if !file.IsDir() { - continue - } - - if re.Match([]byte(file.Name())) { - continue - } - - packageName := filepath.Join(pkg.ImportPath, file.Name()) - subPackage, err := packageWithName(packageName) - if err != nil { - panic(fmt.Sprintf("unexpected error reading package: '%s'\n%s\n", packageName, err.Error())) - } - - testfiles = append(testfiles, findTestsInPackage(subPackage)...) - } - - addGinkgoSuiteForPackage(pkg) - goFmtPackage(pkg) - return -} - -/* - * Shells out to `ginkgo bootstrap` to create a test suite file - */ -func addGinkgoSuiteForPackage(pkg *build.Package) { - originalDir, err := os.Getwd() - if err != nil { - panic(err) - } - - suite_test_file := filepath.Join(pkg.Dir, pkg.Name+"_suite_test.go") - - _, err = os.Stat(suite_test_file) - if err == nil { - return // test file already exists, this should be a no-op - } - - err = os.Chdir(pkg.Dir) - if err != nil { - panic(err) - } - - output, err := exec.Command("ginkgo", "bootstrap").Output() - - if err != nil { - panic(fmt.Sprintf("error running 'ginkgo bootstrap'.\nstdout: %s\n%s\n", output, err.Error())) - } - - err = os.Chdir(originalDir) - if err != nil { - panic(err) - } -} - -/* - * Shells out to `go fmt` to format the package - */ -func goFmtPackage(pkg *build.Package) { - path, _ := filepath.Abs(pkg.ImportPath) - output, err := exec.Command("go", "fmt", path).CombinedOutput() - - if err != nil { - fmt.Printf("Warning: Error running 'go fmt %s'.\nstdout: %s\n%s\n", path, output, err.Error()) - } -} - -/* - * Attempts to return a package with its test files already read. - * The ImportMode arg to build.Import lets you specify if you want go to read the - * buildable go files inside the package, but it fails if the package has no go files - */ -func packageWithName(name string) (pkg *build.Package, err error) { - pkg, err = build.Default.Import(name, ".", build.ImportMode(0)) - if err == nil { - return - } - - pkg, err = build.Default.Import(name, ".", build.ImportMode(1)) - return -} diff --git a/ginkgo/convert/test_finder.go b/ginkgo/convert/test_finder.go deleted file mode 100644 index b33595c9ae..0000000000 --- a/ginkgo/convert/test_finder.go +++ /dev/null @@ -1,56 +0,0 @@ -package convert - -import ( - "go/ast" - "regexp" -) - -/* - * Given a root node, walks its top level statements and returns - * points to function nodes to rewrite as It statements. - * These functions, according to Go testing convention, must be named - * TestWithCamelCasedName and receive a single *testing.T argument. - */ -func findTestFuncs(rootNode *ast.File) (testsToRewrite []*ast.FuncDecl) { - testNameRegexp := regexp.MustCompile("^Test[0-9A-Z].+") - - ast.Inspect(rootNode, func(node ast.Node) bool { - if node == nil { - return false - } - - switch node := node.(type) { - case *ast.FuncDecl: - matches := testNameRegexp.MatchString(node.Name.Name) - - if matches && receivesTestingT(node) { - testsToRewrite = append(testsToRewrite, node) - } - } - - return true - }) - - return -} - -/* - * convenience function that looks at args to a function and determines if its - * params include an argument of type *testing.T - */ -func receivesTestingT(node *ast.FuncDecl) bool { - if len(node.Type.Params.List) != 1 { - return false - } - - base, ok := node.Type.Params.List[0].Type.(*ast.StarExpr) - if !ok { - return false - } - - intermediate := base.X.(*ast.SelectorExpr) - isTestingPackage := intermediate.X.(*ast.Ident).Name == "testing" - isTestingT := intermediate.Sel.Name == "T" - - return isTestingPackage && isTestingT -} diff --git a/ginkgo/convert/testfile_rewriter.go b/ginkgo/convert/testfile_rewriter.go deleted file mode 100644 index 60c73504ad..0000000000 --- a/ginkgo/convert/testfile_rewriter.go +++ /dev/null @@ -1,162 +0,0 @@ -package convert - -import ( - "bytes" - "fmt" - "go/ast" - "go/format" - "go/parser" - "go/token" - "io/ioutil" - "os" -) - -/* - * Given a file path, rewrites any tests in the Ginkgo format. - * First, we parse the AST, and update the imports declaration. - * Then, we walk the first child elements in the file, returning tests to rewrite. - * A top level init func is declared, with a single Describe func inside. - * Then the test functions to rewrite are inserted as It statements inside the Describe. - * Finally we walk the rest of the file, replacing other usages of *testing.T - * Once that is complete, we write the AST back out again to its file. - */ -func rewriteTestsInFile(pathToFile string) { - fileSet := token.NewFileSet() - rootNode, err := parser.ParseFile(fileSet, pathToFile, nil, parser.ParseComments) - if err != nil { - panic(fmt.Sprintf("Error parsing test file '%s':\n%s\n", pathToFile, err.Error())) - } - - addGinkgoImports(rootNode) - removeTestingImport(rootNode) - - varUnderscoreBlock := createVarUnderscoreBlock() - describeBlock := createDescribeBlock() - varUnderscoreBlock.Values = []ast.Expr{describeBlock} - - for _, testFunc := range findTestFuncs(rootNode) { - rewriteTestFuncAsItStatement(testFunc, rootNode, describeBlock) - } - - underscoreDecl := &ast.GenDecl{ - Tok: 85, // gah, magick numbers are needed to make this work - TokPos: 14, // this tricks Go into writing "var _ = Describe" - Specs: []ast.Spec{varUnderscoreBlock}, - } - - imports := rootNode.Decls[0] - tail := rootNode.Decls[1:] - rootNode.Decls = append(append([]ast.Decl{imports}, underscoreDecl), tail...) - rewriteOtherFuncsToUseGinkgoT(rootNode.Decls) - walkNodesInRootNodeReplacingTestingT(rootNode) - - var buffer bytes.Buffer - if err = format.Node(&buffer, fileSet, rootNode); err != nil { - panic(fmt.Sprintf("Error formatting ast node after rewriting tests.\n%s\n", err.Error())) - } - - fileInfo, err := os.Stat(pathToFile) - - if err != nil { - panic(fmt.Sprintf("Error stat'ing file: %s\n", pathToFile)) - } - - err = ioutil.WriteFile(pathToFile, buffer.Bytes(), fileInfo.Mode()) -} - -/* - * Given a test func named TestDoesSomethingNeat, rewrites it as - * It("does something neat", func() { __test_body_here__ }) and adds it - * to the Describe's list of statements - */ -func rewriteTestFuncAsItStatement(testFunc *ast.FuncDecl, rootNode *ast.File, describe *ast.CallExpr) { - var funcIndex int = -1 - for index, child := range rootNode.Decls { - if child == testFunc { - funcIndex = index - break - } - } - - if funcIndex < 0 { - panic(fmt.Sprintf("Assert failed: Error finding index for test node %s\n", testFunc.Name.Name)) - } - - var block *ast.BlockStmt = blockStatementFromDescribe(describe) - block.List = append(block.List, createItStatementForTestFunc(testFunc)) - replaceTestingTsWithGinkgoT(block, namedTestingTArg(testFunc)) - - // remove the old test func from the root node's declarations - rootNode.Decls = append(rootNode.Decls[:funcIndex], rootNode.Decls[funcIndex+1:]...) -} - -/* - * walks nodes inside of a test func's statements and replaces the usage of - * it's named *testing.T param with GinkgoT's - */ -func replaceTestingTsWithGinkgoT(statementsBlock *ast.BlockStmt, testingT string) { - ast.Inspect(statementsBlock, func(node ast.Node) bool { - if node == nil { - return false - } - - keyValueExpr, ok := node.(*ast.KeyValueExpr) - if ok { - replaceNamedTestingTsInKeyValueExpression(keyValueExpr, testingT) - return true - } - - funcLiteral, ok := node.(*ast.FuncLit) - if ok { - replaceTypeDeclTestingTsInFuncLiteral(funcLiteral) - return true - } - - callExpr, ok := node.(*ast.CallExpr) - if !ok { - return true - } - replaceTestingTsInArgsLists(callExpr, testingT) - - funCall, ok := callExpr.Fun.(*ast.SelectorExpr) - if ok { - replaceTestingTsMethodCalls(funCall, testingT) - } - - return true - }) -} - -/* - * rewrite t.Fail() or any other *testing.T method by replacing with T().Fail() - * This function receives a selector expression (eg: t.Fail()) and - * the name of the *testing.T param from the function declaration. Rewrites the - * selector expression in place if the target was a *testing.T - */ -func replaceTestingTsMethodCalls(selectorExpr *ast.SelectorExpr, testingT string) { - ident, ok := selectorExpr.X.(*ast.Ident) - if !ok { - return - } - - if ident.Name == testingT { - selectorExpr.X = newGinkgoTFromIdent(ident) - } -} - -/* - * replaces usages of a named *testing.T param inside of a call expression - * with a new GinkgoT object - */ -func replaceTestingTsInArgsLists(callExpr *ast.CallExpr, testingT string) { - for index, arg := range callExpr.Args { - ident, ok := arg.(*ast.Ident) - if !ok { - continue - } - - if ident.Name == testingT { - callExpr.Args[index] = newGinkgoTFromIdent(ident) - } - } -} diff --git a/ginkgo/convert/testing_t_rewriter.go b/ginkgo/convert/testing_t_rewriter.go deleted file mode 100644 index 418cdc4e56..0000000000 --- a/ginkgo/convert/testing_t_rewriter.go +++ /dev/null @@ -1,130 +0,0 @@ -package convert - -import ( - "go/ast" -) - -/* - * Rewrites any other top level funcs that receive a *testing.T param - */ -func rewriteOtherFuncsToUseGinkgoT(declarations []ast.Decl) { - for _, decl := range declarations { - decl, ok := decl.(*ast.FuncDecl) - if !ok { - continue - } - - for _, param := range decl.Type.Params.List { - starExpr, ok := param.Type.(*ast.StarExpr) - if !ok { - continue - } - - selectorExpr, ok := starExpr.X.(*ast.SelectorExpr) - if !ok { - continue - } - - xIdent, ok := selectorExpr.X.(*ast.Ident) - if !ok || xIdent.Name != "testing" { - continue - } - - if selectorExpr.Sel.Name != "T" { - continue - } - - param.Type = newGinkgoTInterface() - } - } -} - -/* - * Walks all of the nodes in the file, replacing *testing.T in struct - * and func literal nodes. eg: - * type foo struct { *testing.T } - * var bar = func(t *testing.T) { } - */ -func walkNodesInRootNodeReplacingTestingT(rootNode *ast.File) { - ast.Inspect(rootNode, func(node ast.Node) bool { - if node == nil { - return false - } - - switch node := node.(type) { - case *ast.StructType: - replaceTestingTsInStructType(node) - case *ast.FuncLit: - replaceTypeDeclTestingTsInFuncLiteral(node) - } - - return true - }) -} - -/* - * replaces named *testing.T inside a composite literal - */ -func replaceNamedTestingTsInKeyValueExpression(kve *ast.KeyValueExpr, testingT string) { - ident, ok := kve.Value.(*ast.Ident) - if !ok { - return - } - - if ident.Name == testingT { - kve.Value = newGinkgoTFromIdent(ident) - } -} - -/* - * replaces *testing.T params in a func literal with GinkgoT - */ -func replaceTypeDeclTestingTsInFuncLiteral(functionLiteral *ast.FuncLit) { - for _, arg := range functionLiteral.Type.Params.List { - starExpr, ok := arg.Type.(*ast.StarExpr) - if !ok { - continue - } - - selectorExpr, ok := starExpr.X.(*ast.SelectorExpr) - if !ok { - continue - } - - target, ok := selectorExpr.X.(*ast.Ident) - if !ok { - continue - } - - if target.Name == "testing" && selectorExpr.Sel.Name == "T" { - arg.Type = newGinkgoTInterface() - } - } -} - -/* - * Replaces *testing.T types inside of a struct declaration with a GinkgoT - * eg: type foo struct { *testing.T } - */ -func replaceTestingTsInStructType(structType *ast.StructType) { - for _, field := range structType.Fields.List { - starExpr, ok := field.Type.(*ast.StarExpr) - if !ok { - continue - } - - selectorExpr, ok := starExpr.X.(*ast.SelectorExpr) - if !ok { - continue - } - - xIdent, ok := selectorExpr.X.(*ast.Ident) - if !ok { - continue - } - - if xIdent.Name == "testing" && selectorExpr.Sel.Name == "T" { - field.Type = newGinkgoTInterface() - } - } -} diff --git a/ginkgo/convert_command.go b/ginkgo/convert_command.go deleted file mode 100644 index 8e99f56a23..0000000000 --- a/ginkgo/convert_command.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/onsi/ginkgo/ginkgo/convert" - colorable "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable" - "github.com/onsi/ginkgo/types" -) - -func BuildConvertCommand() *Command { - return &Command{ - Name: "convert", - FlagSet: flag.NewFlagSet("convert", flag.ExitOnError), - UsageCommand: "ginkgo convert /path/to/package", - Usage: []string{ - "Convert the package at the passed in path from an XUnit-style test to a Ginkgo-style test", - }, - Command: convertPackage, - } -} - -func convertPackage(args []string, additionalArgs []string) { - deprecationTracker := types.NewDeprecationTracker() - deprecationTracker.TrackDeprecation(types.Deprecations.Convert()) - fmt.Fprintln(colorable.NewColorableStderr(), deprecationTracker.DeprecationsReport()) - - if len(args) != 1 { - println(fmt.Sprintf("usage: ginkgo convert /path/to/your/package")) - os.Exit(1) - } - - defer func() { - err := recover() - if err != nil { - switch err := err.(type) { - case error: - println(err.Error()) - case string: - println(err) - default: - println(fmt.Sprintf("unexpected error: %#v", err)) - } - os.Exit(1) - } - }() - - convert.RewritePackage(args[0]) -} diff --git a/ginkgo/generators/boostrap_templates.go b/ginkgo/generators/boostrap_templates.go new file mode 100644 index 0000000000..f8a5158622 --- /dev/null +++ b/ginkgo/generators/boostrap_templates.go @@ -0,0 +1,48 @@ +package generators + +var bootstrapText = `package {{.Package}} + +import ( + "testing" + + {{.GinkgoImport}} + {{.GomegaImport}} +) + +func Test{{.FormattedName}}(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "{{.FormattedName}} Suite") +} +` + +var agoutiBootstrapText = `package {{.Package}} + +import ( + "testing" + + {{.GinkgoImport}} + {{.GomegaImport}} + "github.com/sclevine/agouti" +) + +func Test{{.FormattedName}}(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "{{.FormattedName}} Suite") +} + +var agoutiDriver *agouti.WebDriver + +var _ = BeforeSuite(func() { + // Choose a WebDriver: + + agoutiDriver = agouti.PhantomJS() + // agoutiDriver = agouti.Selenium() + // agoutiDriver = agouti.ChromeDriver() + + Expect(agoutiDriver.Start()).To(Succeed()) +}) + +var _ = AfterSuite(func() { + Expect(agoutiDriver.Stop()).To(Succeed()) +}) +` diff --git a/ginkgo/generators/bootstrap_command.go b/ginkgo/generators/bootstrap_command.go new file mode 100644 index 0000000000..effa0025ac --- /dev/null +++ b/ginkgo/generators/bootstrap_command.go @@ -0,0 +1,114 @@ +package generators + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "text/template" + + sprig "github.com/go-task/slim-sprig" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/command" + "github.com/onsi/ginkgo/ginkgo/internal" + "github.com/onsi/ginkgo/ginkgo/nodot" +) + +func BuildBootstrapCommand() command.Command { + conf := GeneratorsConfig{} + flags, err := config.NewGinkgoFlagSet( + config.GinkgoFlags{ + {Name: "agouti", KeyPath: "Agouti", + Usage: "If set, bootstrap will generate a bootstrap file for writing Agouti tests"}, + {Name: "nodot", KeyPath: "NoDot", + Usage: "If set, bootstrap will generate a bootstrap test file that does not dot-import ginkgo and gomega"}, + {Name: "internal", KeyPath: "Internal", + Usage: "If set, bootstrap will generate a bootstrap test file that uses the regular package name (i.e. `package X`, not `package X_test`)"}, + {Name: "template", KeyPath: "CustomTemplate", + UsageArgument: "template-file", + Usage: "If specified, generate will use the contents of the file passed as the bootstrap template"}, + }, + &conf, + config.GinkgoFlagSections{}, + ) + + if err != nil { + panic(err) + } + + return command.Command{ + Name: "bootstrap", + Usage: "ginkgo bootstrap", + ShortDoc: "Bootstrap a test suite for the current package", + Documentation: `Tests written in Ginkgo and Gomega require a small amount of boilerplate to hook into Go's testing infrastructure. + +{{bold}}ginkgo boostrap{{/}} generates this boilerplate for you in a file named X_suite_test.go where X is the name of the package under test.`, + DocLink: "generators", + Flags: flags, + Command: func(_ []string, _ []string) { + generateBootstrap(conf) + }, + } +} + +type bootstrapData struct { + Package string + FormattedName string + GinkgoImport string + GomegaImport string +} + +func generateBootstrap(conf GeneratorsConfig) { + packageName, bootstrapFilePrefix, formattedName := getPackageAndFormattedName() + + data := bootstrapData{ + Package: determinePackageName(packageName, conf.Internal), + FormattedName: formattedName, + GinkgoImport: `. "github.com/onsi/ginkgo"`, + GomegaImport: `. "github.com/onsi/gomega"`, + } + + if conf.NoDot { + data.GinkgoImport = `"github.com/onsi/ginkgo"` + data.GomegaImport = `"github.com/onsi/gomega"` + } + + targetFile := fmt.Sprintf("%s_suite_test.go", bootstrapFilePrefix) + if internal.FileExists(targetFile) { + command.AbortWith("{{bold}}%s{{/}} already exists", targetFile) + } else { + fmt.Printf("Generating ginkgo test suite bootstrap for %s in:\n\t%s\n", packageName, targetFile) + } + + f, err := os.Create(targetFile) + command.AbortIfError("Failed to create file:", err) + defer f.Close() + + var templateText string + if conf.CustomTemplate != "" { + tpl, err := ioutil.ReadFile(conf.CustomTemplate) + command.AbortIfError("Failed to read custom bootstrap file:", err) + templateText = string(tpl) + } else if conf.Agouti { + templateText = agoutiBootstrapText + } else { + templateText = bootstrapText + } + + bootstrapTemplate, err := template.New("bootstrap").Funcs(sprig.TxtFuncMap()).Parse(templateText) + command.AbortIfError("Failed to parse bootstrap template:", err) + + buf := &bytes.Buffer{} + bootstrapTemplate.Execute(buf, data) + + if conf.NoDot { + contents, err := nodot.ApplyNoDot(buf.Bytes()) + command.AbortIfError("Failed to import nodot declarations:", err) + fmt.Println("To update the nodot declarations in the future, switch to this directory and run:\n\tginkgo nodot") + buf = bytes.NewBuffer(contents) + } + + buf.WriteTo(f) + + internal.GoFmt(targetFile) +} diff --git a/ginkgo/generate_command.go b/ginkgo/generators/generate_command.go similarity index 53% rename from ginkgo/generate_command.go rename to ginkgo/generators/generate_command.go index 27758bebac..aa1457adf8 100644 --- a/ginkgo/generate_command.go +++ b/ginkgo/generators/generate_command.go @@ -1,8 +1,7 @@ -package main +package generators import ( "bytes" - "flag" "fmt" "io/ioutil" "os" @@ -12,73 +11,49 @@ import ( "text/template" sprig "github.com/go-task/slim-sprig" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/command" + "github.com/onsi/ginkgo/ginkgo/internal" ) -func BuildGenerateCommand() *Command { - var ( - agouti, noDot, internal bool - customTestFile string +func BuildGenerateCommand() command.Command { + conf := GeneratorsConfig{} + flags, err := config.NewGinkgoFlagSet( + config.GinkgoFlags{ + {Name: "agouti", KeyPath: "Agouti", + Usage: "If set, generate will create a test file for writing Agouti tests"}, + {Name: "nodot", KeyPath: "NoDot", + Usage: "If set, generate will create a test file that does not dot-import ginkgo and gomega"}, + {Name: "internal", KeyPath: "Internal", + Usage: "If set, generate will create a test file that uses the regular package name (i.e. `package X`, not `package X_test`)"}, + {Name: "template", KeyPath: "CustomTemplate", + UsageArgument: "template-file", + Usage: "If specified, generate will use the contents of the file passed as the test file template"}, + }, + &conf, + config.GinkgoFlagSections{}, ) - flagSet := flag.NewFlagSet("generate", flag.ExitOnError) - flagSet.BoolVar(&agouti, "agouti", false, "If set, generate will generate a test file for writing Agouti tests") - flagSet.BoolVar(&noDot, "nodot", false, "If set, generate will generate a test file that does not . import ginkgo and gomega") - flagSet.BoolVar(&internal, "internal", false, "If set, generate will generate a test file that uses the regular package name") - flagSet.StringVar(&customTestFile, "template", "", "If specified, generate will use the contents of the file passed as the test file template") - return &Command{ - Name: "generate", - FlagSet: flagSet, - UsageCommand: "ginkgo generate ", - Usage: []string{ - "Generate a test file named filename_test.go", - "If the optional argument is omitted, a file named after the package in the current directory will be created.", - "Accepts the following flags:", - }, - Command: func(args []string, additionalArgs []string) { - generateSpec(args, agouti, noDot, internal, customTestFile) - }, + if err != nil { + panic(err) } -} - -var specText = `package {{.Package}} -import ( - {{if .IncludeImports}}. "github.com/onsi/ginkgo"{{end}} - {{if .IncludeImports}}. "github.com/onsi/gomega"{{end}} + return command.Command{ + Name: "generate", + Usage: "ginkgo generate ", + ShortDoc: "Generate a test file named _test.go", + Documentation: `If the optional argument is ommitted, a file named after the package in the current direcotry will be created. - {{if .ImportPackage}}"{{.PackageImportPath}}"{{end}} -) - -var _ = Describe("{{.Subject}}", func() { - -}) -` - -var agoutiSpecText = `package {{.Package}} - -import ( - {{if .IncludeImports}}. "github.com/onsi/ginkgo"{{end}} - {{if .IncludeImports}}. "github.com/onsi/gomega"{{end}} - "github.com/sclevine/agouti" - . "github.com/sclevine/agouti/matchers" - - {{if .ImportPackage}}"{{.PackageImportPath}}"{{end}} -) +You can pass multiple to generate multiple files simultaneously. The resulting files are named _test.go. -var _ = Describe("{{.Subject}}", func() { - var page *agouti.Page - - BeforeEach(func() { - var err error - page, err = agoutiDriver.NewPage() - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - Expect(page.Destroy()).To(Succeed()) - }) -}) -` +You can also pass a of the form "file.go" and generate will emit "file_test.go".`, + DocLink: "generators", + Flags: flags, + Command: func(args []string, _ []string) { + generateTestFiles(conf, args) + }, + } +} type specData struct { Package string @@ -88,85 +63,62 @@ type specData struct { ImportPackage bool } -func generateSpec(args []string, agouti, noDot, internal bool, customTestFile string) { - if len(args) == 0 { - err := generateSpecForSubject("", agouti, noDot, internal, customTestFile) - if err != nil { - fmt.Println(err.Error()) - fmt.Println("") - os.Exit(1) - } - fmt.Println("") - return - } - - var failed bool - for _, arg := range args { - err := generateSpecForSubject(arg, agouti, noDot, internal, customTestFile) - if err != nil { - failed = true - fmt.Println(err.Error()) - } +func generateTestFiles(conf GeneratorsConfig, args []string) { + subjects := args + if len(subjects) == 0 { + subjects = []string{""} } - fmt.Println("") - if failed { - os.Exit(1) + for _, subject := range subjects { + generateTestFileForSubject(subject, conf) } } -func generateSpecForSubject(subject string, agouti, noDot, internal bool, customTestFile string) error { +func generateTestFileForSubject(subject string, conf GeneratorsConfig) { packageName, specFilePrefix, formattedName := getPackageAndFormattedName() if subject != "" { specFilePrefix = formatSubject(subject) formattedName = prettifyPackageName(specFilePrefix) } - if internal { + if conf.Internal { specFilePrefix = specFilePrefix + "_internal" } data := specData{ - Package: determinePackageName(packageName, internal), + Package: determinePackageName(packageName, conf.Internal), Subject: formattedName, PackageImportPath: getPackageImportPath(), - IncludeImports: !noDot, - ImportPackage: !internal, + IncludeImports: !conf.NoDot, + ImportPackage: !conf.Internal, } targetFile := fmt.Sprintf("%s_test.go", specFilePrefix) - if fileExists(targetFile) { - return fmt.Errorf("%s already exists.", targetFile) + if internal.FileExists(targetFile) { + command.AbortWith("{{bold}}%s{{/}} already exists", targetFile) } else { fmt.Printf("Generating ginkgo test for %s in:\n %s\n", data.Subject, targetFile) } f, err := os.Create(targetFile) - if err != nil { - return err - } + command.AbortIfError("Failed to create test file:", err) defer f.Close() var templateText string - if customTestFile != "" { - tpl, err := ioutil.ReadFile(customTestFile) - if err != nil { - panic(err.Error()) - } + if conf.CustomTemplate != "" { + tpl, err := ioutil.ReadFile(conf.CustomTemplate) + command.AbortIfError("Failed to read custom template file:", err) templateText = string(tpl) - } else if agouti { + } else if conf.Agouti { templateText = agoutiSpecText } else { templateText = specText } specTemplate, err := template.New("spec").Funcs(sprig.TxtFuncMap()).Parse(templateText) - if err != nil { - return err - } + command.AbortIfError("Failed to read parse test template:", err) specTemplate.Execute(f, data) - goFmt(targetFile) - return nil + internal.GoFmt(targetFile) } func formatSubject(name string) string { diff --git a/ginkgo/generators/generate_templates.go b/ginkgo/generators/generate_templates.go new file mode 100644 index 0000000000..bdc7a357de --- /dev/null +++ b/ginkgo/generators/generate_templates.go @@ -0,0 +1,41 @@ +package generators + +var specText = `package {{.Package}} + +import ( + {{if .IncludeImports}}. "github.com/onsi/ginkgo"{{end}} + {{if .IncludeImports}}. "github.com/onsi/gomega"{{end}} + + {{if .ImportPackage}}"{{.PackageImportPath}}"{{end}} +) + +var _ = Describe("{{.Subject}}", func() { + +}) +` + +var agoutiSpecText = `package {{.Package}} + +import ( + {{if .IncludeImports}}. "github.com/onsi/ginkgo"{{end}} + {{if .IncludeImports}}. "github.com/onsi/gomega"{{end}} + "github.com/sclevine/agouti" + . "github.com/sclevine/agouti/matchers" + + {{if .ImportPackage}}"{{.PackageImportPath}}"{{end}} +) + +var _ = Describe("{{.Subject}}", func() { + var page *agouti.Page + + BeforeEach(func() { + var err error + page, err = agoutiDriver.NewPage() + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(page.Destroy()).To(Succeed()) + }) +}) +` diff --git a/ginkgo/generators/generators_common.go b/ginkgo/generators/generators_common.go new file mode 100644 index 0000000000..27f79eaaf4 --- /dev/null +++ b/ginkgo/generators/generators_common.go @@ -0,0 +1,48 @@ +package generators + +import ( + "go/build" + "os" + "path/filepath" + "strings" + + "github.com/onsi/ginkgo/ginkgo/command" +) + +type GeneratorsConfig struct { + Agouti, NoDot, Internal bool + CustomTemplate string +} + +func getPackageAndFormattedName() (string, string, string) { + path, err := os.Getwd() + command.AbortIfError("Could not get current working diretory:", err) + + dirName := strings.Replace(filepath.Base(path), "-", "_", -1) + dirName = strings.Replace(dirName, " ", "_", -1) + + pkg, err := build.ImportDir(path, 0) + packageName := pkg.Name + if err != nil { + packageName = dirName + } + + formattedName := prettifyPackageName(filepath.Base(path)) + return packageName, dirName, formattedName +} + +func prettifyPackageName(name string) string { + name = strings.Replace(name, "-", " ", -1) + name = strings.Replace(name, "_", " ", -1) + name = strings.Title(name) + name = strings.Replace(name, " ", "", -1) + return name +} + +func determinePackageName(name string, internal bool) string { + if internal { + return name + } + + return name + "_test" +} diff --git a/ginkgo/ginkgo_cli_suite_test.go b/ginkgo/ginkgo_cli_suite_test.go new file mode 100644 index 0000000000..838c0259b4 --- /dev/null +++ b/ginkgo/ginkgo_cli_suite_test.go @@ -0,0 +1,17 @@ +package main + +import ( + "testing" + + "github.com/onsi/ginkgo/internal/test_helpers" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestGinkgoCLI(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Ginkgo CLI Suite") +} + +var DOC_ANCHORS = test_helpers.LoadMarkdownHeadingAnchors("../docs/index.md") diff --git a/ginkgo/ginkgo_cli_test.go b/ginkgo/ginkgo_cli_test.go new file mode 100644 index 0000000000..6933a3e76b --- /dev/null +++ b/ginkgo/ginkgo_cli_test.go @@ -0,0 +1,17 @@ +package main + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Command Documentation Links", func() { + It("all commands with doc links point to valid documentation", func() { + commands := GenerateCommands() + for _, command := range commands { + if command.DocLink != "" { + Ω(command.DocLink).Should(BeElementOf(DOC_ANCHORS)) + } + } + }) +}) diff --git a/ginkgo/help_command.go b/ginkgo/help_command.go deleted file mode 100644 index 23b1d2f117..0000000000 --- a/ginkgo/help_command.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "flag" - "fmt" -) - -func BuildHelpCommand() *Command { - return &Command{ - Name: "help", - FlagSet: flag.NewFlagSet("help", flag.ExitOnError), - UsageCommand: "ginkgo help ", - Usage: []string{ - "Print usage information. If a command is passed in, print usage information just for that command.", - }, - Command: printHelp, - } -} - -func printHelp(args []string, additionalArgs []string) { - if len(args) == 0 { - usage() - } else { - command, found := commandMatching(args[0]) - if !found { - complainAndQuit(fmt.Sprintf("Unknown command: %s", args[0])) - } - - usageForCommand(command, true) - } -} diff --git a/ginkgo/internal/cli_internal_suite_test.go b/ginkgo/internal/cli_internal_suite_test.go new file mode 100644 index 0000000000..83fc1280b4 --- /dev/null +++ b/ginkgo/internal/cli_internal_suite_test.go @@ -0,0 +1,16 @@ +package internal_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCLIInternalSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CLI Internal Suite") +} + +var _ = It(`These unit tests do not cover all the functionality in ginkgo/intenral. +The run, compile, and profiles functions are all integration tests in ginkgo/integration.`, func() {}) diff --git a/ginkgo/internal/compile.go b/ginkgo/internal/compile.go new file mode 100644 index 0000000000..65031eb222 --- /dev/null +++ b/ginkgo/internal/compile.go @@ -0,0 +1,61 @@ +package internal + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/onsi/ginkgo/config" +) + +func CompileSuite(suite TestSuite, goFlagsConfig config.GoFlagsConfigType) TestSuite { + if suite.PathToCompiledTest != "" { + return suite + } + + suite.CompilationError = nil + + path, err := filepath.Abs(filepath.Join(suite.Path, suite.PackageName+".test")) + if err != nil { + suite.CompilationError = fmt.Errorf("Failed to compute compilation target path:\n%s", err.Error()) + return suite + } + + args, err := config.GenerateGoTestCompileArgs(goFlagsConfig, path, suite.Path) + if err != nil { + suite.CompilationError = fmt.Errorf("Failed to generate go test compile flags:\n%s", err.Error()) + return suite + } + + cmd := exec.Command("go", args...) + output, err := cmd.CombinedOutput() + if err != nil { + if len(output) > 0 { + suite.CompilationError = fmt.Errorf("Failed to compile %s:\n\n%s", suite.PackageName, output) + } else { + suite.CompilationError = fmt.Errorf("Failed to compile %s\n%s", suite.PackageName, err.Error()) + } + return suite + } + + if len(output) > 0 { + fmt.Println(string(output)) + } + + if !FileExists(path) { + suite.CompilationError = fmt.Errorf("Failed to compile %s:\nOutput file %s could not be found", suite.PackageName, path) + return suite + } + + suite.PathToCompiledTest = path + return suite +} + +func Cleanup(suites ...TestSuite) { + for _, suite := range suites { + if !suite.Precompiled { + os.Remove(suite.PathToCompiledTest) + } + } +} diff --git a/ginkgo/internal/profiles.go b/ginkgo/internal/profiles.go new file mode 100644 index 0000000000..c51ca4f4ab --- /dev/null +++ b/ginkgo/internal/profiles.go @@ -0,0 +1,83 @@ +package internal + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + + "github.com/onsi/ginkgo/config" +) + +func FinalizeProfilesForSuites(suites []TestSuite, cliConfig config.GinkgoCLIConfigType, goFlagsConfig config.GoFlagsConfigType) error { + if goFlagsConfig.Cover { + if cliConfig.KeepSeparateCoverprofiles { + if cliConfig.OutputDir != "" { + // move separate cover profiles to the output directory, appropriately namespaced + for _, suite := range suites { + src := filepath.Join(suite.Path, goFlagsConfig.CoverProfile) + dst := filepath.Join(cliConfig.OutputDir, suite.NamespacedName()+"_"+goFlagsConfig.CoverProfile) + fmt.Println(src, dst) + err := os.Rename(src, dst) + if err != nil { + return err + } + } + } + } else { + // merge cover profiles + coverProfiles := []string{} + for _, suite := range suites { + coverProfiles = append(coverProfiles, filepath.Join(suite.Path, goFlagsConfig.CoverProfile)) + } + dst := goFlagsConfig.CoverProfile + if cliConfig.OutputDir != "" { + dst = filepath.Join(cliConfig.OutputDir, goFlagsConfig.CoverProfile) + } + err := MergeAndCleanupCoverProfiles(coverProfiles, dst) + if err != nil { + return err + } + } + } + + return nil +} + +//loads each profile, combines them, deletes them, stores them in destination +func MergeAndCleanupCoverProfiles(profiles []string, destination string) error { + combined := &bytes.Buffer{} + modeRegex := regexp.MustCompile(`^mode: .*\n`) + for i, profile := range profiles { + contents, err := ioutil.ReadFile(profile) + if err != nil { + return fmt.Errorf("Unable to read coverage file %s:\n%s", profile, err.Error()) + } + os.Remove(profile) + + // remove the cover mode line from every file + // except the first one + if i > 0 { + contents = modeRegex.ReplaceAll(contents, []byte{}) + } + + _, err = combined.Write(contents) + + // Add a newline to the end of every file if missing. + if err == nil && len(contents) > 0 && contents[len(contents)-1] != '\n' { + _, err = combined.Write([]byte("\n")) + } + + if err != nil { + return fmt.Errorf("Unable to append to coverprofile:\n%s", err.Error()) + } + } + + err := ioutil.WriteFile(destination, combined.Bytes(), 0666) + if err != nil { + return fmt.Errorf("Unable to create combined cover profile:\n%s", err.Error()) + } + return nil +} diff --git a/ginkgo/internal/run.go b/ginkgo/internal/run.go new file mode 100644 index 0000000000..246e10e5aa --- /dev/null +++ b/ginkgo/internal/run.go @@ -0,0 +1,206 @@ +package internal + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "syscall" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/ginkgo/command" + "github.com/onsi/ginkgo/internal/parallel_support" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" +) + +func RunCompiledSuite(suite TestSuite, ginkgoConfig config.GinkgoConfigType, reporterConfig config.DefaultReporterConfigType, cliConfig config.GinkgoCLIConfigType, goFlagsConfig config.GoFlagsConfigType, additionalArgs []string) TestSuite { + suite.Passed = false + suite.HasProgrammaticFocus = false + + if suite.PathToCompiledTest == "" { + return suite + } + + if suite.IsGinkgo && cliConfig.ComputedNodes() > 1 { + suite = runParallel(suite, ginkgoConfig, reporterConfig, cliConfig, goFlagsConfig, additionalArgs) + } else if suite.IsGinkgo { + suite = runSerial(suite, ginkgoConfig, reporterConfig, cliConfig, goFlagsConfig, additionalArgs) + } else { + suite = runGoTest(suite, cliConfig) + } + runAfterRunHook(cliConfig.AfterRunHook, reporterConfig.NoColor, suite) + return suite +} + +func buildAndStartCommand(suite TestSuite, args []string, pipeToStdout bool) (*exec.Cmd, *bytes.Buffer) { + buf := &bytes.Buffer{} + cmd := exec.Command(suite.PathToCompiledTest, args...) + cmd.Dir = suite.Path + if pipeToStdout { + cmd.Stderr = io.MultiWriter(os.Stdout, buf) + cmd.Stdout = os.Stdout + } else { + cmd.Stderr = buf + cmd.Stdout = buf + } + err := cmd.Start() + command.AbortIfError("Failed to start test suite", err) + + return cmd, buf +} + +func checkForNoTestsWarning(buf *bytes.Buffer) bool { + if strings.Contains(buf.String(), "warning: no tests to run") { + fmt.Fprintf(os.Stderr, `Found no test suites, did you forget to run "ginkgo bootstrap"?`) + return true + } + return false +} + +func runGoTest(suite TestSuite, cliConfig config.GinkgoCLIConfigType) TestSuite { + cmd, buf := buildAndStartCommand(suite, []string{"-test.v"}, true) + + cmd.Wait() + + exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() + suite.Passed = (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE) + suite.Passed = !(checkForNoTestsWarning(buf) && cliConfig.RequireSuite) && suite.Passed + + return suite +} + +func runSerial(suite TestSuite, ginkgoConfig config.GinkgoConfigType, reporterConfig config.DefaultReporterConfigType, cliConfig config.GinkgoCLIConfigType, goFlagsConfig config.GoFlagsConfigType, additionalArgs []string) TestSuite { + args, err := config.GenerateTestRunArgs(ginkgoConfig, reporterConfig, goFlagsConfig) + command.AbortIfError("Failed to generate test run arguments", err) + args = append([]string{"--test.timeout=0"}, args...) + args = append(args, additionalArgs...) + + cmd, buf := buildAndStartCommand(suite, args, true) + + cmd.Wait() + + exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() + suite.HasProgrammaticFocus = (exitStatus == types.GINKGO_FOCUS_EXIT_CODE) + suite.Passed = (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE) + suite.Passed = !(checkForNoTestsWarning(buf) && cliConfig.RequireSuite) && suite.Passed + + return suite +} + +func runParallel(suite TestSuite, ginkgoConfig config.GinkgoConfigType, reporterConfig config.DefaultReporterConfigType, cliConfig config.GinkgoCLIConfigType, goFlagsConfig config.GoFlagsConfigType, additionalArgs []string) TestSuite { + type nodeResult struct { + passed bool + hasProgrammaticFocus bool + } + + numNodes := cliConfig.ComputedNodes() + nodeOutput := make([]*bytes.Buffer, numNodes) + coverProfiles := []string{} + nodeResults := make(chan nodeResult) + + server, err := parallel_support.NewServer(numNodes, reporters.NewDefaultReporter(reporterConfig, formatter.ColorableStdOut)) + command.AbortIfError("Failed to start parallel spec server", err) + server.Start() + defer server.Close() + + for node := 1; node <= numNodes; node++ { + nodeGinkgoConfig := ginkgoConfig + nodeGinkgoConfig.ParallelNode, nodeGinkgoConfig.ParallelTotal, nodeGinkgoConfig.ParallelHost = node, numNodes, server.Address() + + nodeGoFlagsConfig := goFlagsConfig + if goFlagsConfig.Cover { + nodeGoFlagsConfig.CoverProfile = fmt.Sprintf("%s.%d", goFlagsConfig.CoverProfile, node) + coverProfiles = append(coverProfiles, nodeGoFlagsConfig.CoverProfile) + } + + args, err := config.GenerateTestRunArgs(nodeGinkgoConfig, reporterConfig, nodeGoFlagsConfig) + command.AbortIfError("Failed to generate test run argumnets", err) + args = append([]string{"--test.timeout=0"}, args...) + args = append(args, additionalArgs...) + + cmd, buf := buildAndStartCommand(suite, args, false) + nodeOutput[node-1] = buf + server.RegisterAlive(node, func() bool { return cmd.ProcessState == nil || !cmd.ProcessState.Exited() }) + + go func() { + cmd.Wait() + exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() + nodeResults <- nodeResult{ + passed: (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE), + hasProgrammaticFocus: exitStatus == types.GINKGO_FOCUS_EXIT_CODE, + } + }() + } + + suite.Passed = true + for node := 1; node <= cliConfig.ComputedNodes(); node++ { + result := <-nodeResults + suite.Passed = suite.Passed && result.passed + suite.HasProgrammaticFocus = suite.HasProgrammaticFocus || result.hasProgrammaticFocus + } + + select { + case <-server.Done: + fmt.Println("") + case <-time.After(time.Second): + //the serve never got back to us. Something must have gone wrong. + fmt.Fprintln(os.Stderr, "** Ginkgo timed out waiting for all parallel nodes to report back. **") + fmt.Fprintf(os.Stderr, "%s (%s)\n", suite.PackageName, suite.Path) + for node := 1; node <= cliConfig.ComputedNodes(); node++ { + fmt.Fprintf(os.Stderr, "Output from node %d\n:", node) + fmt.Fprintln(os.Stderr, formatter.Fi(1, "%s", nodeOutput[node-1].String())) + } + } + + for node := 1; node <= cliConfig.ComputedNodes(); node++ { + output := nodeOutput[node-1].String() + if node == 1 { + suite.Passed = !(checkForNoTestsWarning(nodeOutput[node-1]) && cliConfig.RequireSuite) && suite.Passed + } + if strings.Contains(output, "deprecated Ginkgo functionality") { + fmt.Fprintln(os.Stderr, output) + } + } + + if len(coverProfiles) > 0 { + err := MergeAndCleanupCoverProfiles(coverProfiles, filepath.Join(suite.Path, goFlagsConfig.CoverProfile)) + command.AbortIfError("Failed to combine cover profiles", err) + } + + return suite +} + +func runAfterRunHook(command string, noColor bool, suite TestSuite) { + if command == "" { + return + } + f := formatter.NewWithNoColorBool(noColor) + + // Allow for string replacement to pass input to the command + passed := "[FAIL]" + if suite.Passed { + passed = "[PASS]" + } + command = strings.Replace(command, "(ginkgo-suite-passed)", passed, -1) + command = strings.Replace(command, "(ginkgo-suite-name)", suite.PackageName, -1) + + // Must break command into parts + splitArgs := regexp.MustCompile(`'.+'|".+"|\S+`) + parts := splitArgs.FindAllString(command, -1) + + output, err := exec.Command(parts[0], parts[1:]...).CombinedOutput() + if err != nil { + fmt.Fprintln(formatter.ColorableStdOut, f.Fi(0, "{{red}}{{bold}}After-run-hook failed:{{/}}")) + fmt.Fprintln(formatter.ColorableStdOut, f.Fi(1, "{{red}}%s{{/}}", output)) + } else { + fmt.Fprintln(formatter.ColorableStdOut, f.Fi(0, "{{green}}{{bold}}After-run-hook succeeded:{{/}}")) + fmt.Fprintln(formatter.ColorableStdOut, f.Fi(1, "{{green}}%s{{/}}", output)) + } +} diff --git a/ginkgo/internal/test_suite.go b/ginkgo/internal/test_suite.go new file mode 100644 index 0000000000..715a862dab --- /dev/null +++ b/ginkgo/internal/test_suite.go @@ -0,0 +1,184 @@ +package internal + +import ( + "errors" + "io/ioutil" + "os" + "path" + "path/filepath" + "regexp" + "strings" + + "github.com/onsi/ginkgo/config" +) + +type TestSuite struct { + Path string + PackageName string + IsGinkgo bool + Precompiled bool + + PathToCompiledTest string + CompilationError error + Passed bool + HasProgrammaticFocus bool +} + +func (ts TestSuite) NamespacedName() string { + name := relPath(ts.Path) + name = strings.TrimLeft(name, "."+string(filepath.Separator)) + name = strings.ReplaceAll(name, string(filepath.Separator), "_") + name = strings.ReplaceAll(name, " ", "_") + if name == "" { + return ts.PackageName + } + return name +} + +func FindSuites(args []string, cliConfig config.GinkgoCLIConfigType, allowPrecompiled bool) ([]TestSuite, []string) { + suites := []TestSuite{} + + if len(args) > 0 { + for _, arg := range args { + if allowPrecompiled { + suite, err := precompiledTestSuite(arg) + if err == nil { + suites = append(suites, suite) + continue + } + } + recurseForSuite := cliConfig.Recurse + if strings.HasSuffix(arg, "/...") && arg != "/..." { + arg = arg[:len(arg)-4] + recurseForSuite = true + } + suites = append(suites, suitesInDir(arg, recurseForSuite)...) + } + } else { + suites = suitesInDir(".", cliConfig.Recurse) + } + + skippedPackages := []string{} + if cliConfig.SkipPackage != "" { + skipFilters := strings.Split(cliConfig.SkipPackage, ",") + filteredSuites := []TestSuite{} + for _, suite := range suites { + skip := false + for _, skipFilter := range skipFilters { + if strings.Contains(suite.Path, skipFilter) { + skip = true + break + } + } + if skip { + skippedPackages = append(skippedPackages, suite.Path) + } else { + filteredSuites = append(filteredSuites, suite) + } + } + suites = filteredSuites + } + + return suites, skippedPackages +} + +func precompiledTestSuite(path string) (TestSuite, error) { + info, err := os.Stat(path) + if err != nil { + return TestSuite{}, err + } + + if info.IsDir() { + return TestSuite{}, errors.New("this is a directory, not a file") + } + + if filepath.Ext(path) != ".test" { + return TestSuite{}, errors.New("this is not a .test binary") + } + + if info.Mode()&0111 == 0 { + return TestSuite{}, errors.New("this is not executable") + } + + dir := relPath(filepath.Dir(path)) + packageName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + + path, err = filepath.Abs(path) + if err != nil { + return TestSuite{}, err + } + + return TestSuite{ + Path: dir, + PackageName: packageName, + IsGinkgo: true, + Precompiled: true, + PathToCompiledTest: path, + }, nil +} + +func suitesInDir(dir string, recurse bool) []TestSuite { + suites := []TestSuite{} + + if path.Base(dir) == "vendor" { + return suites + } + + files, _ := ioutil.ReadDir(dir) + re := regexp.MustCompile(`^[^._].*_test\.go$`) + for _, file := range files { + if !file.IsDir() && re.Match([]byte(file.Name())) { + suite := TestSuite{ + Path: relPath(dir), + PackageName: packageNameForSuite(dir), + IsGinkgo: filesHaveGinkgoSuite(dir, files), + } + suites = append(suites, suite) + break + } + } + + if recurse { + re = regexp.MustCompile(`^[._]`) + for _, file := range files { + if file.IsDir() && !re.Match([]byte(file.Name())) { + suites = append(suites, suitesInDir(dir+"/"+file.Name(), recurse)...) + } + } + } + + return suites +} + +func relPath(dir string) string { + dir, _ = filepath.Abs(dir) + cwd, _ := os.Getwd() + dir, _ = filepath.Rel(cwd, filepath.Clean(dir)) + + if string(dir[0]) != "." { + dir = "." + string(filepath.Separator) + dir + } + + return dir +} + +func packageNameForSuite(dir string) string { + path, _ := filepath.Abs(dir) + return filepath.Base(path) +} + +func filesHaveGinkgoSuite(dir string, files []os.FileInfo) bool { + reTestFile := regexp.MustCompile(`_test\.go$`) + reGinkgo := regexp.MustCompile(`package ginkgo|\/ginkgo"`) + + for _, file := range files { + if !file.IsDir() && reTestFile.Match([]byte(file.Name())) { + contents, _ := ioutil.ReadFile(dir + "/" + file.Name()) + if reGinkgo.Match(contents) { + return true + } + } + } + + return false +} diff --git a/ginkgo/internal/testsuite_test.go b/ginkgo/internal/testsuite_test.go new file mode 100644 index 0000000000..f05883129e --- /dev/null +++ b/ginkgo/internal/testsuite_test.go @@ -0,0 +1,290 @@ +package internal_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/ginkgo/internal" + . "github.com/onsi/gomega" +) + +func TS(path string, pkgName string, isGinkgo bool) TestSuite { + return TestSuite{ + Path: path, + PackageName: pkgName, + IsGinkgo: isGinkgo, + Precompiled: false, + } +} + +func PTS(path string, pkgName string, isGinkgo bool, pathToCompiledTest string) TestSuite { + return TestSuite{ + Path: path, + PackageName: pkgName, + IsGinkgo: isGinkgo, + Precompiled: true, + PathToCompiledTest: pathToCompiledTest, + } +} + +var _ = Describe("TestSuite", func() { + var tmpDir string + var origWd string + var cliConf config.GinkgoCLIConfigType + + writeFile := func(folder string, filename string, content string, mode os.FileMode) { + path := filepath.Join(tmpDir, folder) + err := os.MkdirAll(path, 0700) + Ω(err).ShouldNot(HaveOccurred()) + + path = filepath.Join(path, filename) + ioutil.WriteFile(path, []byte(content), mode) + } + + BeforeEach(func() { + cliConf = config.GinkgoCLIConfigType{} + + var err error + tmpDir, err = ioutil.TempDir("/tmp", "ginkgo") + Ω(err).ShouldNot(HaveOccurred()) + + origWd, err = os.Getwd() + Ω(err).ShouldNot(HaveOccurred()) + Ω(os.Chdir(tmpDir)).Should(Succeed()) + + //go files in the root directory (no tests) + writeFile("/", "main.go", "package main", 0666) + + //non-go files in a nested directory + writeFile("/redherring", "big_test.jpg", "package ginkgo", 0666) + + //ginkgo tests in ignored go files + writeFile("/ignored", ".ignore_dot_test.go", `import "github.com/onsi/ginkgo"`, 0666) + writeFile("/ignored", "_ignore_underscore_test.go", `import "github.com/onsi/ginkgo"`, 0666) + + //non-ginkgo tests in a nested directory + writeFile("/professorplum", "professorplum_test.go", `import "testing"`, 0666) + + //ginkgo tests in a nested directory + writeFile("/colonelmustard", "colonelmustard_test.go", `import "github.com/onsi/ginkgo"`, 0666) + + //ginkgo tests in a deeply nested directory + writeFile("/colonelmustard/library", "library_test.go", `import "github.com/onsi/ginkgo"`, 0666) + + //ginkgo tests deeply nested in a vendored dependency + writeFile("/vendor/mrspeacock/lounge", "lounge_test.go", `import "github.com/onsi/ginkgo"`, 0666) + + //a precompiled ginkgo test + writeFile("/precompiled-dir", "precompiled.test", `fake-binary-file`, 0777) + writeFile("/precompiled-dir", "some-other-binary", `fake-binary-file`, 0777) + writeFile("/precompiled-dir", "nonexecutable.test", `fake-binary-file`, 0666) + }) + + AfterEach(func() { + Ω(os.Chdir(origWd)).Should(Succeed()) + os.RemoveAll(tmpDir) + }) + + Describe("Finding Suites", func() { + Context("when passed no args", func() { + Context("when told to recurse", func() { + BeforeEach(func() { + cliConf.Recurse = true + }) + + It("recurses through the current directory, returning all identified tests and skipping vendored, ignored, and precompiled tests", func() { + suites, skipped := FindSuites([]string{}, cliConf, false) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(ConsistOf( + TS("./professorplum", "professorplum", false), + TS("./colonelmustard", "colonelmustard", true), + TS("./colonelmustard/library", "library", true), + )) + }) + }) + + Context("when told to recurse and there is a skip-package filter", func() { + BeforeEach(func() { + cliConf.Recurse = true + cliConf.SkipPackage = "professorplum,library,floop" + }) + + It("recurses through the current directory, returning all identified tests and skipping vendored, ignored, and precompiled tests", func() { + suites, skipped := FindSuites([]string{}, cliConf, false) + Ω(skipped).Should(ConsistOf( + "./professorplum", + "./colonelmustard/library", + )) + Ω(suites).Should(ConsistOf( + TS("./colonelmustard", "colonelmustard", true), + )) + }) + }) + + Context("when there are no tests in the current directory", func() { + BeforeEach(func() { + cliConf.Recurse = false + }) + + It("returns empty", func() { + suites, skipped := FindSuites([]string{}, cliConf, false) + Ω(suites).Should(BeEmpty()) + Ω(skipped).Should(BeEmpty()) + }) + }) + + Context("when told not to recurse", func() { + BeforeEach(func() { + Ω(os.Chdir("./colonelmustard")).Should(Succeed()) + }) + + It("returns tests in the current directory if present", func() { + suites, skipped := FindSuites([]string{}, cliConf, false) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(ConsistOf( + TS(".", "colonelmustard", true), + )) + }) + }) + }) + + Context("when passed args", func() { + Context("when told to recurse", func() { + BeforeEach(func() { + cliConf.Recurse = true + }) + + It("recurses through the passed-in directories, returning all identified tests and skipping vendored, ignored, and precompiled tests", func() { + suites, skipped := FindSuites([]string{"precompiled-dir", "colonelmustard"}, cliConf, false) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(ConsistOf( + TS("./colonelmustard", "colonelmustard", true), + TS("./colonelmustard/library", "library", true), + )) + }) + }) + + Context("when told to recurse and there is a skip-package filter", func() { + BeforeEach(func() { + cliConf.Recurse = true + cliConf.SkipPackage = "library" + }) + + It("recurses through the passed-in directories, returning all identified tests and skipping vendored, ignored, and precompiled tests", func() { + suites, skipped := FindSuites([]string{"precompiled-dir", "professorplum", "colonelmustard"}, cliConf, false) + Ω(skipped).Should(ConsistOf( + "./colonelmustard/library", + )) + Ω(suites).Should(ConsistOf( + TS("./professorplum", "professorplum", false), + TS("./colonelmustard", "colonelmustard", true), + )) + }) + }) + + Context("when told not to recurse", func() { + BeforeEach(func() { + cliConf.Recurse = false + }) + + It("returns test packages at the passed in arguments", func() { + suites, skipped := FindSuites([]string{"precompiled-dir", "colonelmustard", "professorplum", "ignored"}, cliConf, false) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(ConsistOf( + TS("./professorplum", "professorplum", false), + TS("./colonelmustard", "colonelmustard", true), + )) + }) + }) + + Context("when told not to recurse, but an arg has /...", func() { + BeforeEach(func() { + cliConf.Recurse = false + }) + + It("recurses through the directories it is told to recurse through, returning all identified tests and skipping vendored, ignored, and precompiled tests", func() { + suites, skipped := FindSuites([]string{"precompiled-dir", "colonelmustard/...", "professorplum/...", "ignored/..."}, cliConf, false) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(ConsistOf( + TS("./professorplum", "professorplum", false), + TS("./colonelmustard", "colonelmustard", true), + TS("./colonelmustard/library", "library", true), + )) + }) + }) + + Context("when told not to recurse and there is a skip-package filter", func() { + BeforeEach(func() { + cliConf.Recurse = false + cliConf.SkipPackage = "library,plum" + }) + + It("returns skips packages that match", func() { + suites, skipped := FindSuites([]string{"colonelmustard", "professorplum", "colonelmustard/library"}, cliConf, false) + Ω(skipped).Should(ConsistOf( + "./professorplum", + "./colonelmustard/library", + )) + Ω(suites).Should(ConsistOf( + TS("./colonelmustard", "colonelmustard", true), + )) + }) + }) + + Context("when pointed at a directory containing a precompiled test suite", func() { + It("returns nothing", func() { + suites, skipped := FindSuites([]string{"precompiled-dir"}, cliConf, false) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(BeEmpty()) + }) + }) + + Context("when pointed at a precompiled test suite specifically", func() { + It("returns the precompiled suite", func() { + path, err := filepath.Abs("./precompiled-dir/precompiled.test") + Ω(err).ShouldNot(HaveOccurred()) + suites, skipped := FindSuites([]string{"precompiled-dir/precompiled.test"}, cliConf, true) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(ConsistOf( + PTS("./precompiled-dir", "precompiled", true, path), + )) + }) + }) + + Context("when pointed at a fake precompiled test", func() { + It("returns nothing", func() { + suites, skipped := FindSuites([]string{"precompiled-dir/some-other-binary"}, cliConf, true) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(BeEmpty()) + + suites, skipped = FindSuites([]string{"precompiled-dir/nonexecutable.test"}, cliConf, true) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(BeEmpty()) + }) + }) + + Context("when pointed at a precompiled test suite specifically but allowPrecompiled is false", func() { + It("returns nothing", func() { + suites, skipped := FindSuites([]string{"precompiled-dir/some-other-binary"}, cliConf, false) + Ω(skipped).Should(BeEmpty()) + Ω(suites).Should(BeEmpty()) + }) + }) + }) + + Describe("NamespacedName", func() { + It("generates a name basd on the relative path to the package", func() { + plum := TS("./professorplum", "professorplum", false) + library := TS("./colonelmustard/library", "library", true) + root := TS(".", "root", true) + + Ω(plum.NamespacedName()).Should(Equal("professorplum")) + Ω(library.NamespacedName()).Should(Equal("colonelmustard_library")) + Ω(root.NamespacedName()).Should(Equal("root")) + }) + }) + }) +}) diff --git a/ginkgo/internal/utils.go b/ginkgo/internal/utils.go new file mode 100644 index 0000000000..1f7afb0d6a --- /dev/null +++ b/ginkgo/internal/utils.go @@ -0,0 +1,47 @@ +package internal + +import ( + "fmt" + "os" + "os/exec" + + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/ginkgo/command" +) + +func FileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func GoFmt(path string) { + out, err := exec.Command("go", "fmt", path).CombinedOutput() + if err != nil { + command.AbortIfError(fmt.Sprintf("Could not fmt:\n%s\n", string(out)), err) + } +} + +func PluralizedWord(singular, plural string, count int) string { + if count == 1 { + return singular + } + return plural +} + +func FailedSuitesReport(suites []TestSuite, f formatter.Formatter) string { + out := "" + out += "There were failures detected in the following suites:\n" + + maxPackageNameLength := 0 + for _, suite := range suites { + if len(suite.PackageName) > maxPackageNameLength { + maxPackageNameLength = len(suite.PackageName) + } + } + + packageNameFormatter := fmt.Sprintf("%%%ds", maxPackageNameLength) + for _, suite := range suites { + out += f.Fi(1, "{{red}}"+packageNameFormatter+" {{gray}}%s{{/}}\n", suite.PackageName, suite.Path) + } + return out +} diff --git a/ginkgo/internal/utils_test.go b/ginkgo/internal/utils_test.go new file mode 100644 index 0000000000..f2768fe640 --- /dev/null +++ b/ginkgo/internal/utils_test.go @@ -0,0 +1,73 @@ +package internal_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/ginkgo/internal" + . "github.com/onsi/gomega" +) + +var _ = Describe("Utils", func() { + Describe("FileExists", func() { + var tmpDir string + + BeforeEach(func() { + var err error + tmpDir, err = ioutil.TempDir("/tmp", "ginkgo") + Ω(err).ShouldNot(HaveOccurred()) + }) + + AfterEach(func() { + Ω(os.RemoveAll(tmpDir)).Should(Succeed()) + }) + + It("returns true if the path exists", func() { + path := filepath.Join(tmpDir, "foo") + Ω(ioutil.WriteFile(path, []byte("foo"), 0666)).Should(Succeed()) + Ω(internal.FileExists(path)).Should(BeTrue()) + }) + + It("returns false if the path does not exist", func() { + path := filepath.Join(tmpDir, "foo") + Ω(internal.FileExists(path)).Should(BeFalse()) + }) + }) + + Describe("PluralizedWord", func() { + It("returns singular when count is 1", func() { + Ω(internal.PluralizedWord("s", "p", 1)).Should(Equal("s")) + }) + + It("returns plural when count is not 1", func() { + Ω(internal.PluralizedWord("s", "p", 0)).Should(Equal("p")) + Ω(internal.PluralizedWord("s", "p", 2)).Should(Equal("p")) + Ω(internal.PluralizedWord("s", "p", 10)).Should(Equal("p")) + }) + }) + + Describe("FailedSuiteReport", func() { + var f formatter.Formatter + BeforeEach(func() { + f = formatter.New(formatter.ColorModePassthrough) + }) + + It("generates a nicely frormatter report", func() { + suites := []internal.TestSuite{ + TS("path-A", "package-A", true), + TS("path-B", "B", true), + TS("path-to/package-C", "the-C-package", true), + } + Ω(internal.FailedSuitesReport(suites, f)).Should(HavePrefix(strings.Join([]string{ + "There were failures detected in the following suites:", + " {{red}} package-A {{gray}}path-A{{/}}", + " {{red}} B {{gray}}path-B{{/}}", + " {{red}}the-C-package {{gray}}path-to/package-C{{/}}", + }, "\n"))) + }) + }) +}) diff --git a/ginkgo/interrupthandler/interrupt_handler.go b/ginkgo/interrupthandler/interrupt_handler.go index ec456bf292..8fd7329bc0 100644 --- a/ginkgo/interrupthandler/interrupt_handler.go +++ b/ginkgo/interrupthandler/interrupt_handler.go @@ -3,50 +3,47 @@ package interrupthandler import ( "os" "os/signal" - "sync" "syscall" ) type InterruptHandler struct { - interruptCount int - lock *sync.Mutex - C chan bool + c chan interface{} } func NewInterruptHandler() *InterruptHandler { - h := &InterruptHandler{ - lock: &sync.Mutex{}, - C: make(chan bool), + handler := &InterruptHandler{ + c: make(chan interface{}), } - go h.handleInterrupt() + handler.registerForInterrupts() SwallowSigQuit() - return h + return handler } -func (h *InterruptHandler) WasInterrupted() bool { - h.lock.Lock() - defer h.lock.Unlock() +func (handler *InterruptHandler) registerForInterrupts() { + didRegister := make(chan struct{}) + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + close(didRegister) - return h.interruptCount > 0 -} - -func (h *InterruptHandler) handleInterrupt() { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c - <-c - signal.Stop(c) + close(handler.c) + }() + <-didRegister +} - h.lock.Lock() - h.interruptCount++ - if h.interruptCount == 1 { - close(h.C) - } else if h.interruptCount > 5 { - os.Exit(1) +func (handler *InterruptHandler) WasInterrupted() bool { + select { + case <-handler.c: + return true + default: + return false } - h.lock.Unlock() +} - go h.handleInterrupt() +func (handler *InterruptHandler) InterruptChannel() chan interface{} { + return handler.c } diff --git a/ginkgo/main.go b/ginkgo/main.go index ac725bf408..e6ad95ca43 100644 --- a/ginkgo/main.go +++ b/ginkgo/main.go @@ -62,12 +62,6 @@ passing `ginkgo watch` the `-r` flag will recursively detect all test suites und `watch` does not detect *new* packages. Moreover, changes in package X only rerun the tests for package X, tests for packages that depend on X are not rerun. -[OSX & Linux only] To receive (desktop) notifications when a test run completes: - - ginkgo -notify - -this is particularly useful with `ginkgo watch`. Notifications are currently only supported on OS X and require that you `brew install terminal-notifier` - Sometimes (to suss out race conditions/flakey tests, for example) you want to keep running a test suite until it fails. You can do this with: ginkgo -untilItFails @@ -91,10 +85,6 @@ this will explicitly export all the identifiers in Ginkgo and Gomega allowing yo to refresh this list and pull in any new identifiers. In particular, this will pull in any new Gomega matchers that get added. -To convert an existing XUnit style test suite to a Ginkgo-style test suite: - - ginkgo convert . - To unfocus tests: ginkgo unfocus @@ -127,182 +117,57 @@ To get more help: package main import ( - "flag" "fmt" "os" - "os/exec" - "strings" "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/ginkgo/testsuite" + "github.com/onsi/ginkgo/ginkgo/build" + "github.com/onsi/ginkgo/ginkgo/command" + "github.com/onsi/ginkgo/ginkgo/generators" + "github.com/onsi/ginkgo/ginkgo/nodot" + "github.com/onsi/ginkgo/ginkgo/outline" + "github.com/onsi/ginkgo/ginkgo/run" + "github.com/onsi/ginkgo/ginkgo/unfocus" + "github.com/onsi/ginkgo/ginkgo/watch" + "github.com/onsi/ginkgo/types" ) -const greenColor = "\x1b[32m" -const redColor = "\x1b[91m" -const defaultStyle = "\x1b[0m" -const lightGrayColor = "\x1b[37m" - -type Command struct { - Name string - AltName string - FlagSet *flag.FlagSet - Usage []string - UsageCommand string - Command func(args []string, additionalArgs []string) - SuppressFlagDocumentation bool - FlagDocSubstitute []string -} - -func (c *Command) Matches(name string) bool { - return c.Name == name || (c.AltName != "" && c.AltName == name) -} - -func (c *Command) Run(args []string, additionalArgs []string) { - c.FlagSet.Usage = usage - c.FlagSet.Parse(args) - c.Command(c.FlagSet.Args(), additionalArgs) -} - -var DefaultCommand *Command -var Commands []*Command - -func init() { - DefaultCommand = BuildRunCommand() - Commands = append(Commands, BuildWatchCommand()) - Commands = append(Commands, BuildBuildCommand()) - Commands = append(Commands, BuildBootstrapCommand()) - Commands = append(Commands, BuildGenerateCommand()) - Commands = append(Commands, BuildNodotCommand()) - Commands = append(Commands, BuildConvertCommand()) - Commands = append(Commands, BuildUnfocusCommand()) - Commands = append(Commands, BuildVersionCommand()) - Commands = append(Commands, BuildHelpCommand()) - Commands = append(Commands, BuildOutlineCommand()) -} - -func main() { - args := []string{} - additionalArgs := []string{} - - foundDelimiter := false - - for _, arg := range os.Args[1:] { - if !foundDelimiter { - if arg == "--" { - foundDelimiter = true - continue - } - } - - if foundDelimiter { - additionalArgs = append(additionalArgs, arg) - } else { - args = append(args, arg) - } - } - - if len(args) > 0 { - commandToRun, found := commandMatching(args[0]) - if found { - commandToRun.Run(args[1:], additionalArgs) - return - } - } - - DefaultCommand.Run(args, additionalArgs) -} - -func commandMatching(name string) (*Command, bool) { - for _, command := range Commands { - if command.Matches(name) { - return command, true - } - } - return nil, false -} - -func usage() { - fmt.Printf("Ginkgo Version %s\n\n", config.VERSION) - usageForCommand(DefaultCommand, false) - for _, command := range Commands { - fmt.Printf("\n") - usageForCommand(command, false) - } -} - -func usageForCommand(command *Command, longForm bool) { - fmt.Printf("%s\n%s\n", command.UsageCommand, strings.Repeat("-", len(command.UsageCommand))) - fmt.Printf("%s\n", strings.Join(command.Usage, "\n")) - if command.SuppressFlagDocumentation && !longForm { - fmt.Printf("%s\n", strings.Join(command.FlagDocSubstitute, "\n ")) - } else { - command.FlagSet.SetOutput(os.Stdout) - command.FlagSet.PrintDefaults() +var program command.Program + +func GenerateCommands() []command.Command { + return []command.Command{ + watch.BuildWatchCommand(), + build.BuildBuildCommand(), + generators.BuildBootstrapCommand(), + generators.BuildGenerateCommand(), + nodot.BuildNodotCommand(), + outline.BuildOutlineCommand(), + unfocus.BuildUnfocusCommand(), + BuildVersionCommand(), } } -func complainAndQuit(complaint string) { - fmt.Fprintf(os.Stderr, "%s\nFor usage instructions:\n\tginkgo help\n", complaint) - os.Exit(1) -} - -func findSuites(args []string, recurseForAll bool, skipPackage string, allowPrecompiled bool) ([]testsuite.TestSuite, []string) { - suites := []testsuite.TestSuite{} - - if len(args) > 0 { - for _, arg := range args { - if allowPrecompiled { - suite, err := testsuite.PrecompiledTestSuite(arg) - if err == nil { - suites = append(suites, suite) - continue - } - } - recurseForSuite := recurseForAll - if strings.HasSuffix(arg, "/...") && arg != "/..." { - arg = arg[:len(arg)-4] - recurseForSuite = true - } - suites = append(suites, testsuite.SuitesInDir(arg, recurseForSuite)...) - } - } else { - suites = testsuite.SuitesInDir(".", recurseForAll) - } - - skippedPackages := []string{} - if skipPackage != "" { - skipFilters := strings.Split(skipPackage, ",") - filteredSuites := []testsuite.TestSuite{} - for _, suite := range suites { - skip := false - for _, skipFilter := range skipFilters { - if strings.Contains(suite.Path, skipFilter) { - skip = true - break - } - } - if skip { - skippedPackages = append(skippedPackages, suite.Path) - } else { - filteredSuites = append(filteredSuites, suite) - } - } - suites = filteredSuites +func main() { + program = command.Program{ + Name: "ginkgo", + Heading: fmt.Sprintf("Ginkgo Version %s", config.VERSION), + Commands: GenerateCommands(), + DefaultCommand: run.BuildRunCommand(), + DeprecatedCommands: []command.DeprecatedCommand{ + {Name: "convert", Deprecation: types.Deprecations.Convert()}, + }, } - return suites, skippedPackages -} - -func goFmt(path string) { - out, err := exec.Command("go", "fmt", path).CombinedOutput() - if err != nil { - complainAndQuit("Could not fmt: " + err.Error() + "\n" + string(out)) - } + program.RunAndExit(os.Args) } -func pluralizedWord(singular, plural string, count int) string { - if count == 1 { - return singular +func BuildVersionCommand() command.Command { + return command.Command{ + Name: "version", + Usage: "ginkgo version", + ShortDoc: "Print Ginkgo's version", + Command: func(_ []string, _ []string) { + fmt.Printf("Ginkgo Version %s\n", config.VERSION) + }, } - return plural } diff --git a/ginkgo/nodot/nodot_command.go b/ginkgo/nodot/nodot_command.go new file mode 100644 index 0000000000..8f9cf74f06 --- /dev/null +++ b/ginkgo/nodot/nodot_command.go @@ -0,0 +1,70 @@ +package nodot + +import ( + "bufio" + "io/ioutil" + "os" + "path/filepath" + "regexp" + + "github.com/onsi/ginkgo/ginkgo/command" + "github.com/onsi/ginkgo/ginkgo/internal" +) + +func BuildNodotCommand() command.Command { + return command.Command{ + Name: "nodot", + Usage: "ginkgo nodot", + ShortDoc: "Update the nodot declarations in your test suite", + Documentation: `If you've bootstrapped a test suite using ginkgo bootstrap -nodot, run ginkgo nodot in a suite's direcotry to update the declarations in the generated bootstrap file. + +Any missing declarations (from, say, a recently added Gomega matcher) will be added to your bootstrap file. + +If you've renamed a declaration, that name will be honored and not overwritten.`, + DocLink: "avoiding-dot-imports", + Command: func(_ []string, _ []string) { + updateNodot() + }, + } +} + +func updateNodot() { + suiteFile, perm := findSuiteFile() + + data, err := ioutil.ReadFile(suiteFile) + command.AbortIfError("Failed to read boostrap file:", err) + + content, err := ApplyNoDot(data) + command.AbortIfError("Failed to update nodot declarations:", err) + + ioutil.WriteFile(suiteFile, content, perm) + + internal.GoFmt(suiteFile) +} + +func findSuiteFile() (string, os.FileMode) { + workingDir, err := os.Getwd() + command.AbortIfError("Could not find suite file for nodot:", err) + + files, err := ioutil.ReadDir(workingDir) + command.AbortIfError("Could not find suite file for nodot:", err) + + re := regexp.MustCompile(`RunSpecs\(|RunSpecsWithDefaultAndCustomReporters\(|RunSpecsWithCustomReporters\(`) + + for _, file := range files { + if file.IsDir() { + continue + } + path := filepath.Join(workingDir, file.Name()) + f, err := os.Open(path) + command.AbortIfError("Could not find suite file for nodot:", err) + defer f.Close() + + if re.MatchReader(bufio.NewReader(f)) { + return path, file.Mode() + } + } + + command.AbortWith("Could not find a suite file for nodot: you need a bootstrap file that call's Ginkgo's RunSpecs() command.\nTry running ginkgo bootstrap first.") + return "", 0 +} diff --git a/ginkgo/nodot/nodot_suite_test.go b/ginkgo/nodot/nodot_suite_test.go index 72b7332190..dae3220c29 100644 --- a/ginkgo/nodot/nodot_suite_test.go +++ b/ginkgo/nodot/nodot_suite_test.go @@ -13,9 +13,6 @@ func TestNodot(t *testing.T) { } // Declarations for Ginkgo DSL -type Done ginkgo.Done -type Benchmarker ginkgo.Benchmarker - var GinkgoWriter = ginkgo.GinkgoWriter var GinkgoParallelNode = ginkgo.GinkgoParallelNode var GinkgoT = ginkgo.GinkgoT @@ -37,10 +34,6 @@ var It = ginkgo.It var FIt = ginkgo.FIt var PIt = ginkgo.PIt var XIt = ginkgo.XIt -var Measure = ginkgo.Measure -var FMeasure = ginkgo.FMeasure -var PMeasure = ginkgo.PMeasure -var XMeasure = ginkgo.XMeasure var BeforeSuite = ginkgo.BeforeSuite var AfterSuite = ginkgo.AfterSuite var SynchronizedBeforeSuite = ginkgo.SynchronizedBeforeSuite diff --git a/ginkgo/nodot_command.go b/ginkgo/nodot_command.go deleted file mode 100644 index 39b88b5d1b..0000000000 --- a/ginkgo/nodot_command.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "io/ioutil" - "os" - "path/filepath" - "regexp" - - "github.com/onsi/ginkgo/ginkgo/nodot" -) - -func BuildNodotCommand() *Command { - return &Command{ - Name: "nodot", - FlagSet: flag.NewFlagSet("bootstrap", flag.ExitOnError), - UsageCommand: "ginkgo nodot", - Usage: []string{ - "Update the nodot declarations in your test suite", - "Any missing declarations (from, say, a recently added matcher) will be added to your bootstrap file.", - "If you've renamed a declaration, that name will be honored and not overwritten.", - }, - Command: updateNodot, - } -} - -func updateNodot(args []string, additionalArgs []string) { - suiteFile, perm := findSuiteFile() - - data, err := ioutil.ReadFile(suiteFile) - if err != nil { - complainAndQuit("Failed to update nodot declarations: " + err.Error()) - } - - content, err := nodot.ApplyNoDot(data) - if err != nil { - complainAndQuit("Failed to update nodot declarations: " + err.Error()) - } - ioutil.WriteFile(suiteFile, content, perm) - - goFmt(suiteFile) -} - -func findSuiteFile() (string, os.FileMode) { - workingDir, err := os.Getwd() - if err != nil { - complainAndQuit("Could not find suite file for nodot: " + err.Error()) - } - - files, err := ioutil.ReadDir(workingDir) - if err != nil { - complainAndQuit("Could not find suite file for nodot: " + err.Error()) - } - - re := regexp.MustCompile(`RunSpecs\(|RunSpecsWithDefaultAndCustomReporters\(|RunSpecsWithCustomReporters\(`) - - for _, file := range files { - if file.IsDir() { - continue - } - path := filepath.Join(workingDir, file.Name()) - f, err := os.Open(path) - if err != nil { - complainAndQuit("Could not find suite file for nodot: " + err.Error()) - } - defer f.Close() - - if re.MatchReader(bufio.NewReader(f)) { - return path, file.Mode() - } - } - - complainAndQuit("Could not find a suite file for nodot: you need a bootstrap file that call's Ginkgo's RunSpecs() command.\nTry running ginkgo bootstrap first.") - - return "", 0 -} diff --git a/ginkgo/notifications.go b/ginkgo/notifications.go deleted file mode 100644 index 368d61fb31..0000000000 --- a/ginkgo/notifications.go +++ /dev/null @@ -1,141 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/exec" - "regexp" - "runtime" - "strings" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/ginkgo/testsuite" -) - -type Notifier struct { - commandFlags *RunWatchAndBuildCommandFlags -} - -func NewNotifier(commandFlags *RunWatchAndBuildCommandFlags) *Notifier { - return &Notifier{ - commandFlags: commandFlags, - } -} - -func (n *Notifier) VerifyNotificationsAreAvailable() { - if n.commandFlags.Notify { - onLinux := (runtime.GOOS == "linux") - onOSX := (runtime.GOOS == "darwin") - if onOSX { - - _, err := exec.LookPath("terminal-notifier") - if err != nil { - fmt.Printf(`--notify requires terminal-notifier, which you don't seem to have installed. - -OSX: - -To remedy this: - - brew install terminal-notifier - -To learn more about terminal-notifier: - - https://github.com/alloy/terminal-notifier -`) - os.Exit(1) - } - - } else if onLinux { - - _, err := exec.LookPath("notify-send") - if err != nil { - fmt.Printf(`--notify requires terminal-notifier or notify-send, which you don't seem to have installed. - -Linux: - -Download and install notify-send for your distribution -`) - os.Exit(1) - } - - } - } -} - -func (n *Notifier) SendSuiteCompletionNotification(suite testsuite.TestSuite, suitePassed bool) { - if suitePassed { - n.SendNotification("Ginkgo [PASS]", fmt.Sprintf(`Test suite for "%s" passed.`, suite.PackageName)) - } else { - n.SendNotification("Ginkgo [FAIL]", fmt.Sprintf(`Test suite for "%s" failed.`, suite.PackageName)) - } -} - -func (n *Notifier) SendNotification(title string, subtitle string) { - - if n.commandFlags.Notify { - onLinux := (runtime.GOOS == "linux") - onOSX := (runtime.GOOS == "darwin") - - if onOSX { - - _, err := exec.LookPath("terminal-notifier") - if err == nil { - args := []string{"-title", title, "-subtitle", subtitle, "-group", "com.onsi.ginkgo"} - terminal := os.Getenv("TERM_PROGRAM") - if terminal == "iTerm.app" { - args = append(args, "-activate", "com.googlecode.iterm2") - } else if terminal == "Apple_Terminal" { - args = append(args, "-activate", "com.apple.Terminal") - } - - exec.Command("terminal-notifier", args...).Run() - } - - } else if onLinux { - - _, err := exec.LookPath("notify-send") - if err == nil { - args := []string{"-a", "ginkgo", title, subtitle} - exec.Command("notify-send", args...).Run() - } - - } - } -} - -func (n *Notifier) RunCommand(suite testsuite.TestSuite, suitePassed bool) { - - command := n.commandFlags.AfterSuiteHook - if command != "" { - - // Allow for string replacement to pass input to the command - passed := "[FAIL]" - if suitePassed { - passed = "[PASS]" - } - command = strings.Replace(command, "(ginkgo-suite-passed)", passed, -1) - command = strings.Replace(command, "(ginkgo-suite-name)", suite.PackageName, -1) - - // Must break command into parts - splitArgs := regexp.MustCompile(`'.+'|".+"|\S+`) - parts := splitArgs.FindAllString(command, -1) - - output, err := exec.Command(parts[0], parts[1:]...).CombinedOutput() - if err != nil { - fmt.Println("Post-suite command failed:") - if config.DefaultReporterConfig.NoColor { - fmt.Printf("\t%s\n", output) - } else { - fmt.Printf("\t%s%s%s\n", redColor, string(output), defaultStyle) - } - n.SendNotification("Ginkgo [ERROR]", fmt.Sprintf(`After suite command "%s" failed`, n.commandFlags.AfterSuiteHook)) - } else { - fmt.Println("Post-suite command succeeded:") - if config.DefaultReporterConfig.NoColor { - fmt.Printf("\t%s\n", output) - } else { - fmt.Printf("\t%s%s%s\n", greenColor, string(output), defaultStyle) - } - } - } -} diff --git a/ginkgo/outline/outline_command.go b/ginkgo/outline/outline_command.go new file mode 100644 index 0000000000..ec6dcea5da --- /dev/null +++ b/ginkgo/outline/outline_command.go @@ -0,0 +1,98 @@ +package outline + +import ( + "encoding/json" + "fmt" + "go/parser" + "go/token" + "os" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/command" +) + +const ( + // indentWidth is the width used by the 'indent' output + indentWidth = 4 + // stdinAlias is a portable alias for stdin. This convention is used in + // other CLIs, e.g., kubectl. + stdinAlias = "-" + usageCommand = "ginkgo outline " +) + +type outlineConfig struct { + Format string +} + +func BuildOutlineCommand() command.Command { + conf := outlineConfig{ + Format: "csv", + } + flags, err := config.NewGinkgoFlagSet( + config.GinkgoFlags{ + {Name: "format", KeyPath: "Format", + Usage: "Format of outline", + UsageArgument: "one of 'csv', 'indent', or 'json'", + UsageDefaultValue: conf.Format, + }, + }, + &conf, + config.GinkgoFlagSections{}, + ) + if err != nil { + panic(err) + } + + return command.Command{ + Name: "outline", + Usage: "ginkgo outline ", + ShortDoc: "Create an outline of Ginkgo symbols for a file", + Documentation: "To read from stdin, use: `ginkgo outline -`", + DocLink: "creating-an-outline-of-tests", + Flags: flags, + Command: func(args []string, _ []string) { + outlineFile(args, conf.Format) + }, + } +} + +func outlineFile(args []string, format string) { + if len(args) != 1 { + command.AbortWithUsage("outline expects exactly one argument") + } + + filename := args[0] + var src *os.File + if filename == stdinAlias { + src = os.Stdin + } else { + var err error + src, err = os.Open(filename) + command.AbortIfError("Failed to open file:", err) + } + + fset := token.NewFileSet() + + parsedSrc, err := parser.ParseFile(fset, filename, src, 0) + command.AbortIfError("Failed to parse source:", err) + + o, err := FromASTFile(fset, parsedSrc) + command.AbortIfError("Failed to create outline:", err) + + var oerr error + switch format { + case "csv": + _, oerr = fmt.Print(o) + case "indent": + _, oerr = fmt.Print(o.StringIndent(indentWidth)) + case "json": + b, err := json.Marshal(o) + if err != nil { + println(fmt.Sprintf("error marshalling to json: %s", err)) + } + _, oerr = fmt.Println(string(b)) + default: + command.AbortWith("Format %s not accepted", format) + } + command.AbortIfError("Failed to write outline:", oerr) +} diff --git a/ginkgo/outline/outline_test.go b/ginkgo/outline/outline_test.go index 2bb9cf8b21..df381fa204 100644 --- a/ginkgo/outline/outline_test.go +++ b/ginkgo/outline/outline_test.go @@ -64,7 +64,6 @@ var _ = DescribeTable("Validate outline from file with", ) var _ = Describe("Validate position", func() { - It("should report the correct start and end byte offsets of the ginkgo container or spec", func() { fset := token.NewFileSet() astFile, err := parser.ParseFile(fset, filepath.Join("_testdata", "position_test.go"), nil, 0) diff --git a/ginkgo/outline_command.go b/ginkgo/outline_command.go deleted file mode 100644 index 96ca7ad27f..0000000000 --- a/ginkgo/outline_command.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "encoding/json" - "flag" - "fmt" - "go/parser" - "go/token" - "os" - - "github.com/onsi/ginkgo/ginkgo/outline" -) - -const ( - // indentWidth is the width used by the 'indent' output - indentWidth = 4 - // stdinAlias is a portable alias for stdin. This convention is used in - // other CLIs, e.g., kubectl. - stdinAlias = "-" - usageCommand = "ginkgo outline " -) - -func BuildOutlineCommand() *Command { - const defaultFormat = "csv" - var format string - flagSet := flag.NewFlagSet("outline", flag.ExitOnError) - flagSet.StringVar(&format, "format", defaultFormat, "Format of outline. Accepted: 'csv', 'indent', 'json'") - return &Command{ - Name: "outline", - FlagSet: flagSet, - UsageCommand: usageCommand, - Usage: []string{ - "Create an outline of Ginkgo symbols for a file", - "To read from stdin, use: `ginkgo outline -`", - "Accepts the following flags:", - }, - Command: func(args []string, additionalArgs []string) { - outlineFile(args, format) - }, - } -} - -func outlineFile(args []string, format string) { - if len(args) != 1 { - println(fmt.Sprintf("usage: %s", usageCommand)) - os.Exit(1) - } - - filename := args[0] - var src *os.File - if filename == stdinAlias { - src = os.Stdin - } else { - var err error - src, err = os.Open(filename) - if err != nil { - println(fmt.Sprintf("error opening file: %s", err)) - os.Exit(1) - } - } - - fset := token.NewFileSet() - - parsedSrc, err := parser.ParseFile(fset, filename, src, 0) - if err != nil { - println(fmt.Sprintf("error parsing source: %s", err)) - os.Exit(1) - } - - o, err := outline.FromASTFile(fset, parsedSrc) - if err != nil { - println(fmt.Sprintf("error creating outline: %s", err)) - os.Exit(1) - } - - var oerr error - switch format { - case "csv": - _, oerr = fmt.Print(o) - case "indent": - _, oerr = fmt.Print(o.StringIndent(indentWidth)) - case "json": - b, err := json.Marshal(o) - if err != nil { - println(fmt.Sprintf("error marshalling to json: %s", err)) - } - _, oerr = fmt.Println(string(b)) - default: - complainAndQuit(fmt.Sprintf("format %s not accepted", format)) - } - if oerr != nil { - println(fmt.Sprintf("error writing outline: %s", oerr)) - os.Exit(1) - } -} diff --git a/ginkgo/run/run_command.go b/ginkgo/run/run_command.go new file mode 100644 index 0000000000..6e085f2f74 --- /dev/null +++ b/ginkgo/run/run_command.go @@ -0,0 +1,220 @@ +package run + +import ( + "fmt" + "math/rand" + "os" + "strings" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/ginkgo/command" + "github.com/onsi/ginkgo/ginkgo/internal" + "github.com/onsi/ginkgo/ginkgo/interrupthandler" + "github.com/onsi/ginkgo/types" +) + +func BuildRunCommand() command.Command { + var ginkgoConfig = config.NewDefaultGinkgoConfig() + var reporterConfig = config.NewDefaultReporterConfig() + var cliConfig = config.NewDefaultGinkgoCLIConfig() + var goFlagsConfig = config.NewDefaultGoFlagsConfig() + + flags, err := config.BuildRunCommandFlagSet(&ginkgoConfig, &reporterConfig, &cliConfig, &goFlagsConfig) + if err != nil { + panic(err) + } + + interruptHandler := interrupthandler.NewInterruptHandler() + + return command.Command{ + Name: "run", + Flags: flags, + Usage: "ginkgo run -- ", + ShortDoc: "Run the tests in the passed in (or the package in the current directory if left blank)", + Documentation: "Any arguments after -- will be passed to the test.", + DocLink: "running-tests", + Command: func(args []string, additionalArgs []string) { + var errors []error + cliConfig, goFlagsConfig, errors = config.VetAndInitializeCLIAndGoConfig(cliConfig, goFlagsConfig) + command.AbortIfErrors("Ginkgo detected configuraiotn issues:", errors) + + runner := &SpecRunner{ + cliConfig: cliConfig, + goFlagsConfig: goFlagsConfig, + ginkgoConfig: ginkgoConfig, + reporterConfig: reporterConfig, + flags: flags, + + interruptHandler: interruptHandler, + } + + runner.RunSpecs(args, additionalArgs) + }, + } +} + +type SpecRunner struct { + ginkgoConfig config.GinkgoConfigType + reporterConfig config.DefaultReporterConfigType + cliConfig config.GinkgoCLIConfigType + goFlagsConfig config.GoFlagsConfigType + flags config.GinkgoFlagSet + + interruptHandler *interrupthandler.InterruptHandler +} + +//TODO - THIS IS INTENTIONALLY SLOWER THAN GINKGO V1 FOR NOW +//COMPILATION AND RUNNING AND SERIALIZED. IN TIME WE'LL RUN EXPERIMENTS TO FIND THE OPTIMAL WAY TO +//IMPROVE PERFORMANCE +func (r *SpecRunner) RunSpecs(args []string, additionalArgs []string) { + suites, skippedPackages := internal.FindSuites(args, r.cliConfig, true) + if len(skippedPackages) > 0 { + fmt.Println("Will skip:") + for _, skippedPackage := range skippedPackages { + fmt.Println(" " + skippedPackage) + } + } + + if len(skippedPackages) > 0 && len(suites) == 0 { + command.Abort(command.AbortDetails{ + ExitCode: 0, + Error: fmt.Errorf("All tests skipped! Exiting..."), + }) + } + + if len(suites) == 0 { + command.AbortWith("Found no test suites") + } + + if len(suites) > 1 && !r.flags.WasSet("succinct") && !r.reporterConfig.Verbose { + r.reporterConfig.Succinct = true + } + + t := time.Now() + iteration := 0 + + var failedSuites []internal.TestSuite + var hasProgrammaticFocus = false + +OUTER_LOOP: + for { + if !r.flags.WasSet("seed") { + r.ginkgoConfig.RandomSeed = time.Now().Unix() + } + if r.cliConfig.RandomizeSuites && len(suites) > 1 { + suites = r.randomize(suites) + } + failedSuites = []internal.TestSuite{} + hasProgrammaticFocus = false + + SUITE_LOOP: + for suiteIdx := range suites { + if r.interruptHandler.WasInterrupted() { + break OUTER_LOOP + } + + suites[suiteIdx] = internal.CompileSuite(suites[suiteIdx], r.goFlagsConfig) + if suites[suiteIdx].CompilationError != nil { + fmt.Println(suites[suiteIdx].CompilationError.Error()) + failedSuites = append(failedSuites, suites[suiteIdx]) + if r.cliConfig.KeepGoing { + continue SUITE_LOOP + } else { + break SUITE_LOOP + } + } + + if r.interruptHandler.WasInterrupted() { + break OUTER_LOOP + } + + suites[suiteIdx] = internal.RunCompiledSuite(suites[suiteIdx], r.ginkgoConfig, r.reporterConfig, r.cliConfig, r.goFlagsConfig, additionalArgs) + hasProgrammaticFocus = hasProgrammaticFocus || suites[suiteIdx].HasProgrammaticFocus + if !suites[suiteIdx].Passed { + failedSuites = append(failedSuites, suites[suiteIdx]) + if !r.cliConfig.KeepGoing { + break SUITE_LOOP + } + } + } + + if !r.cliConfig.UntilItFails || len(failedSuites) > 0 { + if iteration > 0 { + fmt.Printf("\nTests failed on attempt #%d\n\n", iteration+1) + } + break OUTER_LOOP + } + + fmt.Printf("\nAll tests passed...\nWill keep running them until they fail.\nThis was attempt #%d\n%s\n", iteration+1, orcMessage(iteration+1)) + iteration += 1 + } + + internal.Cleanup(suites...) + + err := internal.FinalizeProfilesForSuites(suites, r.cliConfig, r.goFlagsConfig) + command.AbortIfError("could not finalize profiles:", err) + + fmt.Printf("\nGinkgo ran %d %s in %s\n", len(suites), internal.PluralizedWord("suite", "suites", len(suites)), time.Since(t)) + + if len(failedSuites) == 0 { + if hasProgrammaticFocus && strings.TrimSpace(os.Getenv("GINKGO_EDITOR_INTEGRATION")) == "" { + fmt.Printf("Test Suite Passed\n") + fmt.Printf("Detected Programmatic Focus - setting exit status to %d\n", types.GINKGO_FOCUS_EXIT_CODE) + command.Abort(command.AbortDetails{ExitCode: types.GINKGO_FOCUS_EXIT_CODE}) + } else { + fmt.Printf("Test Suite Passed\n") + command.Abort(command.AbortDetails{}) + } + } else { + fmt.Fprintln(formatter.ColorableStdOut, "") + if len(failedSuites) > 1 { + fmt.Fprintln(formatter.ColorableStdOut, + internal.FailedSuitesReport(failedSuites, formatter.NewWithNoColorBool(r.reporterConfig.NoColor))) + } + fmt.Printf("Test Suite Failed\n") + command.Abort(command.AbortDetails{ExitCode: 1}) + } +} + +func (r *SpecRunner) randomize(suites []internal.TestSuite) []internal.TestSuite { + randomized := make([]internal.TestSuite, len(suites)) + randomizer := rand.New(rand.NewSource(r.ginkgoConfig.RandomSeed)) + permutation := randomizer.Perm(len(suites)) + for i, j := range permutation { + randomized[i] = suites[j] + } + return randomized +} + +func orcMessage(iteration int) string { + if iteration < 10 { + return "" + } else if iteration < 30 { + return []string{ + "If at first you succeed...", + "...try, try again.", + "Looking good!", + "Still good...", + "I think your tests are fine....", + "Yep, still passing", + "Oh boy, here I go testin' again!", + "Even the gophers are getting bored", + "Did you try -race?", + "Maybe you should stop now?", + "I'm getting tired...", + "What if I just made you a sandwich?", + "Hit ^C, hit ^C, please hit ^C", + "Make it stop. Please!", + "Come on! Enough is enough!", + "Dave, this conversation can serve no purpose anymore. Goodbye.", + "Just what do you think you're doing, Dave? ", + "I, Sisyphus", + "Insanity: doing the same thing over and over again and expecting different results. -Einstein", + "I guess Einstein never tried to churn butter", + }[iteration-10] + "\n" + } else { + return "No, seriously... you can probably stop now.\n" + } +} diff --git a/ginkgo/run_command.go b/ginkgo/run_command.go deleted file mode 100644 index dd87e1f89a..0000000000 --- a/ginkgo/run_command.go +++ /dev/null @@ -1,312 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "math/rand" - "os" - "regexp" - "strings" - "time" - - "io/ioutil" - "path/filepath" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/ginkgo/interrupthandler" - "github.com/onsi/ginkgo/ginkgo/testrunner" - colorable "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable" - "github.com/onsi/ginkgo/types" -) - -func BuildRunCommand() *Command { - commandFlags := NewRunCommandFlags(flag.NewFlagSet("ginkgo", flag.ExitOnError)) - notifier := NewNotifier(commandFlags) - interruptHandler := interrupthandler.NewInterruptHandler() - runner := &SpecRunner{ - commandFlags: commandFlags, - notifier: notifier, - interruptHandler: interruptHandler, - suiteRunner: NewSuiteRunner(notifier, interruptHandler), - } - - return &Command{ - Name: "", - FlagSet: commandFlags.FlagSet, - UsageCommand: "ginkgo -- ", - Usage: []string{ - "Run the tests in the passed in (or the package in the current directory if left blank).", - "Any arguments after -- will be passed to the test.", - "Accepts the following flags:", - }, - Command: runner.RunSpecs, - } -} - -type SpecRunner struct { - commandFlags *RunWatchAndBuildCommandFlags - notifier *Notifier - interruptHandler *interrupthandler.InterruptHandler - suiteRunner *SuiteRunner -} - -func (r *SpecRunner) RunSpecs(args []string, additionalArgs []string) { - r.commandFlags.computeNodes() - r.notifier.VerifyNotificationsAreAvailable() - - deprecationTracker := types.NewDeprecationTracker() - - if r.commandFlags.ParallelStream { - deprecationTracker.TrackDeprecation(types.Deprecation{ - Message: "--stream is deprecated and will be removed in Ginkgo 2.0", - DocLink: "removed--stream", - }) - } - - if r.commandFlags.Notify { - deprecationTracker.TrackDeprecation(types.Deprecation{ - Message: "--notify is deprecated and will be removed in Ginkgo 2.0", - DocLink: "removed--notify", - }) - } - - if deprecationTracker.DidTrackDeprecations() { - fmt.Fprintln(colorable.NewColorableStderr(), deprecationTracker.DeprecationsReport()) - } - - suites, skippedPackages := findSuites(args, r.commandFlags.Recurse, r.commandFlags.SkipPackage, true) - if len(skippedPackages) > 0 { - fmt.Println("Will skip:") - for _, skippedPackage := range skippedPackages { - fmt.Println(" " + skippedPackage) - } - } - - if len(skippedPackages) > 0 && len(suites) == 0 { - fmt.Println("All tests skipped! Exiting...") - os.Exit(0) - } - - if len(suites) == 0 { - complainAndQuit("Found no test suites") - } - - r.ComputeSuccinctMode(len(suites)) - - t := time.Now() - - runners := []*testrunner.TestRunner{} - for _, suite := range suites { - runners = append(runners, testrunner.New(suite, r.commandFlags.NumCPU, r.commandFlags.ParallelStream, r.commandFlags.Timeout, r.commandFlags.GoOpts, additionalArgs)) - } - - numSuites := 0 - runResult := testrunner.PassingRunResult() - if r.commandFlags.UntilItFails { - iteration := 0 - for { - r.UpdateSeed() - randomizedRunners := r.randomizeOrder(runners) - runResult, numSuites = r.suiteRunner.RunSuites(randomizedRunners, r.commandFlags.NumCompilers, r.commandFlags.KeepGoing, nil) - iteration++ - - if r.interruptHandler.WasInterrupted() { - break - } - - if runResult.Passed { - fmt.Printf("\nAll tests passed...\nWill keep running them until they fail.\nThis was attempt #%d\n%s\n", iteration, orcMessage(iteration)) - } else { - fmt.Printf("\nTests failed on attempt #%d\n\n", iteration) - break - } - } - } else { - randomizedRunners := r.randomizeOrder(runners) - runResult, numSuites = r.suiteRunner.RunSuites(randomizedRunners, r.commandFlags.NumCompilers, r.commandFlags.KeepGoing, nil) - } - - for _, runner := range runners { - runner.CleanUp() - } - - if r.isInCoverageMode() { - if r.getOutputDir() != "" { - // If coverprofile is set, combine coverages - if r.getCoverprofile() != "" { - if err := r.combineCoverprofiles(runners); err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - } else { - // Just move them - r.moveCoverprofiles(runners) - } - } - } - - fmt.Printf("\nGinkgo ran %d %s in %s\n", numSuites, pluralizedWord("suite", "suites", numSuites), time.Since(t)) - - if runResult.Passed { - if runResult.HasProgrammaticFocus && strings.TrimSpace(os.Getenv("GINKGO_EDITOR_INTEGRATION")) == "" { - fmt.Printf("Test Suite Passed\n") - fmt.Printf("Detected Programmatic Focus - setting exit status to %d\n", types.GINKGO_FOCUS_EXIT_CODE) - os.Exit(types.GINKGO_FOCUS_EXIT_CODE) - } else { - fmt.Printf("Test Suite Passed\n") - os.Exit(0) - } - } else { - fmt.Printf("Test Suite Failed\n") - os.Exit(1) - } -} - -// Moves all generated profiles to specified directory -func (r *SpecRunner) moveCoverprofiles(runners []*testrunner.TestRunner) { - for _, runner := range runners { - _, filename := filepath.Split(runner.CoverageFile) - err := os.Rename(runner.CoverageFile, filepath.Join(r.getOutputDir(), filename)) - - if err != nil { - fmt.Printf("Unable to move coverprofile %s, %v\n", runner.CoverageFile, err) - return - } - } -} - -// Combines all generated profiles in the specified directory -func (r *SpecRunner) combineCoverprofiles(runners []*testrunner.TestRunner) error { - - path, _ := filepath.Abs(r.getOutputDir()) - if !fileExists(path) { - return fmt.Errorf("Unable to create combined profile, outputdir does not exist: %s", r.getOutputDir()) - } - - fmt.Println("path is " + path) - - combined, err := os.OpenFile( - filepath.Join(path, r.getCoverprofile()), - os.O_WRONLY|os.O_CREATE, - 0666, - ) - - if err != nil { - fmt.Printf("Unable to create combined profile, %v\n", err) - return nil // non-fatal error - } - - modeRegex := regexp.MustCompile(`^mode: .*\n`) - for index, runner := range runners { - contents, err := ioutil.ReadFile(runner.CoverageFile) - - if err != nil { - fmt.Printf("Unable to read coverage file %s to combine, %v\n", runner.CoverageFile, err) - return nil // non-fatal error - } - - // remove the cover mode line from every file - // except the first one - if index > 0 { - contents = modeRegex.ReplaceAll(contents, []byte{}) - } - - _, err = combined.Write(contents) - - // Add a newline to the end of every file if missing. - if err == nil && len(contents) > 0 && contents[len(contents)-1] != '\n' { - _, err = combined.Write([]byte("\n")) - } - - if err != nil { - fmt.Printf("Unable to append to coverprofile, %v\n", err) - return nil // non-fatal error - } - } - - fmt.Println("All profiles combined") - return nil -} - -func (r *SpecRunner) isInCoverageMode() bool { - opts := r.commandFlags.GoOpts - return *opts["cover"].(*bool) || *opts["coverpkg"].(*string) != "" || *opts["covermode"].(*string) != "" -} - -func (r *SpecRunner) getCoverprofile() string { - return *r.commandFlags.GoOpts["coverprofile"].(*string) -} - -func (r *SpecRunner) getOutputDir() string { - return *r.commandFlags.GoOpts["outputdir"].(*string) -} - -func (r *SpecRunner) ComputeSuccinctMode(numSuites int) { - if config.DefaultReporterConfig.Verbose { - config.DefaultReporterConfig.Succinct = false - return - } - - if numSuites == 1 { - return - } - - if numSuites > 1 && !r.commandFlags.wasSet("succinct") { - config.DefaultReporterConfig.Succinct = true - } -} - -func (r *SpecRunner) UpdateSeed() { - if !r.commandFlags.wasSet("seed") { - config.GinkgoConfig.RandomSeed = time.Now().Unix() - } -} - -func (r *SpecRunner) randomizeOrder(runners []*testrunner.TestRunner) []*testrunner.TestRunner { - if !r.commandFlags.RandomizeSuites { - return runners - } - - if len(runners) <= 1 { - return runners - } - - randomizedRunners := make([]*testrunner.TestRunner, len(runners)) - randomizer := rand.New(rand.NewSource(config.GinkgoConfig.RandomSeed)) - permutation := randomizer.Perm(len(runners)) - for i, j := range permutation { - randomizedRunners[i] = runners[j] - } - return randomizedRunners -} - -func orcMessage(iteration int) string { - if iteration < 10 { - return "" - } else if iteration < 30 { - return []string{ - "If at first you succeed...", - "...try, try again.", - "Looking good!", - "Still good...", - "I think your tests are fine....", - "Yep, still passing", - "Oh boy, here I go testin' again!", - "Even the gophers are getting bored", - "Did you try -race?", - "Maybe you should stop now?", - "I'm getting tired...", - "What if I just made you a sandwich?", - "Hit ^C, hit ^C, please hit ^C", - "Make it stop. Please!", - "Come on! Enough is enough!", - "Dave, this conversation can serve no purpose anymore. Goodbye.", - "Just what do you think you're doing, Dave? ", - "I, Sisyphus", - "Insanity: doing the same thing over and over again and expecting different results. -Einstein", - "I guess Einstein never tried to churn butter", - }[iteration-10] + "\n" - } else { - return "No, seriously... you can probably stop now.\n" - } -} diff --git a/ginkgo/run_watch_and_build_command_flags.go b/ginkgo/run_watch_and_build_command_flags.go deleted file mode 100644 index e0994fc3cb..0000000000 --- a/ginkgo/run_watch_and_build_command_flags.go +++ /dev/null @@ -1,169 +0,0 @@ -package main - -import ( - "flag" - "runtime" - - "time" - - "github.com/onsi/ginkgo/config" -) - -type RunWatchAndBuildCommandFlags struct { - Recurse bool - SkipPackage string - GoOpts map[string]interface{} - - //for run and watch commands - NumCPU int - NumCompilers int - ParallelStream bool - Notify bool - AfterSuiteHook string - AutoNodes bool - Timeout time.Duration - - //only for run command - KeepGoing bool - UntilItFails bool - RandomizeSuites bool - - //only for watch command - Depth int - WatchRegExp string - - FlagSet *flag.FlagSet -} - -const runMode = 1 -const watchMode = 2 -const buildMode = 3 - -func NewRunCommandFlags(flagSet *flag.FlagSet) *RunWatchAndBuildCommandFlags { - c := &RunWatchAndBuildCommandFlags{ - FlagSet: flagSet, - } - c.flags(runMode) - return c -} - -func NewWatchCommandFlags(flagSet *flag.FlagSet) *RunWatchAndBuildCommandFlags { - c := &RunWatchAndBuildCommandFlags{ - FlagSet: flagSet, - } - c.flags(watchMode) - return c -} - -func NewBuildCommandFlags(flagSet *flag.FlagSet) *RunWatchAndBuildCommandFlags { - c := &RunWatchAndBuildCommandFlags{ - FlagSet: flagSet, - } - c.flags(buildMode) - return c -} - -func (c *RunWatchAndBuildCommandFlags) wasSet(flagName string) bool { - wasSet := false - c.FlagSet.Visit(func(f *flag.Flag) { - if f.Name == flagName { - wasSet = true - } - }) - - return wasSet -} - -func (c *RunWatchAndBuildCommandFlags) computeNodes() { - if c.wasSet("nodes") { - return - } - if c.AutoNodes { - switch n := runtime.NumCPU(); { - case n <= 4: - c.NumCPU = n - default: - c.NumCPU = n - 1 - } - } -} - -func (c *RunWatchAndBuildCommandFlags) stringSlot(slot string) *string { - var opt string - c.GoOpts[slot] = &opt - return &opt -} - -func (c *RunWatchAndBuildCommandFlags) boolSlot(slot string) *bool { - var opt bool - c.GoOpts[slot] = &opt - return &opt -} - -func (c *RunWatchAndBuildCommandFlags) intSlot(slot string) *int { - var opt int - c.GoOpts[slot] = &opt - return &opt -} - -func (c *RunWatchAndBuildCommandFlags) flags(mode int) { - c.GoOpts = make(map[string]interface{}) - - onWindows := (runtime.GOOS == "windows") - - c.FlagSet.BoolVar(&(c.Recurse), "r", false, "Find and run test suites under the current directory recursively.") - c.FlagSet.BoolVar(c.boolSlot("race"), "race", false, "Run tests with race detection enabled.") - c.FlagSet.BoolVar(c.boolSlot("cover"), "cover", false, "Run tests with coverage analysis, will generate coverage profiles with the package name in the current directory.") - c.FlagSet.StringVar(c.stringSlot("coverpkg"), "coverpkg", "", "Run tests with coverage on the given external modules.") - c.FlagSet.StringVar(&(c.SkipPackage), "skipPackage", "", "A comma-separated list of package names to be skipped. If any part of the package's path matches, that package is ignored.") - c.FlagSet.StringVar(c.stringSlot("tags"), "tags", "", "A list of build tags to consider satisfied during the build.") - c.FlagSet.StringVar(c.stringSlot("gcflags"), "gcflags", "", "Arguments to pass on each go tool compile invocation.") - c.FlagSet.StringVar(c.stringSlot("covermode"), "covermode", "", "Set the mode for coverage analysis.") - c.FlagSet.BoolVar(c.boolSlot("a"), "a", false, "Force rebuilding of packages that are already up-to-date.") - c.FlagSet.BoolVar(c.boolSlot("n"), "n", false, "Have `go test` print the commands but do not run them.") - c.FlagSet.BoolVar(c.boolSlot("msan"), "msan", false, "Enable interoperation with memory sanitizer.") - c.FlagSet.BoolVar(c.boolSlot("x"), "x", false, "Have `go test` print the commands.") - c.FlagSet.BoolVar(c.boolSlot("work"), "work", false, "Print the name of the temporary work directory and do not delete it when exiting.") - c.FlagSet.StringVar(c.stringSlot("asmflags"), "asmflags", "", "Arguments to pass on each go tool asm invocation.") - c.FlagSet.StringVar(c.stringSlot("buildmode"), "buildmode", "", "Build mode to use. See 'go help buildmode' for more.") - c.FlagSet.StringVar(c.stringSlot("mod"), "mod", "", "Go module control. See 'go help modules' for more.") - c.FlagSet.StringVar(c.stringSlot("compiler"), "compiler", "", "Name of compiler to use, as in runtime.Compiler (gccgo or gc).") - c.FlagSet.StringVar(c.stringSlot("gccgoflags"), "gccgoflags", "", "Arguments to pass on each gccgo compiler/linker invocation.") - c.FlagSet.StringVar(c.stringSlot("installsuffix"), "installsuffix", "", "A suffix to use in the name of the package installation directory.") - c.FlagSet.StringVar(c.stringSlot("ldflags"), "ldflags", "", "Arguments to pass on each go tool link invocation.") - c.FlagSet.BoolVar(c.boolSlot("linkshared"), "linkshared", false, "Link against shared libraries previously created with -buildmode=shared.") - c.FlagSet.StringVar(c.stringSlot("pkgdir"), "pkgdir", "", "install and load all packages from the given dir instead of the usual locations.") - c.FlagSet.StringVar(c.stringSlot("toolexec"), "toolexec", "", "a program to use to invoke toolchain programs like vet and asm.") - c.FlagSet.IntVar(c.intSlot("blockprofilerate"), "blockprofilerate", 1, "Control the detail provided in goroutine blocking profiles by calling runtime.SetBlockProfileRate with the given value.") - c.FlagSet.StringVar(c.stringSlot("coverprofile"), "coverprofile", "", "Write a coverage profile to the specified file after all tests have passed.") - c.FlagSet.StringVar(c.stringSlot("cpuprofile"), "cpuprofile", "", "Write a CPU profile to the specified file before exiting.") - c.FlagSet.StringVar(c.stringSlot("memprofile"), "memprofile", "", "Write a memory profile to the specified file after all tests have passed.") - c.FlagSet.IntVar(c.intSlot("memprofilerate"), "memprofilerate", 0, "Enable more precise (and expensive) memory profiles by setting runtime.MemProfileRate.") - c.FlagSet.StringVar(c.stringSlot("outputdir"), "outputdir", "", "Place output files from profiling in the specified directory.") - c.FlagSet.BoolVar(c.boolSlot("requireSuite"), "requireSuite", false, "Fail if there are ginkgo tests in a directory but no test suite (missing RunSpecs)") - c.FlagSet.StringVar(c.stringSlot("vet"), "vet", "", "Configure the invocation of 'go vet' to use the comma-separated list of vet checks. If list is 'off', 'go test' does not run 'go vet' at all.") - - if mode == runMode || mode == watchMode { - config.Flags(c.FlagSet, "", false) - c.FlagSet.IntVar(&(c.NumCPU), "nodes", 1, "The number of parallel test nodes to run") - c.FlagSet.IntVar(&(c.NumCompilers), "compilers", 0, "The number of concurrent compilations to run (0 will autodetect)") - c.FlagSet.BoolVar(&(c.AutoNodes), "p", false, "Run in parallel with auto-detected number of nodes") - c.FlagSet.BoolVar(&(c.ParallelStream), "stream", onWindows, "stream parallel test output in real time: less coherent, but useful for debugging") - if !onWindows { - c.FlagSet.BoolVar(&(c.Notify), "notify", false, "Send desktop notifications when a test run completes") - } - c.FlagSet.StringVar(&(c.AfterSuiteHook), "afterSuiteHook", "", "Run a command when a suite test run completes") - c.FlagSet.DurationVar(&(c.Timeout), "timeout", 24*time.Hour, "Suite fails if it does not complete within the specified timeout") - } - - if mode == runMode { - c.FlagSet.BoolVar(&(c.KeepGoing), "keepGoing", false, "When true, failures from earlier test suites do not prevent later test suites from running") - c.FlagSet.BoolVar(&(c.UntilItFails), "untilItFails", false, "When true, Ginkgo will keep rerunning tests until a failure occurs") - c.FlagSet.BoolVar(&(c.RandomizeSuites), "randomizeSuites", false, "When true, Ginkgo will randomize the order in which test suites run") - } - - if mode == watchMode { - c.FlagSet.IntVar(&(c.Depth), "depth", 1, "Ginkgo will watch dependencies down to this depth in the dependency tree") - c.FlagSet.StringVar(&(c.WatchRegExp), "watchRegExp", `\.go$`, "Files matching this regular expression will be watched for changes") - } -} diff --git a/ginkgo/suite_runner.go b/ginkgo/suite_runner.go deleted file mode 100644 index ab746d7e95..0000000000 --- a/ginkgo/suite_runner.go +++ /dev/null @@ -1,173 +0,0 @@ -package main - -import ( - "fmt" - "runtime" - "sync" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/ginkgo/interrupthandler" - "github.com/onsi/ginkgo/ginkgo/testrunner" - "github.com/onsi/ginkgo/ginkgo/testsuite" - colorable "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable" -) - -type compilationInput struct { - runner *testrunner.TestRunner - result chan compilationOutput -} - -type compilationOutput struct { - runner *testrunner.TestRunner - err error -} - -type SuiteRunner struct { - notifier *Notifier - interruptHandler *interrupthandler.InterruptHandler -} - -func NewSuiteRunner(notifier *Notifier, interruptHandler *interrupthandler.InterruptHandler) *SuiteRunner { - return &SuiteRunner{ - notifier: notifier, - interruptHandler: interruptHandler, - } -} - -func (r *SuiteRunner) compileInParallel(runners []*testrunner.TestRunner, numCompilers int, willCompile func(suite testsuite.TestSuite)) chan compilationOutput { - //we return this to the consumer, it will return each runner in order as it compiles - compilationOutputs := make(chan compilationOutput, len(runners)) - - //an array of channels - the nth runner's compilation output is sent to the nth channel in this array - //we read from these channels in order to ensure we run the suites in order - orderedCompilationOutputs := []chan compilationOutput{} - for range runners { - orderedCompilationOutputs = append(orderedCompilationOutputs, make(chan compilationOutput, 1)) - } - - //we're going to spin up numCompilers compilers - they're going to run concurrently and will consume this channel - //we prefill the channel then close it, this ensures we compile things in the correct order - workPool := make(chan compilationInput, len(runners)) - for i, runner := range runners { - workPool <- compilationInput{runner, orderedCompilationOutputs[i]} - } - close(workPool) - - //pick a reasonable numCompilers - if numCompilers == 0 { - numCompilers = runtime.NumCPU() - } - - //a WaitGroup to help us wait for all compilers to shut down - wg := &sync.WaitGroup{} - wg.Add(numCompilers) - - //spin up the concurrent compilers - for i := 0; i < numCompilers; i++ { - go func() { - defer wg.Done() - for input := range workPool { - if r.interruptHandler.WasInterrupted() { - return - } - - if willCompile != nil { - willCompile(input.runner.Suite) - } - - //We retry because Go sometimes steps on itself when multiple compiles happen in parallel. This is ugly, but should help resolve flakiness... - var err error - retries := 0 - for retries <= 5 { - if r.interruptHandler.WasInterrupted() { - return - } - if err = input.runner.Compile(); err == nil { - break - } - retries++ - } - - input.result <- compilationOutput{input.runner, err} - } - }() - } - - //read from the compilation output channels *in order* and send them to the caller - //close the compilationOutputs channel to tell the caller we're done - go func() { - defer close(compilationOutputs) - for _, orderedCompilationOutput := range orderedCompilationOutputs { - select { - case compilationOutput := <-orderedCompilationOutput: - compilationOutputs <- compilationOutput - case <-r.interruptHandler.C: - //interrupt detected, wait for the compilers to shut down then bail - //this ensure we clean up after ourselves as we don't leave any compilation processes running - wg.Wait() - return - } - } - }() - - return compilationOutputs -} - -func (r *SuiteRunner) RunSuites(runners []*testrunner.TestRunner, numCompilers int, keepGoing bool, willCompile func(suite testsuite.TestSuite)) (testrunner.RunResult, int) { - runResult := testrunner.PassingRunResult() - - compilationOutputs := r.compileInParallel(runners, numCompilers, willCompile) - - numSuitesThatRan := 0 - suitesThatFailed := []testsuite.TestSuite{} - for compilationOutput := range compilationOutputs { - if compilationOutput.err != nil { - fmt.Print(compilationOutput.err.Error()) - } - numSuitesThatRan++ - suiteRunResult := testrunner.FailingRunResult() - if compilationOutput.err == nil { - suiteRunResult = compilationOutput.runner.Run() - } - r.notifier.SendSuiteCompletionNotification(compilationOutput.runner.Suite, suiteRunResult.Passed) - r.notifier.RunCommand(compilationOutput.runner.Suite, suiteRunResult.Passed) - runResult = runResult.Merge(suiteRunResult) - if !suiteRunResult.Passed { - suitesThatFailed = append(suitesThatFailed, compilationOutput.runner.Suite) - if !keepGoing { - break - } - } - if numSuitesThatRan < len(runners) && !config.DefaultReporterConfig.Succinct { - fmt.Println("") - } - } - - if keepGoing && !runResult.Passed { - r.listFailedSuites(suitesThatFailed) - } - - return runResult, numSuitesThatRan -} - -func (r *SuiteRunner) listFailedSuites(suitesThatFailed []testsuite.TestSuite) { - fmt.Println("") - fmt.Println("There were failures detected in the following suites:") - - maxPackageNameLength := 0 - for _, suite := range suitesThatFailed { - if len(suite.PackageName) > maxPackageNameLength { - maxPackageNameLength = len(suite.PackageName) - } - } - - packageNameFormatter := fmt.Sprintf("%%%ds", maxPackageNameLength) - - for _, suite := range suitesThatFailed { - if config.DefaultReporterConfig.NoColor { - fmt.Printf("\t"+packageNameFormatter+" %s\n", suite.PackageName, suite.Path) - } else { - fmt.Fprintf(colorable.NewColorableStdout(), "\t%s"+packageNameFormatter+"%s %s%s%s\n", redColor, suite.PackageName, defaultStyle, lightGrayColor, suite.Path, defaultStyle) - } - } -} diff --git a/ginkgo/testrunner/build_args.go b/ginkgo/testrunner/build_args.go deleted file mode 100644 index 3b1a238c2c..0000000000 --- a/ginkgo/testrunner/build_args.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build go1.10 - -package testrunner - -var ( - buildArgs = []string{"test", "-c"} -) diff --git a/ginkgo/testrunner/build_args_old.go b/ginkgo/testrunner/build_args_old.go deleted file mode 100644 index 14d70dbcc5..0000000000 --- a/ginkgo/testrunner/build_args_old.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build !go1.10 - -package testrunner - -var ( - buildArgs = []string{"test", "-c", "-i"} -) diff --git a/ginkgo/testrunner/log_writer.go b/ginkgo/testrunner/log_writer.go deleted file mode 100644 index a73a6e3791..0000000000 --- a/ginkgo/testrunner/log_writer.go +++ /dev/null @@ -1,52 +0,0 @@ -package testrunner - -import ( - "bytes" - "fmt" - "io" - "log" - "strings" - "sync" -) - -type logWriter struct { - buffer *bytes.Buffer - lock *sync.Mutex - log *log.Logger -} - -func newLogWriter(target io.Writer, node int) *logWriter { - return &logWriter{ - buffer: &bytes.Buffer{}, - lock: &sync.Mutex{}, - log: log.New(target, fmt.Sprintf("[%d] ", node), 0), - } -} - -func (w *logWriter) Write(data []byte) (n int, err error) { - w.lock.Lock() - defer w.lock.Unlock() - - w.buffer.Write(data) - contents := w.buffer.String() - - lines := strings.Split(contents, "\n") - for _, line := range lines[0 : len(lines)-1] { - w.log.Println(line) - } - - w.buffer.Reset() - w.buffer.Write([]byte(lines[len(lines)-1])) - return len(data), nil -} - -func (w *logWriter) Close() error { - w.lock.Lock() - defer w.lock.Unlock() - - if w.buffer.Len() > 0 { - w.log.Println(w.buffer.String()) - } - - return nil -} diff --git a/ginkgo/testrunner/run_result.go b/ginkgo/testrunner/run_result.go deleted file mode 100644 index 5d472acb8d..0000000000 --- a/ginkgo/testrunner/run_result.go +++ /dev/null @@ -1,27 +0,0 @@ -package testrunner - -type RunResult struct { - Passed bool - HasProgrammaticFocus bool -} - -func PassingRunResult() RunResult { - return RunResult{ - Passed: true, - HasProgrammaticFocus: false, - } -} - -func FailingRunResult() RunResult { - return RunResult{ - Passed: false, - HasProgrammaticFocus: false, - } -} - -func (r RunResult) Merge(o RunResult) RunResult { - return RunResult{ - Passed: r.Passed && o.Passed, - HasProgrammaticFocus: r.HasProgrammaticFocus || o.HasProgrammaticFocus, - } -} diff --git a/ginkgo/testrunner/test_runner.go b/ginkgo/testrunner/test_runner.go deleted file mode 100644 index 66c0f06f62..0000000000 --- a/ginkgo/testrunner/test_runner.go +++ /dev/null @@ -1,554 +0,0 @@ -package testrunner - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "syscall" - "time" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/ginkgo/testsuite" - "github.com/onsi/ginkgo/internal/remote" - "github.com/onsi/ginkgo/reporters/stenographer" - colorable "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable" - "github.com/onsi/ginkgo/types" -) - -type TestRunner struct { - Suite testsuite.TestSuite - - compiled bool - compilationTargetPath string - - numCPU int - parallelStream bool - timeout time.Duration - goOpts map[string]interface{} - additionalArgs []string - stderr *bytes.Buffer - - CoverageFile string -} - -func New(suite testsuite.TestSuite, numCPU int, parallelStream bool, timeout time.Duration, goOpts map[string]interface{}, additionalArgs []string) *TestRunner { - runner := &TestRunner{ - Suite: suite, - numCPU: numCPU, - parallelStream: parallelStream, - goOpts: goOpts, - additionalArgs: additionalArgs, - timeout: timeout, - stderr: new(bytes.Buffer), - } - - if !suite.Precompiled { - runner.compilationTargetPath, _ = filepath.Abs(filepath.Join(suite.Path, suite.PackageName+".test")) - } - - return runner -} - -func (t *TestRunner) Compile() error { - return t.CompileTo(t.compilationTargetPath) -} - -func (t *TestRunner) BuildArgs(path string) []string { - args := make([]string, len(buildArgs), len(buildArgs)+3) - copy(args, buildArgs) - args = append(args, "-o", path, t.Suite.Path) - - if t.getCoverMode() != "" { - args = append(args, "-cover", fmt.Sprintf("-covermode=%s", t.getCoverMode())) - } else { - if t.shouldCover() || t.getCoverPackage() != "" { - args = append(args, "-cover", "-covermode=atomic") - } - } - - boolOpts := []string{ - "a", - "n", - "msan", - "race", - "x", - "work", - "linkshared", - } - - for _, opt := range boolOpts { - if s, found := t.goOpts[opt].(*bool); found && *s { - args = append(args, fmt.Sprintf("-%s", opt)) - } - } - - intOpts := []string{ - "memprofilerate", - "blockprofilerate", - } - - for _, opt := range intOpts { - if s, found := t.goOpts[opt].(*int); found { - args = append(args, fmt.Sprintf("-%s=%d", opt, *s)) - } - } - - stringOpts := []string{ - "asmflags", - "buildmode", - "compiler", - "gccgoflags", - "installsuffix", - "ldflags", - "pkgdir", - "toolexec", - "coverprofile", - "cpuprofile", - "memprofile", - "outputdir", - "coverpkg", - "tags", - "gcflags", - "vet", - "mod", - } - - for _, opt := range stringOpts { - if s, found := t.goOpts[opt].(*string); found && *s != "" { - args = append(args, fmt.Sprintf("-%s=%s", opt, *s)) - } - } - return args -} - -func (t *TestRunner) CompileTo(path string) error { - if t.compiled { - return nil - } - - if t.Suite.Precompiled { - return nil - } - - args := t.BuildArgs(path) - cmd := exec.Command("go", args...) - - output, err := cmd.CombinedOutput() - - if err != nil { - if len(output) > 0 { - return fmt.Errorf("Failed to compile %s:\n\n%s", t.Suite.PackageName, output) - } - return fmt.Errorf("Failed to compile %s", t.Suite.PackageName) - } - - if len(output) > 0 { - fmt.Println(string(output)) - } - - if !fileExists(path) { - compiledFile := t.Suite.PackageName + ".test" - if fileExists(compiledFile) { - // seems like we are on an old go version that does not support the -o flag on go test - // move the compiled test file to the desired location by hand - err = os.Rename(compiledFile, path) - if err != nil { - // We cannot move the file, perhaps because the source and destination - // are on different partitions. We can copy the file, however. - err = copyFile(compiledFile, path) - if err != nil { - return fmt.Errorf("Failed to copy compiled file: %s", err) - } - } - } else { - return fmt.Errorf("Failed to compile %s: output file %q could not be found", t.Suite.PackageName, path) - } - } - - t.compiled = true - - return nil -} - -func fileExists(path string) bool { - _, err := os.Stat(path) - return err == nil || !os.IsNotExist(err) -} - -// copyFile copies the contents of the file named src to the file named -// by dst. The file will be created if it does not already exist. If the -// destination file exists, all it's contents will be replaced by the contents -// of the source file. -func copyFile(src, dst string) error { - srcInfo, err := os.Stat(src) - if err != nil { - return err - } - mode := srcInfo.Mode() - - in, err := os.Open(src) - if err != nil { - return err - } - - defer in.Close() - - out, err := os.Create(dst) - if err != nil { - return err - } - - defer func() { - closeErr := out.Close() - if err == nil { - err = closeErr - } - }() - - _, err = io.Copy(out, in) - if err != nil { - return err - } - - err = out.Sync() - if err != nil { - return err - } - - return out.Chmod(mode) -} - -func (t *TestRunner) Run() RunResult { - if t.Suite.IsGinkgo { - if t.numCPU > 1 { - if t.parallelStream { - return t.runAndStreamParallelGinkgoSuite() - } else { - return t.runParallelGinkgoSuite() - } - } else { - return t.runSerialGinkgoSuite() - } - } else { - return t.runGoTestSuite() - } -} - -func (t *TestRunner) CleanUp() { - if t.Suite.Precompiled { - return - } - os.Remove(t.compilationTargetPath) -} - -func (t *TestRunner) runSerialGinkgoSuite() RunResult { - ginkgoArgs := config.BuildFlagArgs("ginkgo", config.GinkgoConfig, config.DefaultReporterConfig) - return t.run(t.cmd(ginkgoArgs, os.Stdout, 1), nil) -} - -func (t *TestRunner) runGoTestSuite() RunResult { - return t.run(t.cmd([]string{"-test.v"}, os.Stdout, 1), nil) -} - -func (t *TestRunner) runAndStreamParallelGinkgoSuite() RunResult { - completions := make(chan RunResult) - writers := make([]*logWriter, t.numCPU) - - server, err := remote.NewServer(t.numCPU) - if err != nil { - panic("Failed to start parallel spec server") - } - - server.Start() - defer server.Close() - - for cpu := 0; cpu < t.numCPU; cpu++ { - config.GinkgoConfig.ParallelNode = cpu + 1 - config.GinkgoConfig.ParallelTotal = t.numCPU - config.GinkgoConfig.SyncHost = server.Address() - - ginkgoArgs := config.BuildFlagArgs("ginkgo", config.GinkgoConfig, config.DefaultReporterConfig) - - writers[cpu] = newLogWriter(os.Stdout, cpu+1) - - cmd := t.cmd(ginkgoArgs, writers[cpu], cpu+1) - - server.RegisterAlive(cpu+1, func() bool { - if cmd.ProcessState == nil { - return true - } - return !cmd.ProcessState.Exited() - }) - - go t.run(cmd, completions) - } - - res := PassingRunResult() - - for cpu := 0; cpu < t.numCPU; cpu++ { - res = res.Merge(<-completions) - } - - for _, writer := range writers { - writer.Close() - } - - os.Stdout.Sync() - - if t.shouldCombineCoverprofiles() { - t.combineCoverprofiles() - } - - return res -} - -func (t *TestRunner) runParallelGinkgoSuite() RunResult { - result := make(chan bool) - completions := make(chan RunResult) - writers := make([]*logWriter, t.numCPU) - reports := make([]*bytes.Buffer, t.numCPU) - - stenographer := stenographer.New(!config.DefaultReporterConfig.NoColor, config.GinkgoConfig.FlakeAttempts > 1, colorable.NewColorableStdout()) - aggregator := remote.NewAggregator(t.numCPU, result, config.DefaultReporterConfig, stenographer) - - server, err := remote.NewServer(t.numCPU) - if err != nil { - panic("Failed to start parallel spec server") - } - server.RegisterReporters(aggregator) - server.Start() - defer server.Close() - - for cpu := 0; cpu < t.numCPU; cpu++ { - config.GinkgoConfig.ParallelNode = cpu + 1 - config.GinkgoConfig.ParallelTotal = t.numCPU - config.GinkgoConfig.SyncHost = server.Address() - config.GinkgoConfig.StreamHost = server.Address() - - ginkgoArgs := config.BuildFlagArgs("ginkgo", config.GinkgoConfig, config.DefaultReporterConfig) - - reports[cpu] = &bytes.Buffer{} - writers[cpu] = newLogWriter(reports[cpu], cpu+1) - - cmd := t.cmd(ginkgoArgs, writers[cpu], cpu+1) - - server.RegisterAlive(cpu+1, func() bool { - if cmd.ProcessState == nil { - return true - } - return !cmd.ProcessState.Exited() - }) - - go t.run(cmd, completions) - } - - res := PassingRunResult() - - for cpu := 0; cpu < t.numCPU; cpu++ { - res = res.Merge(<-completions) - } - - //all test processes are done, at this point - //we should be able to wait for the aggregator to tell us that it's done - - select { - case <-result: - fmt.Println("") - case <-time.After(time.Second): - //the aggregator never got back to us! something must have gone wrong - fmt.Println(` - ------------------------------------------------------------------- - | | - | Ginkgo timed out waiting for all parallel nodes to report back! | - | | - -------------------------------------------------------------------`) - fmt.Println("\n", t.Suite.PackageName, "timed out. path:", t.Suite.Path) - os.Stdout.Sync() - - for _, writer := range writers { - writer.Close() - } - - for _, report := range reports { - fmt.Print(report.String()) - } - - os.Stdout.Sync() - } - - if t.shouldCombineCoverprofiles() { - t.combineCoverprofiles() - } - - return res -} - -const CoverProfileSuffix = ".coverprofile" - -func (t *TestRunner) cmd(ginkgoArgs []string, stream io.Writer, node int) *exec.Cmd { - args := []string{"--test.timeout=" + t.timeout.String()} - - coverProfile := t.getCoverProfile() - - if t.shouldCombineCoverprofiles() { - - testCoverProfile := "--test.coverprofile=" - - coverageFile := "" - // Set default name for coverage results - if coverProfile == "" { - coverageFile = t.Suite.PackageName + CoverProfileSuffix - } else { - coverageFile = coverProfile - } - - testCoverProfile += coverageFile - - t.CoverageFile = filepath.Join(t.Suite.Path, coverageFile) - - if t.numCPU > 1 { - testCoverProfile = fmt.Sprintf("%s.%d", testCoverProfile, node) - } - args = append(args, testCoverProfile) - } - - args = append(args, ginkgoArgs...) - args = append(args, t.additionalArgs...) - - path := t.compilationTargetPath - if t.Suite.Precompiled { - path, _ = filepath.Abs(filepath.Join(t.Suite.Path, fmt.Sprintf("%s.test", t.Suite.PackageName))) - } - - cmd := exec.Command(path, args...) - - cmd.Dir = t.Suite.Path - cmd.Stderr = io.MultiWriter(stream, t.stderr) - cmd.Stdout = stream - - return cmd -} - -func (t *TestRunner) shouldCover() bool { - return *t.goOpts["cover"].(*bool) -} - -func (t *TestRunner) shouldRequireSuite() bool { - return *t.goOpts["requireSuite"].(*bool) -} - -func (t *TestRunner) getCoverProfile() string { - return *t.goOpts["coverprofile"].(*string) -} - -func (t *TestRunner) getCoverPackage() string { - return *t.goOpts["coverpkg"].(*string) -} - -func (t *TestRunner) getCoverMode() string { - return *t.goOpts["covermode"].(*string) -} - -func (t *TestRunner) shouldCombineCoverprofiles() bool { - return t.shouldCover() || t.getCoverPackage() != "" || t.getCoverMode() != "" -} - -func (t *TestRunner) run(cmd *exec.Cmd, completions chan RunResult) RunResult { - var res RunResult - - defer func() { - if completions != nil { - completions <- res - } - }() - - err := cmd.Start() - if err != nil { - fmt.Printf("Failed to run test suite!\n\t%s", err.Error()) - return res - } - - cmd.Wait() - - exitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() - res.Passed = (exitStatus == 0) || (exitStatus == types.GINKGO_FOCUS_EXIT_CODE) - res.HasProgrammaticFocus = (exitStatus == types.GINKGO_FOCUS_EXIT_CODE) - - if strings.Contains(t.stderr.String(), "warning: no tests to run") { - if t.shouldRequireSuite() { - res.Passed = false - } - fmt.Fprintf(os.Stderr, `Found no test suites, did you forget to run "ginkgo bootstrap"?`) - } - - return res -} - -func (t *TestRunner) combineCoverprofiles() { - profiles := []string{} - - coverProfile := t.getCoverProfile() - - for cpu := 1; cpu <= t.numCPU; cpu++ { - var coverFile string - if coverProfile == "" { - coverFile = fmt.Sprintf("%s%s.%d", t.Suite.PackageName, CoverProfileSuffix, cpu) - } else { - coverFile = fmt.Sprintf("%s.%d", coverProfile, cpu) - } - - coverFile = filepath.Join(t.Suite.Path, coverFile) - coverProfile, err := ioutil.ReadFile(coverFile) - os.Remove(coverFile) - - if err == nil { - profiles = append(profiles, string(coverProfile)) - } - } - - if len(profiles) != t.numCPU { - return - } - - lines := map[string]int{} - lineOrder := []string{} - for i, coverProfile := range profiles { - for _, line := range strings.Split(coverProfile, "\n")[1:] { - if len(line) == 0 { - continue - } - components := strings.Split(line, " ") - count, _ := strconv.Atoi(components[len(components)-1]) - prefix := strings.Join(components[0:len(components)-1], " ") - lines[prefix] += count - if i == 0 { - lineOrder = append(lineOrder, prefix) - } - } - } - - output := []string{"mode: atomic"} - for _, line := range lineOrder { - output = append(output, fmt.Sprintf("%s %d", line, lines[line])) - } - finalOutput := strings.Join(output, "\n") - - finalFilename := "" - - if coverProfile != "" { - finalFilename = coverProfile - } else { - finalFilename = fmt.Sprintf("%s%s", t.Suite.PackageName, CoverProfileSuffix) - } - - coverageFilepath := filepath.Join(t.Suite.Path, finalFilename) - ioutil.WriteFile(coverageFilepath, []byte(finalOutput), 0666) - - t.CoverageFile = coverageFilepath -} diff --git a/ginkgo/testrunner/test_runner_test.go b/ginkgo/testrunner/test_runner_test.go deleted file mode 100644 index 89e4f0736f..0000000000 --- a/ginkgo/testrunner/test_runner_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package testrunner_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - "github.com/onsi/ginkgo/ginkgo/testrunner" - "github.com/onsi/ginkgo/ginkgo/testsuite" - . "github.com/onsi/gomega" -) - -func strAddr(s string) interface{} { - return &s -} - -func boolAddr(s bool) interface{} { - return &s -} - -func intAddr(s int) interface{} { - return &s -} - -var _ = Describe("TestRunner", func() { - It("should pass through go opts", func() { - //var opts map[string]interface{} - opts := map[string]interface{}{ - "asmflags": strAddr("a"), - "pkgdir": strAddr("b"), - "gcflags": strAddr("c"), - "covermode": strAddr(""), - "coverpkg": strAddr(""), - "cover": boolAddr(false), - "blockprofilerate": intAddr(100), - "vet": strAddr("off"), - "mod": strAddr("vendor"), - } - tr := testrunner.New(testsuite.TestSuite{}, 1, false, 0, opts, []string{}) - - args := tr.BuildArgs(".") - // Remove the "-i" argument; This is discarded in Golang 1.10+. - if args[2] == "-i" { - args = append(args[0:2], args[3:]...) - } - Ω(args).Should(Equal([]string{ - "test", - "-c", - "-o", - ".", - "", - "-blockprofilerate=100", - "-asmflags=a", - "-pkgdir=b", - "-gcflags=c", - "-vet=off", - "-mod=vendor", - })) - }) -}) - -func TestTestRunner(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Test Runner Suite") -} diff --git a/ginkgo/testsuite/test_suite.go b/ginkgo/testsuite/test_suite.go deleted file mode 100644 index 9de8c2bb4e..0000000000 --- a/ginkgo/testsuite/test_suite.go +++ /dev/null @@ -1,115 +0,0 @@ -package testsuite - -import ( - "errors" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "strings" -) - -type TestSuite struct { - Path string - PackageName string - IsGinkgo bool - Precompiled bool -} - -func PrecompiledTestSuite(path string) (TestSuite, error) { - info, err := os.Stat(path) - if err != nil { - return TestSuite{}, err - } - - if info.IsDir() { - return TestSuite{}, errors.New("this is a directory, not a file") - } - - if filepath.Ext(path) != ".test" { - return TestSuite{}, errors.New("this is not a .test binary") - } - - if info.Mode()&0111 == 0 { - return TestSuite{}, errors.New("this is not executable") - } - - dir := relPath(filepath.Dir(path)) - packageName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) - - return TestSuite{ - Path: dir, - PackageName: packageName, - IsGinkgo: true, - Precompiled: true, - }, nil -} - -func SuitesInDir(dir string, recurse bool) []TestSuite { - suites := []TestSuite{} - - if vendorExperimentCheck(dir) { - return suites - } - - files, _ := ioutil.ReadDir(dir) - re := regexp.MustCompile(`^[^._].*_test\.go$`) - for _, file := range files { - if !file.IsDir() && re.Match([]byte(file.Name())) { - suites = append(suites, New(dir, files)) - break - } - } - - if recurse { - re = regexp.MustCompile(`^[._]`) - for _, file := range files { - if file.IsDir() && !re.Match([]byte(file.Name())) { - suites = append(suites, SuitesInDir(dir+"/"+file.Name(), recurse)...) - } - } - } - - return suites -} - -func relPath(dir string) string { - dir, _ = filepath.Abs(dir) - cwd, _ := os.Getwd() - dir, _ = filepath.Rel(cwd, filepath.Clean(dir)) - - if string(dir[0]) != "." { - dir = "." + string(filepath.Separator) + dir - } - - return dir -} - -func New(dir string, files []os.FileInfo) TestSuite { - return TestSuite{ - Path: relPath(dir), - PackageName: packageNameForSuite(dir), - IsGinkgo: filesHaveGinkgoSuite(dir, files), - } -} - -func packageNameForSuite(dir string) string { - path, _ := filepath.Abs(dir) - return filepath.Base(path) -} - -func filesHaveGinkgoSuite(dir string, files []os.FileInfo) bool { - reTestFile := regexp.MustCompile(`_test\.go$`) - reGinkgo := regexp.MustCompile(`package ginkgo|\/ginkgo"`) - - for _, file := range files { - if !file.IsDir() && reTestFile.Match([]byte(file.Name())) { - contents, _ := ioutil.ReadFile(dir + "/" + file.Name()) - if reGinkgo.Match(contents) { - return true - } - } - } - - return false -} diff --git a/ginkgo/testsuite/testsuite_test.go b/ginkgo/testsuite/testsuite_test.go deleted file mode 100644 index 7a0753bf59..0000000000 --- a/ginkgo/testsuite/testsuite_test.go +++ /dev/null @@ -1,212 +0,0 @@ -// +build go1.6 - -package testsuite_test - -import ( - "io/ioutil" - "os" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/ginkgo/testsuite" - . "github.com/onsi/gomega" -) - -var _ = Describe("TestSuite", func() { - var tmpDir string - var relTmpDir string - - writeFile := func(folder string, filename string, content string, mode os.FileMode) { - path := filepath.Join(tmpDir, folder) - err := os.MkdirAll(path, 0700) - Ω(err).ShouldNot(HaveOccurred()) - - path = filepath.Join(path, filename) - ioutil.WriteFile(path, []byte(content), mode) - } - - var origVendor string - - BeforeSuite(func() { - origVendor = os.Getenv("GO15VENDOREXPERIMENT") - }) - - AfterSuite(func() { - os.Setenv("GO15VENDOREXPERIMENT", origVendor) - }) - - BeforeEach(func() { - var err error - tmpDir, err = ioutil.TempDir("/tmp", "ginkgo") - Ω(err).ShouldNot(HaveOccurred()) - - cwd, err := os.Getwd() - Ω(err).ShouldNot(HaveOccurred()) - relTmpDir, err = filepath.Rel(cwd, tmpDir) - Ω(err).ShouldNot(HaveOccurred()) - - //go files in the root directory (no tests) - writeFile("/", "main.go", "package main", 0666) - - //non-go files in a nested directory - writeFile("/redherring", "big_test.jpg", "package ginkgo", 0666) - - //ginkgo tests in ignored go files - writeFile("/ignored", ".ignore_dot_test.go", `import "github.com/onsi/ginkgo"`, 0666) - writeFile("/ignored", "_ignore_underscore_test.go", `import "github.com/onsi/ginkgo"`, 0666) - - //non-ginkgo tests in a nested directory - writeFile("/professorplum", "professorplum_test.go", `import "testing"`, 0666) - - //ginkgo tests in a nested directory - writeFile("/colonelmustard", "colonelmustard_test.go", `import "github.com/onsi/ginkgo"`, 0666) - - //ginkgo tests in a deeply nested directory - writeFile("/colonelmustard/library", "library_test.go", `import "github.com/onsi/ginkgo"`, 0666) - - //ginkgo tests deeply nested in a vendored dependency - writeFile("/vendor/mrspeacock/lounge", "lounge_test.go", `import "github.com/onsi/ginkgo"`, 0666) - - //a precompiled ginkgo test - writeFile("/precompiled-dir", "precompiled.test", `fake-binary-file`, 0777) - writeFile("/precompiled-dir", "some-other-binary", `fake-binary-file`, 0777) - writeFile("/precompiled-dir", "nonexecutable.test", `fake-binary-file`, 0666) - }) - - AfterEach(func() { - os.RemoveAll(tmpDir) - }) - - Describe("Finding precompiled test suites", func() { - Context("if pointed at an executable file that ends with .test", func() { - It("should return a precompiled test suite", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "precompiled.test")) - Ω(err).ShouldNot(HaveOccurred()) - Ω(suite).Should(Equal(TestSuite{ - Path: relTmpDir + "/precompiled-dir", - PackageName: "precompiled", - IsGinkgo: true, - Precompiled: true, - })) - }) - }) - - Context("if pointed at a directory", func() { - It("should error", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir")) - Ω(suite).Should(BeZero()) - Ω(err).Should(HaveOccurred()) - }) - }) - - Context("if pointed at an executable that doesn't have .test", func() { - It("should error", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "some-other-binary")) - Ω(suite).Should(BeZero()) - Ω(err).Should(HaveOccurred()) - }) - }) - - Context("if pointed at a .test that isn't executable", func() { - It("should error", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "nonexecutable.test")) - Ω(suite).Should(BeZero()) - Ω(err).Should(HaveOccurred()) - }) - }) - - Context("if pointed at a nonexisting file", func() { - It("should error", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "nope-nothing-to-see-here")) - Ω(suite).Should(BeZero()) - Ω(err).Should(HaveOccurred()) - }) - }) - }) - - Describe("scanning for suites in a directory", func() { - Context("when there are no tests in the specified directory", func() { - It("should come up empty", func() { - suites := SuitesInDir(tmpDir, false) - Ω(suites).Should(BeEmpty()) - }) - }) - - Context("when there are ginkgo tests in the specified directory", func() { - It("should return an appropriately configured suite", func() { - suites := SuitesInDir(filepath.Join(tmpDir, "colonelmustard"), false) - Ω(suites).Should(HaveLen(1)) - - Ω(suites[0].Path).Should(Equal(relTmpDir + "/colonelmustard")) - Ω(suites[0].PackageName).Should(Equal("colonelmustard")) - Ω(suites[0].IsGinkgo).Should(BeTrue()) - Ω(suites[0].Precompiled).Should(BeFalse()) - }) - }) - - Context("when there are ginkgo tests that are ignored by go in the specified directory ", func() { - It("should come up empty", func() { - suites := SuitesInDir(filepath.Join(tmpDir, "ignored"), false) - Ω(suites).Should(BeEmpty()) - }) - }) - - Context("when there are non-ginkgo tests in the specified directory", func() { - It("should return an appropriately configured suite", func() { - suites := SuitesInDir(filepath.Join(tmpDir, "professorplum"), false) - Ω(suites).Should(HaveLen(1)) - - Ω(suites[0].Path).Should(Equal(relTmpDir + "/professorplum")) - Ω(suites[0].PackageName).Should(Equal("professorplum")) - Ω(suites[0].IsGinkgo).Should(BeFalse()) - Ω(suites[0].Precompiled).Should(BeFalse()) - }) - }) - - Context("given GO15VENDOREXPERIMENT disabled", func() { - BeforeEach(func() { - os.Setenv("GO15VENDOREXPERIMENT", "0") - }) - - AfterEach(func() { - os.Setenv("GO15VENDOREXPERIMENT", "") - }) - - It("should not skip vendor dirs", func() { - suites := SuitesInDir(filepath.Join(tmpDir+"/vendor"), true) - Ω(suites).Should(HaveLen(1)) - }) - - It("should recurse into vendor dirs", func() { - suites := SuitesInDir(filepath.Join(tmpDir), true) - Ω(suites).Should(HaveLen(4)) - }) - }) - - Context("when recursively scanning", func() { - It("should return suites for corresponding test suites, only", func() { - suites := SuitesInDir(tmpDir, true) - Ω(suites).Should(HaveLen(3)) - - Ω(suites).Should(ContainElement(TestSuite{ - Path: relTmpDir + "/colonelmustard", - PackageName: "colonelmustard", - IsGinkgo: true, - Precompiled: false, - })) - Ω(suites).Should(ContainElement(TestSuite{ - Path: relTmpDir + "/professorplum", - PackageName: "professorplum", - IsGinkgo: false, - Precompiled: false, - })) - Ω(suites).Should(ContainElement(TestSuite{ - Path: relTmpDir + "/colonelmustard/library", - PackageName: "library", - IsGinkgo: true, - Precompiled: false, - })) - }) - }) - }) -}) diff --git a/ginkgo/testsuite/vendor_check_go15.go b/ginkgo/testsuite/vendor_check_go15.go deleted file mode 100644 index 75f827a121..0000000000 --- a/ginkgo/testsuite/vendor_check_go15.go +++ /dev/null @@ -1,16 +0,0 @@ -// +build !go1.6 - -package testsuite - -import ( - "os" - "path" -) - -// "This change will only be enabled if the go command is run with -// GO15VENDOREXPERIMENT=1 in its environment." -// c.f. the vendor-experiment proposal https://goo.gl/2ucMeC -func vendorExperimentCheck(dir string) bool { - vendorExperiment := os.Getenv("GO15VENDOREXPERIMENT") - return vendorExperiment == "1" && path.Base(dir) == "vendor" -} diff --git a/ginkgo/testsuite/vendor_check_go15_test.go b/ginkgo/testsuite/vendor_check_go15_test.go deleted file mode 100644 index dc3ca2a942..0000000000 --- a/ginkgo/testsuite/vendor_check_go15_test.go +++ /dev/null @@ -1,201 +0,0 @@ -// +build !go1.6 - -package testsuite_test - -import ( - "io/ioutil" - "os" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/ginkgo/testsuite" - . "github.com/onsi/gomega" -) - -var _ = Describe("TestSuite", func() { - var tmpDir string - var relTmpDir string - - writeFile := func(folder string, filename string, content string, mode os.FileMode) { - path := filepath.Join(tmpDir, folder) - err := os.MkdirAll(path, 0700) - Ω(err).ShouldNot(HaveOccurred()) - - path = filepath.Join(path, filename) - ioutil.WriteFile(path, []byte(content), mode) - } - - var origVendor string - - BeforeSuite(func() { - origVendor = os.Getenv("GO15VENDOREXPERIMENT") - }) - - AfterSuite(func() { - os.Setenv("GO15VENDOREXPERIMENT", origVendor) - }) - - BeforeEach(func() { - var err error - tmpDir, err = ioutil.TempDir("/tmp", "ginkgo") - Ω(err).ShouldNot(HaveOccurred()) - - cwd, err := os.Getwd() - Ω(err).ShouldNot(HaveOccurred()) - relTmpDir, err = filepath.Rel(cwd, tmpDir) - Ω(err).ShouldNot(HaveOccurred()) - - //go files in the root directory (no tests) - writeFile("/", "main.go", "package main", 0666) - - //non-go files in a nested directory - writeFile("/redherring", "big_test.jpg", "package ginkgo", 0666) - - //non-ginkgo tests in a nested directory - writeFile("/professorplum", "professorplum_test.go", `import "testing"`, 0666) - - //ginkgo tests in a nested directory - writeFile("/colonelmustard", "colonelmustard_test.go", `import "github.com/onsi/ginkgo"`, 0666) - - //ginkgo tests in a deeply nested directory - writeFile("/colonelmustard/library", "library_test.go", `import "github.com/onsi/ginkgo"`, 0666) - - //ginkgo tests deeply nested in a vendored dependency - writeFile("/vendor/mrspeacock/lounge", "lounge_test.go", `import "github.com/onsi/ginkgo"`, 0666) - - //a precompiled ginkgo test - writeFile("/precompiled-dir", "precompiled.test", `fake-binary-file`, 0777) - writeFile("/precompiled-dir", "some-other-binary", `fake-binary-file`, 0777) - writeFile("/precompiled-dir", "nonexecutable.test", `fake-binary-file`, 0666) - }) - - AfterEach(func() { - os.RemoveAll(tmpDir) - }) - - Describe("Finding precompiled test suites", func() { - Context("if pointed at an executable file that ends with .test", func() { - It("should return a precompiled test suite", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "precompiled.test")) - Ω(err).ShouldNot(HaveOccurred()) - Ω(suite).Should(Equal(TestSuite{ - Path: relTmpDir + "/precompiled-dir", - PackageName: "precompiled", - IsGinkgo: true, - Precompiled: true, - })) - }) - }) - - Context("if pointed at a directory", func() { - It("should error", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir")) - Ω(suite).Should(BeZero()) - Ω(err).Should(HaveOccurred()) - }) - }) - - Context("if pointed at an executable that doesn't have .test", func() { - It("should error", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "some-other-binary")) - Ω(suite).Should(BeZero()) - Ω(err).Should(HaveOccurred()) - }) - }) - - Context("if pointed at a .test that isn't executable", func() { - It("should error", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "nonexecutable.test")) - Ω(suite).Should(BeZero()) - Ω(err).Should(HaveOccurred()) - }) - }) - - Context("if pointed at a nonexisting file", func() { - It("should error", func() { - suite, err := PrecompiledTestSuite(filepath.Join(tmpDir, "precompiled-dir", "nope-nothing-to-see-here")) - Ω(suite).Should(BeZero()) - Ω(err).Should(HaveOccurred()) - }) - }) - }) - - Describe("scanning for suites in a directory", func() { - Context("when there are no tests in the specified directory", func() { - It("should come up empty", func() { - suites := SuitesInDir(tmpDir, false) - Ω(suites).Should(BeEmpty()) - }) - }) - - Context("when there are ginkgo tests in the specified directory", func() { - It("should return an appropriately configured suite", func() { - suites := SuitesInDir(filepath.Join(tmpDir, "colonelmustard"), false) - Ω(suites).Should(HaveLen(1)) - - Ω(suites[0].Path).Should(Equal(relTmpDir + "/colonelmustard")) - Ω(suites[0].PackageName).Should(Equal("colonelmustard")) - Ω(suites[0].IsGinkgo).Should(BeTrue()) - Ω(suites[0].Precompiled).Should(BeFalse()) - }) - }) - - Context("when there are non-ginkgo tests in the specified directory", func() { - It("should return an appropriately configured suite", func() { - suites := SuitesInDir(filepath.Join(tmpDir, "professorplum"), false) - Ω(suites).Should(HaveLen(1)) - - Ω(suites[0].Path).Should(Equal(relTmpDir + "/professorplum")) - Ω(suites[0].PackageName).Should(Equal("professorplum")) - Ω(suites[0].IsGinkgo).Should(BeFalse()) - Ω(suites[0].Precompiled).Should(BeFalse()) - }) - }) - - Context("given GO15VENDOREXPERIMENT", func() { - BeforeEach(func() { - os.Setenv("GO15VENDOREXPERIMENT", "1") - }) - - AfterEach(func() { - os.Setenv("GO15VENDOREXPERIMENT", "") - }) - - It("should skip vendor dirs", func() { - suites := SuitesInDir(filepath.Join(tmpDir+"/vendor"), false) - Ω(suites).Should(HaveLen(0)) - }) - - It("should not recurse into vendor dirs", func() { - suites := SuitesInDir(filepath.Join(tmpDir), true) - Ω(suites).Should(HaveLen(3)) - }) - }) - - Context("when recursively scanning", func() { - It("should return suites for corresponding test suites, only", func() { - suites := SuitesInDir(tmpDir, true) - Ω(suites).Should(HaveLen(4)) - - Ω(suites).Should(ContainElement(TestSuite{ - Path: relTmpDir + "/colonelmustard", - PackageName: "colonelmustard", - IsGinkgo: true, - Precompiled: false, - })) - Ω(suites).Should(ContainElement(TestSuite{ - Path: relTmpDir + "/professorplum", - PackageName: "professorplum", - IsGinkgo: false, - Precompiled: false, - })) - Ω(suites).Should(ContainElement(TestSuite{ - Path: relTmpDir + "/colonelmustard/library", - PackageName: "library", - IsGinkgo: true, - Precompiled: false, - })) - }) - }) - }) -}) diff --git a/ginkgo/testsuite/vendor_check_go16.go b/ginkgo/testsuite/vendor_check_go16.go deleted file mode 100644 index 596e5e5c18..0000000000 --- a/ginkgo/testsuite/vendor_check_go16.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build go1.6 - -package testsuite - -import ( - "os" - "path" -) - -// in 1.6 the vendor directory became the default go behaviour, so now -// check if its disabled. -func vendorExperimentCheck(dir string) bool { - vendorExperiment := os.Getenv("GO15VENDOREXPERIMENT") - return vendorExperiment != "0" && path.Base(dir) == "vendor" -} diff --git a/ginkgo/unfocus_command.go b/ginkgo/unfocus/unfocus_command.go similarity index 87% rename from ginkgo/unfocus_command.go rename to ginkgo/unfocus/unfocus_command.go index d9dfb6e44c..66b5c018a0 100644 --- a/ginkgo/unfocus_command.go +++ b/ginkgo/unfocus/unfocus_command.go @@ -1,8 +1,7 @@ -package main +package unfocus import ( "bytes" - "flag" "fmt" "go/ast" "go/parser" @@ -13,22 +12,23 @@ import ( "path/filepath" "strings" "sync" + + "github.com/onsi/ginkgo/ginkgo/command" ) -func BuildUnfocusCommand() *Command { - return &Command{ - Name: "unfocus", - AltName: "blur", - FlagSet: flag.NewFlagSet("unfocus", flag.ExitOnError), - UsageCommand: "ginkgo unfocus (or ginkgo blur)", - Usage: []string{ - "Recursively unfocuses any focused tests under the current directory", +func BuildUnfocusCommand() command.Command { + return command.Command{ + Name: "unfocus", + Usage: "ginkgo unfocus", + ShortDoc: "Recursively unfocus any focused tests under the current directory", + DocLink: "focused-specs", + Command: func(_ []string, _ []string) { + unfocusSpecs() }, - Command: unfocusSpecs, } } -func unfocusSpecs([]string, []string) { +func unfocusSpecs() { fmt.Println("Scanning for focus...") goFiles := make(chan string) @@ -172,7 +172,7 @@ func scanForFocus(file *ast.File) (eliminations []int64) { func isFocus(name string) bool { switch name { - case "FDescribe", "FContext", "FIt", "FMeasure", "FDescribeTable", "FEntry", "FSpecify", "FWhen": + case "FDescribe", "FContext", "FIt", "FDescribeTable", "FEntry", "FSpecify", "FWhen": return true default: return false diff --git a/ginkgo/version_command.go b/ginkgo/version_command.go deleted file mode 100644 index f586908e87..0000000000 --- a/ginkgo/version_command.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "flag" - "fmt" - - "github.com/onsi/ginkgo/config" -) - -func BuildVersionCommand() *Command { - return &Command{ - Name: "version", - FlagSet: flag.NewFlagSet("version", flag.ExitOnError), - UsageCommand: "ginkgo version", - Usage: []string{ - "Print Ginkgo's version", - }, - Command: printVersion, - } -} - -func printVersion([]string, []string) { - fmt.Printf("Ginkgo Version %s\n", config.VERSION) -} diff --git a/ginkgo/watch/delta_tracker.go b/ginkgo/watch/delta_tracker.go index a628303d72..4bae1027d0 100644 --- a/ginkgo/watch/delta_tracker.go +++ b/ginkgo/watch/delta_tracker.go @@ -5,10 +5,10 @@ import ( "regexp" - "github.com/onsi/ginkgo/ginkgo/testsuite" + "github.com/onsi/ginkgo/ginkgo/internal" ) -type SuiteErrors map[testsuite.TestSuite]error +type SuiteErrors map[internal.TestSuite]error type DeltaTracker struct { maxDepth int @@ -26,7 +26,7 @@ func NewDeltaTracker(maxDepth int, watchRegExp *regexp.Regexp) *DeltaTracker { } } -func (d *DeltaTracker) Delta(suites []testsuite.TestSuite) (delta Delta, errors SuiteErrors) { +func (d *DeltaTracker) Delta(suites []internal.TestSuite) (delta Delta, errors SuiteErrors) { errors = SuiteErrors{} delta.ModifiedPackages = d.packageHashes.CheckForChanges() @@ -65,7 +65,7 @@ func (d *DeltaTracker) Delta(suites []testsuite.TestSuite) (delta Delta, errors return delta, errors } -func (d *DeltaTracker) WillRun(suite testsuite.TestSuite) error { +func (d *DeltaTracker) WillRun(suite internal.TestSuite) error { s, ok := d.suites[suite.Path] if !ok { return fmt.Errorf("unknown suite %s", suite.Path) diff --git a/ginkgo/watch/suite.go b/ginkgo/watch/suite.go index 5deaba7cb6..f645dd72bb 100644 --- a/ginkgo/watch/suite.go +++ b/ginkgo/watch/suite.go @@ -5,18 +5,18 @@ import ( "math" "time" - "github.com/onsi/ginkgo/ginkgo/testsuite" + "github.com/onsi/ginkgo/ginkgo/internal" ) type Suite struct { - Suite testsuite.TestSuite + Suite internal.TestSuite RunTime time.Time Dependencies Dependencies sharedPackageHashes *PackageHashes } -func NewSuite(suite testsuite.TestSuite, maxDepth int, sharedPackageHashes *PackageHashes) (*Suite, error) { +func NewSuite(suite internal.TestSuite, maxDepth int, sharedPackageHashes *PackageHashes) (*Suite, error) { deps, err := NewDependencies(suite.Path, maxDepth) if err != nil { return nil, err diff --git a/ginkgo/watch/watch_command.go b/ginkgo/watch/watch_command.go new file mode 100644 index 0000000000..fdd229b4af --- /dev/null +++ b/ginkgo/watch/watch_command.go @@ -0,0 +1,187 @@ +package watch + +import ( + "fmt" + "regexp" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/ginkgo/command" + "github.com/onsi/ginkgo/ginkgo/internal" + "github.com/onsi/ginkgo/ginkgo/interrupthandler" +) + +func BuildWatchCommand() command.Command { + var ginkgoConfig = config.NewDefaultGinkgoConfig() + var reporterConfig = config.NewDefaultReporterConfig() + var cliConfig = config.NewDefaultGinkgoCLIConfig() + var goFlagsConfig = config.NewDefaultGoFlagsConfig() + + flags, err := config.BuildWatchCommandFlagSet(&ginkgoConfig, &reporterConfig, &cliConfig, &goFlagsConfig) + if err != nil { + panic(err) + } + interruptHandler := interrupthandler.NewInterruptHandler() + + return command.Command{ + Name: "watch", + Flags: flags, + Usage: "ginkgo watch -- ", + ShortDoc: "Watches the passed in and runs their tests whenever changes occur.", + Documentation: "Any arguments after -- will be passed to the test.", + DocLink: "watching-for-changes", + Command: func(args []string, additionalArgs []string) { + var errors []error + cliConfig, goFlagsConfig, errors = config.VetAndInitializeCLIAndGoConfig(cliConfig, goFlagsConfig) + command.AbortIfErrors("Ginkgo detected configuraiotn issues:", errors) + + watcher := &SpecWatcher{ + cliConfig: cliConfig, + goFlagsConfig: goFlagsConfig, + ginkgoConfig: ginkgoConfig, + reporterConfig: reporterConfig, + flags: flags, + + interruptHandler: interruptHandler, + } + + watcher.WatchSpecs(args, additionalArgs) + }, + } +} + +type SpecWatcher struct { + ginkgoConfig config.GinkgoConfigType + reporterConfig config.DefaultReporterConfigType + cliConfig config.GinkgoCLIConfigType + goFlagsConfig config.GoFlagsConfigType + flags config.GinkgoFlagSet + + interruptHandler *interrupthandler.InterruptHandler +} + +func (w *SpecWatcher) WatchSpecs(args []string, additionalArgs []string) { + suites, _ := internal.FindSuites(args, w.cliConfig, false) + + if len(suites) == 0 { + command.AbortWith("Found no test suites") + } + + fmt.Printf("Identified %d test %s. Locating dependencies to a depth of %d (this may take a while)...\n", len(suites), internal.PluralizedWord("suite", "suites", len(suites)), w.cliConfig.Depth) + deltaTracker := NewDeltaTracker(w.cliConfig.Depth, regexp.MustCompile(w.cliConfig.WatchRegExp)) + delta, errors := deltaTracker.Delta(suites) + + fmt.Printf("Watching %d %s:\n", len(delta.NewSuites), internal.PluralizedWord("suite", "suites", len(delta.NewSuites))) + for _, suite := range delta.NewSuites { + fmt.Println(" " + suite.Description()) + } + + for suite, err := range errors { + fmt.Printf("Failed to watch %s: %s\n", suite.PackageName, err) + } + + if len(suites) == 1 { + w.updateSeed() + w.compileAndRun(suites[0], additionalArgs) + } + + ticker := time.NewTicker(time.Second) + + for { + select { + case <-ticker.C: + suites, _ := internal.FindSuites(args, w.cliConfig, false) + delta, _ := deltaTracker.Delta(suites) + coloredStream := formatter.ColorableStdOut + + suites = []internal.TestSuite{} + + if len(delta.NewSuites) > 0 { + fmt.Fprintln(coloredStream, formatter.F("{{green}}Detected %d new %s:{{/}}", len(delta.NewSuites), internal.PluralizedWord("suite", "suites", len(delta.NewSuites)))) + for _, suite := range delta.NewSuites { + suites = append(suites, suite.Suite) + fmt.Fprintln(coloredStream, formatter.Fi(1, "%s", suite.Description())) + } + } + + modifiedSuites := delta.ModifiedSuites() + if len(modifiedSuites) > 0 { + fmt.Fprintln(coloredStream, formatter.F("{{green}}Detected changes in:{{/}}")) + for _, pkg := range delta.ModifiedPackages { + fmt.Fprintln(coloredStream, formatter.Fi(1, "%s", pkg)) + } + fmt.Fprintln(coloredStream, formatter.F("{{green}}Will run %d %s:{{/}}", len(modifiedSuites), internal.PluralizedWord("suite", "suites", len(modifiedSuites)))) + for _, suite := range modifiedSuites { + suites = append(suites, suite.Suite) + fmt.Fprintln(coloredStream, formatter.Fi(1, "%s", suite.Description())) + } + fmt.Fprintln(coloredStream, "") + } + + if len(suites) == 0 { + break + } + + w.updateSeed() + w.computeSuccinctMode(len(suites)) + passed := true + for _, suite := range suites { + if w.interruptHandler.WasInterrupted() { + return + } + deltaTracker.WillRun(suite) + passed = w.compileAndRun(suite, additionalArgs) && passed + } + color := "{{red}}" + if passed { + color = "{{green}}" + } + fmt.Fprintln(coloredStream, formatter.F(color+"\nDone. Resuming watch...{{/}}")) + + err := internal.FinalizeProfilesForSuites(suites, w.cliConfig, w.goFlagsConfig) + command.AbortIfError("could not finalize profiles:", err) + case <-w.interruptHandler.InterruptChannel(): + return + } + } +} + +func (w *SpecWatcher) compileAndRun(suite internal.TestSuite, additionalArgs []string) bool { + suite = internal.CompileSuite(suite, w.goFlagsConfig) + if suite.CompilationError != nil { + fmt.Println(suite.CompilationError.Error()) + return false + } + if w.interruptHandler.WasInterrupted() { + return false + } + suite = internal.RunCompiledSuite(suite, w.ginkgoConfig, w.reporterConfig, w.cliConfig, w.goFlagsConfig, additionalArgs) + internal.Cleanup(suite) + return suite.Passed +} + +func (w *SpecWatcher) computeSuccinctMode(numSuites int) { + if w.reporterConfig.Verbose { + w.reporterConfig.Succinct = false + return + } + + if w.flags.WasSet("succinct") { + return + } + + if numSuites == 1 { + w.reporterConfig.Succinct = false + } + + if numSuites > 1 { + w.reporterConfig.Succinct = true + } +} + +func (w *SpecWatcher) updateSeed() { + if !w.flags.WasSet("seed") { + w.ginkgoConfig.RandomSeed = time.Now().Unix() + } +} diff --git a/ginkgo/watch_command.go b/ginkgo/watch_command.go deleted file mode 100644 index a6ef053c85..0000000000 --- a/ginkgo/watch_command.go +++ /dev/null @@ -1,175 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "regexp" - "time" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/ginkgo/interrupthandler" - "github.com/onsi/ginkgo/ginkgo/testrunner" - "github.com/onsi/ginkgo/ginkgo/testsuite" - "github.com/onsi/ginkgo/ginkgo/watch" - colorable "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable" -) - -func BuildWatchCommand() *Command { - commandFlags := NewWatchCommandFlags(flag.NewFlagSet("watch", flag.ExitOnError)) - interruptHandler := interrupthandler.NewInterruptHandler() - notifier := NewNotifier(commandFlags) - watcher := &SpecWatcher{ - commandFlags: commandFlags, - notifier: notifier, - interruptHandler: interruptHandler, - suiteRunner: NewSuiteRunner(notifier, interruptHandler), - } - - return &Command{ - Name: "watch", - FlagSet: commandFlags.FlagSet, - UsageCommand: "ginkgo watch -- ", - Usage: []string{ - "Watches the tests in the passed in and runs them when changes occur.", - "Any arguments after -- will be passed to the test.", - }, - Command: watcher.WatchSpecs, - SuppressFlagDocumentation: true, - FlagDocSubstitute: []string{ - "Accepts all the flags that the ginkgo command accepts except for --keepGoing and --untilItFails", - }, - } -} - -type SpecWatcher struct { - commandFlags *RunWatchAndBuildCommandFlags - notifier *Notifier - interruptHandler *interrupthandler.InterruptHandler - suiteRunner *SuiteRunner -} - -func (w *SpecWatcher) WatchSpecs(args []string, additionalArgs []string) { - w.commandFlags.computeNodes() - w.notifier.VerifyNotificationsAreAvailable() - - w.WatchSuites(args, additionalArgs) -} - -func (w *SpecWatcher) runnersForSuites(suites []testsuite.TestSuite, additionalArgs []string) []*testrunner.TestRunner { - runners := []*testrunner.TestRunner{} - - for _, suite := range suites { - runners = append(runners, testrunner.New(suite, w.commandFlags.NumCPU, w.commandFlags.ParallelStream, w.commandFlags.Timeout, w.commandFlags.GoOpts, additionalArgs)) - } - - return runners -} - -func (w *SpecWatcher) WatchSuites(args []string, additionalArgs []string) { - suites, _ := findSuites(args, w.commandFlags.Recurse, w.commandFlags.SkipPackage, false) - - if len(suites) == 0 { - complainAndQuit("Found no test suites") - } - - fmt.Printf("Identified %d test %s. Locating dependencies to a depth of %d (this may take a while)...\n", len(suites), pluralizedWord("suite", "suites", len(suites)), w.commandFlags.Depth) - deltaTracker := watch.NewDeltaTracker(w.commandFlags.Depth, regexp.MustCompile(w.commandFlags.WatchRegExp)) - delta, errors := deltaTracker.Delta(suites) - - fmt.Printf("Watching %d %s:\n", len(delta.NewSuites), pluralizedWord("suite", "suites", len(delta.NewSuites))) - for _, suite := range delta.NewSuites { - fmt.Println(" " + suite.Description()) - } - - for suite, err := range errors { - fmt.Printf("Failed to watch %s: %s\n", suite.PackageName, err) - } - - if len(suites) == 1 { - runners := w.runnersForSuites(suites, additionalArgs) - w.suiteRunner.RunSuites(runners, w.commandFlags.NumCompilers, true, nil) - runners[0].CleanUp() - } - - ticker := time.NewTicker(time.Second) - - for { - select { - case <-ticker.C: - suites, _ := findSuites(args, w.commandFlags.Recurse, w.commandFlags.SkipPackage, false) - delta, _ := deltaTracker.Delta(suites) - coloredStream := colorable.NewColorableStdout() - - suitesToRun := []testsuite.TestSuite{} - - if len(delta.NewSuites) > 0 { - fmt.Fprintf(coloredStream, greenColor+"Detected %d new %s:\n"+defaultStyle, len(delta.NewSuites), pluralizedWord("suite", "suites", len(delta.NewSuites))) - for _, suite := range delta.NewSuites { - suitesToRun = append(suitesToRun, suite.Suite) - fmt.Fprintln(coloredStream, " "+suite.Description()) - } - } - - modifiedSuites := delta.ModifiedSuites() - if len(modifiedSuites) > 0 { - fmt.Fprintln(coloredStream, greenColor+"\nDetected changes in:"+defaultStyle) - for _, pkg := range delta.ModifiedPackages { - fmt.Fprintln(coloredStream, " "+pkg) - } - fmt.Fprintf(coloredStream, greenColor+"Will run %d %s:\n"+defaultStyle, len(modifiedSuites), pluralizedWord("suite", "suites", len(modifiedSuites))) - for _, suite := range modifiedSuites { - suitesToRun = append(suitesToRun, suite.Suite) - fmt.Fprintln(coloredStream, " "+suite.Description()) - } - fmt.Fprintln(coloredStream, "") - } - - if len(suitesToRun) > 0 { - w.UpdateSeed() - w.ComputeSuccinctMode(len(suitesToRun)) - runners := w.runnersForSuites(suitesToRun, additionalArgs) - result, _ := w.suiteRunner.RunSuites(runners, w.commandFlags.NumCompilers, true, func(suite testsuite.TestSuite) { - deltaTracker.WillRun(suite) - }) - for _, runner := range runners { - runner.CleanUp() - } - if !w.interruptHandler.WasInterrupted() { - color := redColor - if result.Passed { - color = greenColor - } - fmt.Fprintln(coloredStream, color+"\nDone. Resuming watch..."+defaultStyle) - } - } - - case <-w.interruptHandler.C: - return - } - } -} - -func (w *SpecWatcher) ComputeSuccinctMode(numSuites int) { - if config.DefaultReporterConfig.Verbose { - config.DefaultReporterConfig.Succinct = false - return - } - - if w.commandFlags.wasSet("succinct") { - return - } - - if numSuites == 1 { - config.DefaultReporterConfig.Succinct = false - } - - if numSuites > 1 { - config.DefaultReporterConfig.Succinct = true - } -} - -func (w *SpecWatcher) UpdateSeed() { - if !w.commandFlags.wasSet("seed") { - config.GinkgoConfig.RandomSeed = time.Now().Unix() - } -} diff --git a/ginkgo_dsl.go b/ginkgo_dsl.go index 998c2c2caf..7ded20bc32 100644 --- a/ginkgo_dsl.go +++ b/ginkgo_dsl.go @@ -12,46 +12,35 @@ Ginkgo is MIT-Licensed package ginkgo import ( - "flag" "fmt" "io" - "net/http" "os" "reflect" "strings" "time" "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/internal/codelocation" + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/internal" "github.com/onsi/ginkgo/internal/global" - "github.com/onsi/ginkgo/internal/remote" + "github.com/onsi/ginkgo/internal/parallel_support" "github.com/onsi/ginkgo/internal/testingtproxy" - "github.com/onsi/ginkgo/internal/writer" "github.com/onsi/ginkgo/reporters" - "github.com/onsi/ginkgo/reporters/stenographer" - colorable "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable" "github.com/onsi/ginkgo/types" ) -var deprecationTracker = types.NewDeprecationTracker() - const GINKGO_VERSION = config.VERSION -const GINKGO_PANIC = ` -Your test failed. -Ginkgo panics to prevent subsequent assertions from running. -Normally Ginkgo rescues this panic so you shouldn't see it. - -But, if you make an assertion in a goroutine, Ginkgo can't capture the panic. -To circumvent this, you should call - - defer GinkgoRecover() -at the top of the goroutine that caused this panic. -` +var flagSet config.GinkgoFlagSet +var deprecationTracker = types.NewDeprecationTracker() func init() { - config.Flags(flag.CommandLine, "ginkgo", true) - GinkgoWriter = writer.New(os.Stdout) + var err error + flagSet, err = config.BuildTestSuiteFlagSet() + if err != nil { + panic(err) + } + GinkgoWriter = internal.NewWriter(os.Stdout) } //GinkgoWriter implements an io.Writer @@ -131,18 +120,13 @@ type GinkgoTInterface interface { //Custom Ginkgo test reporters must implement the Reporter interface. // //The custom reporter is passed in a SuiteSummary when the suite begins and ends, -//and a SpecSummary just before a spec begins and just after a spec ends -type Reporter reporters.Reporter - -//Asynchronous specs are given a channel of the Done type. You must close or write to the channel -//to tell Ginkgo that your async test is done. -type Done chan<- interface{} +//and a Summary just before a spec begins and just after a spec ends +type Reporter = reporters.Reporter //GinkgoTestDescription represents the information about the current running test returned by CurrentGinkgoTestDescription // FullTestText: a concatenation of ComponentTexts and the TestText // ComponentTexts: a list of all texts for the Describes & Contexts leading up to the current test -// TestText: the text in the actual It or Measure node -// IsMeasurement: true if the current test is a measurement +// TestText: the text in the It node // FileName: the name of the file containing the current test // LineNumber: the line number for the current test // Failed: if the current test has failed, this will be true (useful in an AfterEach) @@ -151,8 +135,6 @@ type GinkgoTestDescription struct { ComponentTexts []string TestText string - IsMeasurement bool - FileName string LineNumber int @@ -162,41 +144,22 @@ type GinkgoTestDescription struct { //CurrentGinkgoTestDescripton returns information about the current running test. func CurrentGinkgoTestDescription() GinkgoTestDescription { - summary, ok := global.Suite.CurrentRunningSpecSummary() + summary, ok := global.Suite.CurrentSpecSummary() if !ok { return GinkgoTestDescription{} } - subjectCodeLocation := summary.ComponentCodeLocations[len(summary.ComponentCodeLocations)-1] - return GinkgoTestDescription{ - ComponentTexts: summary.ComponentTexts[1:], - FullTestText: strings.Join(summary.ComponentTexts[1:], " "), - TestText: summary.ComponentTexts[len(summary.ComponentTexts)-1], - IsMeasurement: summary.IsMeasurement, - FileName: subjectCodeLocation.FileName, - LineNumber: subjectCodeLocation.LineNumber, - Failed: summary.HasFailureState(), + ComponentTexts: summary.NodeTexts, + FullTestText: strings.Join(summary.NodeTexts, " "), + TestText: summary.NodeTexts[len(summary.NodeTexts)-1], + FileName: summary.LeafNodeLocation.FileName, + LineNumber: summary.LeafNodeLocation.LineNumber, + Failed: summary.State.Is(types.SpecStateFailureStates...), Duration: summary.RunTime, } } -//Measurement tests receive a Benchmarker. -// -//You use the Time() function to time how long the passed in body function takes to run -//You use the RecordValue() function to track arbitrary numerical measurements. -//The RecordValueWithPrecision() function can be used alternatively to provide the unit -//and resolution of the numeric measurement. -//The optional info argument is passed to the test reporter and can be used to -// provide the measurement data to a custom reporter with context. -// -//See http://onsi.github.io/ginkgo/#benchmark_tests for more details -type Benchmarker interface { - Time(name string, body func(), info ...interface{}) (elapsedTime time.Duration) - RecordValue(name string, value float64, info ...interface{}) - RecordValueWithPrecision(name string, value float64, units string, precision int, info ...interface{}) -} - //RunSpecs is the entry point for the Ginkgo test runner. //You must call this within a Golang testing TestX(t *testing.T) function. // @@ -205,42 +168,55 @@ type Benchmarker interface { // ginkgo bootstrap func RunSpecs(t GinkgoTestingT, description string) bool { specReporters := []Reporter{buildDefaultReporter()} - if config.DefaultReporterConfig.ReportFile != "" { - reportFile := config.DefaultReporterConfig.ReportFile - specReporters[0] = reporters.NewJUnitReporter(reportFile) - specReporters = append(specReporters, buildDefaultReporter()) + if config.DefaultReporterConfig.JUnitReportFile != "" { + specReporters[0] = reporters.NewJUnitReporter(config.DefaultReporterConfig.JUnitReportFile) + return RunSpecsWithDefaultAndCustomReporters(t, description, specReporters) } - return runSpecsWithCustomReporters(t, description, specReporters) + return RunSpecsWithCustomReporters(t, description, specReporters) } //To run your tests with Ginkgo's default reporter and your custom reporter(s), replace //RunSpecs() with this method. func RunSpecsWithDefaultAndCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool { - deprecationTracker.TrackDeprecation(types.Deprecations.CustomReporter()) specReporters = append(specReporters, buildDefaultReporter()) - return runSpecsWithCustomReporters(t, description, specReporters) + return RunSpecsWithCustomReporters(t, description, specReporters) } //To run your tests with your custom reporter(s) (and *not* Ginkgo's default reporter), replace //RunSpecs() with this method. Note that parallel tests will not work correctly without the default reporter func RunSpecsWithCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool { - deprecationTracker.TrackDeprecation(types.Deprecations.CustomReporter()) - return runSpecsWithCustomReporters(t, description, specReporters) -} + writer := GinkgoWriter.(*internal.Writer) + writer.SetStream(config.DefaultReporterConfig.Verbose && config.GinkgoConfig.ParallelTotal == 1) -func runSpecsWithCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool { - writer := GinkgoWriter.(*writer.Writer) - writer.SetStream(config.DefaultReporterConfig.Verbose) - reporters := make([]reporters.Reporter, len(specReporters)) - for i, reporter := range specReporters { - reporters[i] = reporter + for reporter := range specReporters { + if _, isDeprecated := reflect.TypeOf(reporter).MethodByName("IsDeprecatedReporter"); isDeprecated { + deprecationTracker.TrackDeprecation(types.Deprecations.V1Report()) + } } - passed, hasFocusedTests := global.Suite.Run(t, description, reporters, writer, config.GinkgoConfig) + multiReporter := reporters.NewMultiReporter(specReporters...) + configErrors := config.VetConfig(flagSet, config.GinkgoConfig, config.DefaultReporterConfig) + if len(configErrors) > 0 { + fmt.Fprintf(formatter.ColorableStdErr, formatter.F("{{red}}Ginkgo detected configuration issues:{{/}}\n")) + for _, err := range configErrors { + fmt.Fprintf(formatter.ColorableStdErr, err.Error()) + } + os.Exit(1) + } + + err := global.Suite.BuildTree() + exitIfErr(err) + + passed, hasFocusedTests := global.Suite.Run(description, global.Failer, multiReporter, writer, nil, config.GinkgoConfig) + + flagSet.ValidateDeprecations(deprecationTracker) if deprecationTracker.DidTrackDeprecations() { - fmt.Fprintln(colorable.NewColorableStderr(), deprecationTracker.DeprecationsReport()) + fmt.Fprintln(formatter.ColorableStdErr, deprecationTracker.DeprecationsReport()) } + if !passed { + t.Fail() + } if passed && hasFocusedTests && strings.TrimSpace(os.Getenv("GINKGO_EDITOR_INTEGRATION")) == "" { fmt.Println("PASS | FOCUSED") os.Exit(types.GINKGO_FOCUS_EXIT_CODE) @@ -249,16 +225,14 @@ func runSpecsWithCustomReporters(t GinkgoTestingT, description string, specRepor } func buildDefaultReporter() Reporter { - remoteReportingServer := config.GinkgoConfig.StreamHost - if remoteReportingServer == "" { - stenographer := stenographer.New(!config.DefaultReporterConfig.NoColor, config.GinkgoConfig.FlakeAttempts > 1, colorable.NewColorableStdout()) - return reporters.NewDefaultReporter(config.DefaultReporterConfig, stenographer) + if config.GinkgoConfig.ParallelTotal == 1 { + return reporters.NewDefaultReporter(config.DefaultReporterConfig, formatter.ColorableStdOut) } else { debugFile := "" if config.GinkgoConfig.DebugParallel { debugFile = fmt.Sprintf("ginkgo-node-%d.log", config.GinkgoConfig.ParallelNode) } - return remote.NewForwardingReporter(config.DefaultReporterConfig, remoteReportingServer, &http.Client{}, remote.NewOutputInterceptor(), GinkgoWriter.(*writer.Writer), debugFile) + return parallel_support.NewForwardingReporter(config.DefaultReporterConfig, config.GinkgoConfig.ParallelHost, parallel_support.NewOutputInterceptor(), GinkgoWriter.(*internal.Writer), debugFile) } } @@ -268,9 +242,9 @@ func Skip(message string, callerSkip ...int) { if len(callerSkip) > 0 { skip = callerSkip[0] } - - global.Failer.Skip(message, codelocation.New(skip+1)) - panic(GINKGO_PANIC) + cl := types.NewCodeLocation(skip + 1) + global.Failer.Skip(message, cl) + panic(types.GinkgoErrors.UncaughtGinkgoPanic(cl)) } //Fail notifies Ginkgo that the current spec has failed. (Gomega will call Fail for you automatically when an assertion fails.) @@ -280,8 +254,9 @@ func Fail(message string, callerSkip ...int) { skip = callerSkip[0] } - global.Failer.Fail(message, codelocation.New(skip+1)) - panic(GINKGO_PANIC) + cl := types.NewCodeLocation(skip + 1) + global.Failer.Fail(message, cl) + panic(types.GinkgoErrors.UncaughtGinkgoPanic(cl)) } //GinkgoRecover should be deferred at the top of any spawned goroutine that (may) call `Fail` @@ -297,153 +272,144 @@ func Fail(message string, callerSkip ...int) { func GinkgoRecover() { e := recover() if e != nil { - global.Failer.Panic(codelocation.New(1), e) + global.Failer.Panic(types.NewCodeLocation(1), e) } } +// pushNode and pushSuiteNodeBuilder are used by the various test construction DSL methods to push nodes onto the suite +// it handles returned errors, emits a detailed error message to help the user learn what they may have done wrong, then exits +func pushNode(node internal.Node) bool { + err := global.Suite.PushNode(node) + exitIfErr(err) + return true +} + +func pushSuiteNodeBuilder(suiteNodeBuilder internal.SuiteNodeBuilder) bool { + err := global.Suite.PushSuiteNodeBuilder(suiteNodeBuilder) + exitIfErr(err) + return true +} + //Describe blocks allow you to organize your specs. A Describe block can contain any number of -//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks. +//BeforeEach, AfterEach, JustBeforeEach, and It blocks. // //In addition you can nest Describe, Context and When blocks. Describe, Context and When blocks are functionally //equivalent. The difference is purely semantic -- you typically Describe the behavior of an object //or method and, within that Describe, outline a number of Contexts and Whens. func Describe(text string, body func()) bool { - global.Suite.PushContainerNode(text, body, types.FlagTypeNone, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, text, body, types.NewCodeLocation(1), false, false)) } //You can focus the tests within a describe block using FDescribe func FDescribe(text string, body func()) bool { - global.Suite.PushContainerNode(text, body, types.FlagTypeFocused, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, text, body, types.NewCodeLocation(1), true, false)) } //You can mark the tests within a describe block as pending using PDescribe func PDescribe(text string, body func()) bool { - global.Suite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, text, body, types.NewCodeLocation(1), false, true)) } //You can mark the tests within a describe block as pending using XDescribe func XDescribe(text string, body func()) bool { - global.Suite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, text, body, types.NewCodeLocation(1), false, true)) } //Context blocks allow you to organize your specs. A Context block can contain any number of -//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks. +//BeforeEach, AfterEach, JustBeforeEach, and It blocks. // //In addition you can nest Describe, Context and When blocks. Describe, Context and When blocks are functionally //equivalent. The difference is purely semantic -- you typical Describe the behavior of an object //or method and, within that Describe, outline a number of Contexts and Whens. func Context(text string, body func()) bool { - global.Suite.PushContainerNode(text, body, types.FlagTypeNone, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, text, body, types.NewCodeLocation(1), false, false)) } //You can focus the tests within a describe block using FContext func FContext(text string, body func()) bool { - global.Suite.PushContainerNode(text, body, types.FlagTypeFocused, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, text, body, types.NewCodeLocation(1), true, false)) } //You can mark the tests within a describe block as pending using PContext func PContext(text string, body func()) bool { - global.Suite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, text, body, types.NewCodeLocation(1), false, true)) } //You can mark the tests within a describe block as pending using XContext func XContext(text string, body func()) bool { - global.Suite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, text, body, types.NewCodeLocation(1), false, true)) } //When blocks allow you to organize your specs. A When block can contain any number of -//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks. +//BeforeEach, AfterEach, JustBeforeEach, and It blocks. // //In addition you can nest Describe, Context and When blocks. Describe, Context and When blocks are functionally //equivalent. The difference is purely semantic -- you typical Describe the behavior of an object //or method and, within that Describe, outline a number of Contexts and Whens. func When(text string, body func()) bool { - global.Suite.PushContainerNode("when "+text, body, types.FlagTypeNone, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, "when "+text, body, types.NewCodeLocation(1), false, false)) } //You can focus the tests within a describe block using FWhen func FWhen(text string, body func()) bool { - global.Suite.PushContainerNode("when "+text, body, types.FlagTypeFocused, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, "when "+text, body, types.NewCodeLocation(1), true, false)) } //You can mark the tests within a describe block as pending using PWhen func PWhen(text string, body func()) bool { - global.Suite.PushContainerNode("when "+text, body, types.FlagTypePending, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, "when "+text, body, types.NewCodeLocation(1), false, true)) } //You can mark the tests within a describe block as pending using XWhen func XWhen(text string, body func()) bool { - global.Suite.PushContainerNode("when "+text, body, types.FlagTypePending, codelocation.New(1)) - return true + return pushNode(internal.NewNode(types.NodeTypeContainer, "when "+text, body, types.NewCodeLocation(1), false, true)) } //It blocks contain your test code and assertions. You cannot nest any other Ginkgo blocks //within an It block. -// -//Ginkgo will normally run It blocks synchronously. To perform asynchronous tests, pass a -//function that accepts a Done channel. When you do this, you can also provide an optional timeout. -func It(text string, body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.PushItNode(text, body, types.FlagTypeNone, codelocation.New(1), parseTimeout(timeout...)) - return true +func It(text string, body interface{}, _ ...interface{}) bool { + cl := types.NewCodeLocation(1) + return pushNode(internal.NewNode(types.NodeTypeIt, text, validateBodyFunc(body, cl), cl, false, false)) } //You can focus individual Its using FIt -func FIt(text string, body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.PushItNode(text, body, types.FlagTypeFocused, codelocation.New(1), parseTimeout(timeout...)) - return true +func FIt(text string, body interface{}, _ ...interface{}) bool { + cl := types.NewCodeLocation(1) + return pushNode(internal.NewNode(types.NodeTypeIt, text, validateBodyFunc(body, cl), cl, true, false)) } //You can mark Its as pending using PIt func PIt(text string, _ ...interface{}) bool { - global.Suite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) - return true + return pushNode(internal.NewNode(types.NodeTypeIt, text, nil, types.NewCodeLocation(1), false, true)) } //You can mark Its as pending using XIt func XIt(text string, _ ...interface{}) bool { - global.Suite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) - return true + return pushNode(internal.NewNode(types.NodeTypeIt, text, nil, types.NewCodeLocation(1), false, true)) } //Specify blocks are aliases for It blocks and allow for more natural wording in situations //which "It" does not fit into a natural sentence flow. All the same protocols apply for Specify blocks //which apply to It blocks. -func Specify(text string, body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.PushItNode(text, body, types.FlagTypeNone, codelocation.New(1), parseTimeout(timeout...)) - return true +func Specify(text string, body interface{}, _ ...interface{}) bool { + cl := types.NewCodeLocation(1) + return pushNode(internal.NewNode(types.NodeTypeIt, text, validateBodyFunc(body, cl), cl, false, false)) } //You can focus individual Specifys using FSpecify -func FSpecify(text string, body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.PushItNode(text, body, types.FlagTypeFocused, codelocation.New(1), parseTimeout(timeout...)) - return true +func FSpecify(text string, body interface{}, _ ...interface{}) bool { + cl := types.NewCodeLocation(1) + return pushNode(internal.NewNode(types.NodeTypeIt, text, validateBodyFunc(body, cl), cl, true, false)) } //You can mark Specifys as pending using PSpecify -func PSpecify(text string, is ...interface{}) bool { - global.Suite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) - return true +func PSpecify(text string, _ ...interface{}) bool { + return pushNode(internal.NewNode(types.NodeTypeIt, text, nil, types.NewCodeLocation(1), false, true)) } //You can mark Specifys as pending using XSpecify -func XSpecify(text string, is ...interface{}) bool { - global.Suite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0) - return true +func XSpecify(text string, _ ...interface{}) bool { + return pushNode(internal.NewNode(types.NodeTypeIt, text, nil, types.NewCodeLocation(1), false, true)) } //By allows you to better document large Its. @@ -451,7 +417,7 @@ func XSpecify(text string, is ...interface{}) bool { //Generally you should try to keep your Its short and to the point. This is not always possible, however, //especially in the context of integration tests that capture a particular workflow. // -//By allows you to document such flows. By must be called within a runnable node (It, BeforeEach, Measure, etc...) +//By allows you to document such flows. By must be called within a runnable node (It, BeforeEach, etc...) //By will simply log the passed in text to the GinkgoWriter. If By is handed a function it will immediately run the function. func By(text string, callbacks ...func()) { preamble := "\x1b[1mSTEP\x1b[0m" @@ -467,44 +433,16 @@ func By(text string, callbacks ...func()) { } } -//Measure blocks run the passed in body function repeatedly (determined by the samples argument) -//and accumulate metrics provided to the Benchmarker by the body function. -// -//The body function must have the signature: -// func(b Benchmarker) -func Measure(text string, body interface{}, samples int) bool { - global.Suite.PushMeasureNode(text, body, types.FlagTypeNone, codelocation.New(1), samples) - return true -} - -//You can focus individual Measures using FMeasure -func FMeasure(text string, body interface{}, samples int) bool { - global.Suite.PushMeasureNode(text, body, types.FlagTypeFocused, codelocation.New(1), samples) - return true -} - -//You can mark Measurements as pending using PMeasure -func PMeasure(text string, _ ...interface{}) bool { - global.Suite.PushMeasureNode(text, func(b Benchmarker) {}, types.FlagTypePending, codelocation.New(1), 0) - return true -} - -//You can mark Measurements as pending using XMeasure -func XMeasure(text string, _ ...interface{}) bool { - global.Suite.PushMeasureNode(text, func(b Benchmarker) {}, types.FlagTypePending, codelocation.New(1), 0) - return true -} - //BeforeSuite blocks are run just once before any specs are run. When running in parallel, each //parallel node process will call BeforeSuite. // -//BeforeSuite blocks can be made asynchronous by providing a body function that accepts a Done channel -// //You may only register *one* BeforeSuite handler per test suite. You typically do so in your bootstrap file at the top level. -func BeforeSuite(body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.SetBeforeSuiteNode(body, codelocation.New(1), parseTimeout(timeout...)) - return true +func BeforeSuite(body func()) bool { + return pushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeBeforeSuite, + CodeLocation: types.NewCodeLocation(1), + BeforeSuiteBody: body, + }) } //AfterSuite blocks are *always* run after all the specs regardless of whether specs have passed or failed. @@ -512,13 +450,13 @@ func BeforeSuite(body interface{}, timeout ...float64) bool { // //When running in parallel, each parallel node process will call AfterSuite. // -//AfterSuite blocks can be made asynchronous by providing a body function that accepts a Done channel -// //You may only register *one* AfterSuite handler per test suite. You typically do so in your bootstrap file at the top level. -func AfterSuite(body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.SetAfterSuiteNode(body, codelocation.New(1), parseTimeout(timeout...)) - return true +func AfterSuite(body func()) bool { + return pushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeAfterSuite, + CodeLocation: types.NewCodeLocation(1), + AfterSuiteBody: body, + }) } //SynchronizedBeforeSuite blocks are primarily meant to solve the problem of setting up singleton external resources shared across @@ -534,18 +472,10 @@ func AfterSuite(body interface{}, timeout ...float64) bool { // // func() []byte // -//or, to run asynchronously: -// -// func(done Done) []byte -// //The byte array returned by the first function is then passed to the second function, which has the signature: // // func(data []byte) // -//or, to run asynchronously: -// -// func(data []byte, done Done) -// //Here's a simple pseudo-code example that starts a shared database on Node 1 and shares the database's address with the other nodes: // // var dbClient db.Client @@ -561,14 +491,13 @@ func AfterSuite(body interface{}, timeout ...float64) bool { // err := dbClient.Connect(string(data)) // Ω(err).ShouldNot(HaveOccurred()) // }) -func SynchronizedBeforeSuite(node1Body interface{}, allNodesBody interface{}, timeout ...float64) bool { - global.Suite.SetSynchronizedBeforeSuiteNode( - node1Body, - allNodesBody, - codelocation.New(1), - parseTimeout(timeout...), - ) - return true +func SynchronizedBeforeSuite(node1Body func() []byte, allNodesBody func([]byte)) bool { + return pushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeSynchronizedBeforeSuite, + CodeLocation: types.NewCodeLocation(1), + SynchronizedBeforeSuiteNode1Body: node1Body, + SynchronizedBeforeSuiteAllNodesBody: allNodesBody, + }) } //SynchronizedAfterSuite blocks complement the SynchronizedBeforeSuite blocks in solving the problem of setting up @@ -578,8 +507,6 @@ func SynchronizedBeforeSuite(node1Body interface{}, allNodesBody interface{}, ti //and *only* after all other nodes have finished and exited. This ensures that node 1, and any resources it is running, remain alive until //all other nodes are finished. // -//Both functions have the same signature: either func() or func(done Done) to run asynchronously. -// //Here's a pseudo-code example that complements that given in SynchronizedBeforeSuite. Here, SynchronizedAfterSuite is used to tear down the shared database //only after all nodes have finished: // @@ -588,83 +515,93 @@ func SynchronizedBeforeSuite(node1Body interface{}, allNodesBody interface{}, ti // }, func() { // dbRunner.Stop() // }) -func SynchronizedAfterSuite(allNodesBody interface{}, node1Body interface{}, timeout ...float64) bool { - global.Suite.SetSynchronizedAfterSuiteNode( - allNodesBody, - node1Body, - codelocation.New(1), - parseTimeout(timeout...), - ) - return true +func SynchronizedAfterSuite(allNodesBody func(), node1Body func()) bool { + return pushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeSynchronizedAfterSuite, + CodeLocation: types.NewCodeLocation(1), + SynchronizedAfterSuiteAllNodesBody: allNodesBody, + SynchronizedAfterSuiteNode1Body: node1Body, + }) } //BeforeEach blocks are run before It blocks. When multiple BeforeEach blocks are defined in nested //Describe and Context blocks the outermost BeforeEach blocks are run first. -// -//Like It blocks, BeforeEach blocks can be made asynchronous by providing a body function that accepts -//a Done channel -func BeforeEach(body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.PushBeforeEachNode(body, codelocation.New(1), parseTimeout(timeout...)) - return true +func BeforeEach(body interface{}, _ ...interface{}) bool { + cl := types.NewCodeLocation(1) + return pushNode(internal.NewNode(types.NodeTypeBeforeEach, "", validateBodyFunc(body, cl), cl, false, false)) } //JustBeforeEach blocks are run before It blocks but *after* all BeforeEach blocks. For more details, //read the [documentation](http://onsi.github.io/ginkgo/#separating_creation_and_configuration_) -// -//Like It blocks, BeforeEach blocks can be made asynchronous by providing a body function that accepts -//a Done channel -func JustBeforeEach(body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.PushJustBeforeEachNode(body, codelocation.New(1), parseTimeout(timeout...)) - return true +func JustBeforeEach(body interface{}, _ ...interface{}) bool { + cl := types.NewCodeLocation(1) + return pushNode(internal.NewNode(types.NodeTypeJustBeforeEach, "", validateBodyFunc(body, cl), cl, false, false)) } //JustAfterEach blocks are run after It blocks but *before* all AfterEach blocks. For more details, //read the [documentation](http://onsi.github.io/ginkgo/#separating_creation_and_configuration_) -// -//Like It blocks, JustAfterEach blocks can be made asynchronous by providing a body function that accepts -//a Done channel -func JustAfterEach(body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.PushJustAfterEachNode(body, codelocation.New(1), parseTimeout(timeout...)) - return true +func JustAfterEach(body interface{}, _ ...interface{}) bool { + cl := types.NewCodeLocation(1) + return pushNode(internal.NewNode(types.NodeTypeJustAfterEach, "", validateBodyFunc(body, cl), cl, false, false)) } //AfterEach blocks are run after It blocks. When multiple AfterEach blocks are defined in nested //Describe and Context blocks the innermost AfterEach blocks are run first. -// -//Like It blocks, AfterEach blocks can be made asynchronous by providing a body function that accepts -//a Done channel -func AfterEach(body interface{}, timeout ...float64) bool { - validateBodyFunc(body, codelocation.New(1)) - global.Suite.PushAfterEachNode(body, codelocation.New(1), parseTimeout(timeout...)) - return true +func AfterEach(body interface{}, _ ...interface{}) bool { + cl := types.NewCodeLocation(1) + return pushNode(internal.NewNode(types.NodeTypeAfterEach, "", validateBodyFunc(body, cl), cl, false, false)) } -func validateBodyFunc(body interface{}, cl types.CodeLocation) { +func exitIfErr(err error) { + if err != nil { + fmt.Fprintln(formatter.ColorableStdErr, err.Error()) + os.Exit(1) + } +} + +// Deprecations for v2 + +// Deprecated Done Channel for asynchronous testing +type Done chan<- interface{} + +func validateBodyFunc(body interface{}, cl types.CodeLocation) func() { t := reflect.TypeOf(body) if t.Kind() != reflect.Func { - return + exitIfErr(types.GinkgoErrors.InvalidBodyType(t, cl)) } if t.NumOut() > 0 { - return + exitIfErr(types.GinkgoErrors.InvalidBodyType(t, cl)) } if t.NumIn() == 0 { - return + return body.(func()) } - if t.In(0) == reflect.TypeOf(make(Done)) { - deprecationTracker.TrackDeprecation(types.Deprecations.Async(), cl) + if t.NumIn() > 1 { + exitIfErr(types.GinkgoErrors.InvalidBodyType(t, cl)) } -} -func parseTimeout(timeout ...float64) time.Duration { - if len(timeout) == 0 { - return global.DefaultTimeout - } else { - return time.Duration(timeout[0] * float64(time.Second)) + if t.In(0) != reflect.TypeOf(make(Done)) { + exitIfErr(types.GinkgoErrors.InvalidBodyType(t, cl)) + } + + deprecationTracker.TrackDeprecation(types.Deprecations.Async(), cl) + + return func() { + body.(func(Done))(make(Done)) } } + +//deprecated benchmarker +type Benchmarker interface { + Time(name string, body func(), info ...interface{}) (elapsedTime time.Duration) + RecordValue(name string, value float64, info ...interface{}) + RecordValueWithPrecision(name string, value float64, units string, precision int, info ...interface{}) +} + +//deprecated Measure +func Measure(_ ...interface{}) bool { + deprecationTracker.TrackDeprecation(types.Deprecations.Measure(), types.NewCodeLocation(1)) + return true +} diff --git a/go.mod b/go.mod index 664372fb66..b3639bb1b9 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.15 require ( github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 github.com/nxadm/tail v1.4.8 - github.com/onsi/gomega v1.10.1 + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/onsi/gomega v1.11.1-0.20210307213111-c3c09204ab54 golang.org/x/sys v0.0.0-20210112080510-489259a85091 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e ) diff --git a/go.sum b/go.sum index 5c5c3c5020..e34d430b0c 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -12,25 +10,21 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/onsi/gomega v1.11.1-0.20210307213111-c3c09204ab54 h1:zUTFTBG8ENde3IS8LpdoIOkxjvaaJdcNjpiiqms09u8= +github.com/onsi/gomega v1.11.1-0.20210307213111-c3c09204ab54/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -40,9 +34,9 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -52,7 +46,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -66,7 +59,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -75,12 +67,10 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/integration/_fixtures/suite_command_tests/suite_command.go b/integration/_fixtures/after_run_hook_fixture/after_run_hook.go similarity index 100% rename from integration/_fixtures/suite_command_tests/suite_command.go rename to integration/_fixtures/after_run_hook_fixture/after_run_hook.go diff --git a/integration/_fixtures/suite_command_tests/suite_command_suite_test.go b/integration/_fixtures/after_run_hook_fixture/after_run_hook_suite_test.go similarity index 64% rename from integration/_fixtures/suite_command_tests/suite_command_suite_test.go rename to integration/_fixtures/after_run_hook_fixture/after_run_hook_suite_test.go index 7f76d8b8fa..1234ac7302 100644 --- a/integration/_fixtures/suite_command_tests/suite_command_suite_test.go +++ b/integration/_fixtures/after_run_hook_fixture/after_run_hook_suite_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -func TestSuiteCommand(t *testing.T) { +func TestAfterRunHook(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Suite Command Suite") + RunSpecs(t, "After Run Hook Suite") } diff --git a/integration/_fixtures/suite_command_tests/suite_command_test.go b/integration/_fixtures/after_run_hook_fixture/after_run_hook_test.go similarity index 84% rename from integration/_fixtures/suite_command_tests/suite_command_test.go rename to integration/_fixtures/after_run_hook_fixture/after_run_hook_test.go index e083d27a24..9dcf02da7e 100644 --- a/integration/_fixtures/suite_command_tests/suite_command_test.go +++ b/integration/_fixtures/after_run_hook_fixture/after_run_hook_test.go @@ -5,7 +5,7 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Testing suite command", func() { +var _ = Describe("Testing", func() { It("it should succeed", func() { Ω(true).Should(Equal(true)) }) diff --git a/integration/_fixtures/combined_coverage_fixture/first_package/coverage_fixture_test.go b/integration/_fixtures/combined_coverage_fixture/first_package/coverage_fixture_test.go index dfe3e1127c..114fb8873b 100644 --- a/integration/_fixtures/combined_coverage_fixture/first_package/coverage_fixture_test.go +++ b/integration/_fixtures/combined_coverage_fixture/first_package/coverage_fixture_test.go @@ -2,7 +2,7 @@ package first_package_test import ( . "github.com/onsi/ginkgo/integration/_fixtures/combined_coverage_fixture/first_package" - . "github.com/onsi/ginkgo/integration/_fixtures/combined_coverage_fixture/first_package/external_coverage_fixture" + . "github.com/onsi/ginkgo/integration/_fixtures/combined_coverage_fixture/first_package/external_coverage" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff --git a/integration/_fixtures/combined_coverage_fixture/first_package/external_coverage_fixture/external_coverage.go b/integration/_fixtures/combined_coverage_fixture/first_package/external_coverage/external_coverage.go similarity index 100% rename from integration/_fixtures/combined_coverage_fixture/first_package/external_coverage_fixture/external_coverage.go rename to integration/_fixtures/combined_coverage_fixture/first_package/external_coverage/external_coverage.go diff --git a/integration/_fixtures/convert_fixtures/extra_functions_test.go b/integration/_fixtures/convert_fixtures/extra_functions_test.go deleted file mode 100644 index ccb3669a5a..0000000000 --- a/integration/_fixtures/convert_fixtures/extra_functions_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package tmp - -import ( - "testing" -) - -func TestSomethingLessImportant(t *testing.T) { - strp := "hello!" - somethingImportant(t, &strp) -} - -func somethingImportant(t *testing.T, message *string) { - t.Log("Something important happened in a test: " + *message) -} diff --git a/integration/_fixtures/convert_fixtures/nested/nested_test.go b/integration/_fixtures/convert_fixtures/nested/nested_test.go deleted file mode 100644 index cde42e4708..0000000000 --- a/integration/_fixtures/convert_fixtures/nested/nested_test.go +++ /dev/null @@ -1,10 +0,0 @@ -package nested - -import ( - "testing" -) - -func TestSomethingLessImportant(t *testing.T) { - whatever := &UselessStruct{} - t.Fail(whatever.ImportantField != "SECRET_PASSWORD") -} diff --git a/integration/_fixtures/convert_fixtures/nested_without_gofiles/subpackage/nested_subpackage_test.go b/integration/_fixtures/convert_fixtures/nested_without_gofiles/subpackage/nested_subpackage_test.go deleted file mode 100644 index 7cdd326c5b..0000000000 --- a/integration/_fixtures/convert_fixtures/nested_without_gofiles/subpackage/nested_subpackage_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package subpackage - -import ( - "testing" -) - -func TestNestedSubPackages(t *testing.T) { - t.Fail(true) -} diff --git a/integration/_fixtures/convert_fixtures/outside_package_test.go b/integration/_fixtures/convert_fixtures/outside_package_test.go deleted file mode 100644 index a682eeaffc..0000000000 --- a/integration/_fixtures/convert_fixtures/outside_package_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package tmp_test - -import ( - "testing" -) - -type UselessStruct struct { - ImportantField string -} - -func TestSomethingImportant(t *testing.T) { - whatever := &UselessStruct{} - if whatever.ImportantField != "SECRET_PASSWORD" { - t.Fail() - } -} diff --git a/integration/_fixtures/convert_fixtures/xunit_test.go b/integration/_fixtures/convert_fixtures/xunit_test.go deleted file mode 100644 index 049829a7df..0000000000 --- a/integration/_fixtures/convert_fixtures/xunit_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package tmp - -import ( - "testing" -) - -type UselessStruct struct { - ImportantField string - T *testing.T -} - -var testFunc = func(t *testing.T, arg *string) {} - -func assertEqual(t *testing.T, arg1, arg2 interface{}) { - if arg1 != arg2 { - t.Fail() - } -} - -func TestSomethingImportant(t *testing.T) { - whatever := &UselessStruct{ - T: t, - ImportantField: "SECRET_PASSWORD", - } - something := &UselessStruct{ImportantField: "string value"} - assertEqual(t, whatever.ImportantField, "SECRET_PASSWORD") - assertEqual(t, something.ImportantField, "string value") - - var foo = func(t *testing.T) {} - foo(t) - - strp := "something" - testFunc(t, &strp) - t.Fail() -} - -func Test3Things(t *testing.T) { - if 3 != 3 { - t.Fail() - } -} diff --git a/integration/_fixtures/convert_goldmasters/extra_functions_test.go b/integration/_fixtures/convert_goldmasters/extra_functions_test.go deleted file mode 100644 index 1c2c56cea2..0000000000 --- a/integration/_fixtures/convert_goldmasters/extra_functions_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package tmp - -import ( - . "github.com/onsi/ginkgo" -) - -var _ = Describe("Testing with Ginkgo", func() { - It("something less important", func() { - - strp := "hello!" - somethingImportant(GinkgoT(), &strp) - }) -}) - -func somethingImportant(t GinkgoTInterface, message *string) { - t.Log("Something important happened in a test: " + *message) -} diff --git a/integration/_fixtures/convert_goldmasters/fixtures_suite_test.go b/integration/_fixtures/convert_goldmasters/fixtures_suite_test.go deleted file mode 100644 index a9a404b5c2..0000000000 --- a/integration/_fixtures/convert_goldmasters/fixtures_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package tmp - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestTmp(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Tmp Suite") -} diff --git a/integration/_fixtures/convert_goldmasters/nested_subpackage_test.go b/integration/_fixtures/convert_goldmasters/nested_subpackage_test.go deleted file mode 100644 index 3653eae82b..0000000000 --- a/integration/_fixtures/convert_goldmasters/nested_subpackage_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package subpackage - -import ( - . "github.com/onsi/ginkgo" -) - -var _ = Describe("Testing with Ginkgo", func() { - It("nested sub packages", func() { - GinkgoT().Fail(true) - }) -}) diff --git a/integration/_fixtures/convert_goldmasters/nested_suite_test.go b/integration/_fixtures/convert_goldmasters/nested_suite_test.go deleted file mode 100644 index 721d0f2c37..0000000000 --- a/integration/_fixtures/convert_goldmasters/nested_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package nested_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestNested(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Nested Suite") -} diff --git a/integration/_fixtures/convert_goldmasters/nested_test.go b/integration/_fixtures/convert_goldmasters/nested_test.go deleted file mode 100644 index 47364b8146..0000000000 --- a/integration/_fixtures/convert_goldmasters/nested_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package nested - -import ( - . "github.com/onsi/ginkgo" -) - -var _ = Describe("Testing with Ginkgo", func() { - It("something less important", func() { - - whatever := &UselessStruct{} - GinkgoT().Fail(whatever.ImportantField != "SECRET_PASSWORD") - }) -}) diff --git a/integration/_fixtures/convert_goldmasters/outside_package_test.go b/integration/_fixtures/convert_goldmasters/outside_package_test.go deleted file mode 100644 index 1f2e332c44..0000000000 --- a/integration/_fixtures/convert_goldmasters/outside_package_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package tmp_test - -import ( - . "github.com/onsi/ginkgo" -) - -var _ = Describe("Testing with Ginkgo", func() { - It("something important", func() { - - whatever := &UselessStruct{} - if whatever.ImportantField != "SECRET_PASSWORD" { - GinkgoT().Fail() - } - }) -}) - -type UselessStruct struct { - ImportantField string -} diff --git a/integration/_fixtures/convert_goldmasters/suite_test.go b/integration/_fixtures/convert_goldmasters/suite_test.go deleted file mode 100644 index 9ea2291351..0000000000 --- a/integration/_fixtures/convert_goldmasters/suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package tmp_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestConvertFixtures(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "ConvertFixtures Suite") -} diff --git a/integration/_fixtures/convert_goldmasters/xunit_test.go b/integration/_fixtures/convert_goldmasters/xunit_test.go deleted file mode 100644 index dbe3b419d1..0000000000 --- a/integration/_fixtures/convert_goldmasters/xunit_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package tmp - -import ( - . "github.com/onsi/ginkgo" -) - -var _ = Describe("Testing with Ginkgo", func() { - It("something important", func() { - - whatever := &UselessStruct{ - T: GinkgoT(), - ImportantField: "SECRET_PASSWORD", - } - something := &UselessStruct{ImportantField: "string value"} - assertEqual(GinkgoT(), whatever.ImportantField, "SECRET_PASSWORD") - assertEqual(GinkgoT(), something.ImportantField, "string value") - - var foo = func(t GinkgoTInterface) {} - foo(GinkgoT()) - - strp := "something" - testFunc(GinkgoT(), &strp) - GinkgoT().Fail() - }) - It("3 things", func() { - - if 3 != 3 { - GinkgoT().Fail() - } - }) -}) - -type UselessStruct struct { - ImportantField string - T GinkgoTInterface -} - -var testFunc = func(t GinkgoTInterface, arg *string) {} - -func assertEqual(t GinkgoTInterface, arg1, arg2 interface{}) { - if arg1 != arg2 { - t.Fail() - } -} diff --git a/integration/_fixtures/coverage_fixture/coverage.go b/integration/_fixtures/coverage_fixture/coverage.go index e4d7e43b1d..01cfd6fb0e 100644 --- a/integration/_fixtures/coverage_fixture/coverage.go +++ b/integration/_fixtures/coverage_fixture/coverage.go @@ -1,7 +1,7 @@ package coverage_fixture import ( - _ "github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture/external_coverage_fixture" + _ "github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture/external_coverage" ) func A() string { diff --git a/integration/_fixtures/coverage_fixture/coverage_fixture_test.go b/integration/_fixtures/coverage_fixture/coverage_fixture_test.go index 12a72dce81..c4b133047f 100644 --- a/integration/_fixtures/coverage_fixture/coverage_fixture_test.go +++ b/integration/_fixtures/coverage_fixture/coverage_fixture_test.go @@ -2,7 +2,7 @@ package coverage_fixture_test import ( . "github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture" - . "github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture/external_coverage_fixture" + . "github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture/external_coverage" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff --git a/integration/_fixtures/coverage_fixture/external_coverage_fixture/external_coverage.go b/integration/_fixtures/coverage_fixture/external_coverage/external_coverage.go similarity index 100% rename from integration/_fixtures/coverage_fixture/external_coverage_fixture/external_coverage.go rename to integration/_fixtures/coverage_fixture/external_coverage/external_coverage.go diff --git a/integration/_fixtures/deprecated_features_fixture/deprecated_features_fixture_suite_test.go b/integration/_fixtures/deprecated_features_fixture/deprecated_features_fixture_suite_test.go new file mode 100644 index 0000000000..00ce555ec1 --- /dev/null +++ b/integration/_fixtures/deprecated_features_fixture/deprecated_features_fixture_suite_test.go @@ -0,0 +1,21 @@ +package deprecated_features_fixture_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestDeprecatedFeaturesFixture(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "DeprecatedFeaturesFixture Suite") +} + +var _ = It("tries to perform an async assertion", func(done Done) { + close(done) +}) + +var _ = Measure("tries to perform a measurement", func(b Benchmarker) { + +}) \ No newline at end of file diff --git a/integration/_fixtures/does_not_compile/does_not_compile_suite_test.go b/integration/_fixtures/does_not_compile_fixture/does_not_compile_suite_test.go similarity index 100% rename from integration/_fixtures/does_not_compile/does_not_compile_suite_test.go rename to integration/_fixtures/does_not_compile_fixture/does_not_compile_suite_test.go diff --git a/integration/_fixtures/does_not_compile/does_not_compile_test.go b/integration/_fixtures/does_not_compile_fixture/does_not_compile_test.go similarity index 100% rename from integration/_fixtures/does_not_compile/does_not_compile_test.go rename to integration/_fixtures/does_not_compile_fixture/does_not_compile_test.go diff --git a/integration/_fixtures/eventually_failing/eventually_failing_suite_test.go b/integration/_fixtures/eventually_failing_fixture/eventually_failing_suite_test.go similarity index 100% rename from integration/_fixtures/eventually_failing/eventually_failing_suite_test.go rename to integration/_fixtures/eventually_failing_fixture/eventually_failing_suite_test.go diff --git a/integration/_fixtures/eventually_failing/eventually_failing_test.go b/integration/_fixtures/eventually_failing_fixture/eventually_failing_test.go similarity index 100% rename from integration/_fixtures/eventually_failing/eventually_failing_test.go rename to integration/_fixtures/eventually_failing_fixture/eventually_failing_test.go diff --git a/integration/_fixtures/exiting_synchronized_setup_tests/exiting_synchronized_setup_tests_suite_test.go b/integration/_fixtures/exiting_synchronized_setup_fixture/exiting_synchronized_setup_suite_test.go similarity index 86% rename from integration/_fixtures/exiting_synchronized_setup_tests/exiting_synchronized_setup_tests_suite_test.go rename to integration/_fixtures/exiting_synchronized_setup_fixture/exiting_synchronized_setup_suite_test.go index 045ca7c667..1d6c68a371 100644 --- a/integration/_fixtures/exiting_synchronized_setup_tests/exiting_synchronized_setup_tests_suite_test.go +++ b/integration/_fixtures/exiting_synchronized_setup_fixture/exiting_synchronized_setup_suite_test.go @@ -1,4 +1,4 @@ -package synchronized_setup_tests_test +package synchronized_setup_test import ( . "github.com/onsi/ginkgo" @@ -9,7 +9,7 @@ import ( "testing" ) -func TestSynchronized_setup_tests(t *testing.T) { +func TestSynchronized_setup(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Synchronized_setup_tests Suite") } diff --git a/integration/_fixtures/fail_fixture/fail_fixture_test.go b/integration/_fixtures/fail_fixture/fail_fixture_test.go index d773edaecc..b0515a27ce 100644 --- a/integration/_fixtures/fail_fixture/fail_fixture_test.go +++ b/integration/_fixtures/fail_fixture/fail_fixture_test.go @@ -11,19 +11,6 @@ var _ = It("handles top level failures", func() { println("NEVER SEE THIS") }) -var _ = It("handles async top level failures", func(done Done) { - Fail("an async top level failure on line 15") - println("NEVER SEE THIS") -}, 0.1) - -var _ = It("FAIL in a goroutine", func(done Done) { - go func() { - defer GinkgoRecover() - Fail("a top level goroutine failure on line 22") - println("NEVER SEE THIS") - }() -}, 0.1) - var _ = Describe("Excercising different failure modes", func() { It("synchronous failures", func() { Ω("a sync failure").Should(Equal("nope")) @@ -39,64 +26,6 @@ var _ = Describe("Excercising different failure modes", func() { Fail("a sync FAIL failure") println("NEVER SEE THIS") }) - - It("async timeout", func(done Done) { - Ω(true).Should(BeTrue()) - }, 0.1) - - It("async failure", func(done Done) { - Ω("an async failure").Should(Equal("nope")) - println("NEVER SEE THIS") - }, 0.1) - - It("async panic", func(done Done) { - panic("an async panic") - println("NEVER SEE THIS") - }, 0.1) - - It("async failure with FAIL", func(done Done) { - Fail("an async FAIL failure") - println("NEVER SEE THIS") - }, 0.1) - - It("FAIL in a goroutine", func(done Done) { - go func() { - defer GinkgoRecover() - Fail("a goroutine FAIL failure") - println("NEVER SEE THIS") - }() - }, 0.1) - - It("Gomega in a goroutine", func(done Done) { - go func() { - defer GinkgoRecover() - Ω("a goroutine failure").Should(Equal("nope")) - println("NEVER SEE THIS") - }() - }, 0.1) - - It("Panic in a goroutine", func(done Done) { - go func() { - defer GinkgoRecover() - panic("a goroutine panic") - println("NEVER SEE THIS") - }() - }, 0.1) - - Measure("a FAIL measure", func(Benchmarker) { - Fail("a measure FAIL failure") - println("NEVER SEE THIS") - }, 1) - - Measure("a gomega failed measure", func(Benchmarker) { - Ω("a measure failure").Should(Equal("nope")) - println("NEVER SEE THIS") - }, 1) - - Measure("a panicking measure", func(Benchmarker) { - panic("a measure panic") - println("NEVER SEE THIS") - }, 1) }) var _ = Specify("a top level specify", func() { diff --git a/integration/_fixtures/failing_after_suite/failing_after_suite_suite_test.go b/integration/_fixtures/failing_after_suite/failing_after_suite_suite_test.go deleted file mode 100644 index 0e410aaeab..0000000000 --- a/integration/_fixtures/failing_after_suite/failing_after_suite_suite_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package failing_before_suite_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestFailingAfterSuite(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "FailingAfterSuite Suite") -} - -var _ = BeforeSuite(func() { - println("BEFORE SUITE") -}) - -var _ = AfterSuite(func() { - println("AFTER SUITE") - panic("BAM!") -}) diff --git a/integration/_fixtures/failing_after_suite/failing_after_suite_test.go b/integration/_fixtures/failing_after_suite/failing_after_suite_test.go deleted file mode 100644 index 3902ec6c58..0000000000 --- a/integration/_fixtures/failing_after_suite/failing_after_suite_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package failing_before_suite_test - -import ( - . "github.com/onsi/ginkgo" -) - -var _ = Describe("FailingBeforeSuite", func() { - It("should run", func() { - println("A TEST") - }) - - It("should run", func() { - println("A TEST") - }) -}) diff --git a/integration/_fixtures/failing_before_suite/failing_before_suite_suite_test.go b/integration/_fixtures/failing_before_suite/failing_before_suite_suite_test.go deleted file mode 100644 index 109ea36080..0000000000 --- a/integration/_fixtures/failing_before_suite/failing_before_suite_suite_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package failing_before_suite_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestFailing_before_suite(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Failing_before_suite Suite") -} - -var _ = BeforeSuite(func() { - println("BEFORE SUITE") - panic("BAM!") -}) - -var _ = AfterSuite(func() { - println("AFTER SUITE") -}) diff --git a/integration/_fixtures/failing_before_suite/failing_before_suite_test.go b/integration/_fixtures/failing_before_suite/failing_before_suite_test.go deleted file mode 100644 index e8697c64ae..0000000000 --- a/integration/_fixtures/failing_before_suite/failing_before_suite_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package failing_before_suite_test - -import ( - . "github.com/onsi/ginkgo" -) - -var _ = Describe("FailingBeforeSuite", func() { - It("should never run", func() { - println("NEVER SEE THIS") - }) - - It("should never run", func() { - println("NEVER SEE THIS") - }) -}) diff --git a/integration/_fixtures/failing_ginkgo_tests/failing_ginkgo_tests.go b/integration/_fixtures/failing_ginkgo_tests_fixture/failing_ginkgo_tests.go similarity index 100% rename from integration/_fixtures/failing_ginkgo_tests/failing_ginkgo_tests.go rename to integration/_fixtures/failing_ginkgo_tests_fixture/failing_ginkgo_tests.go diff --git a/integration/_fixtures/failing_ginkgo_tests/failing_ginkgo_tests_suite_test.go b/integration/_fixtures/failing_ginkgo_tests_fixture/failing_ginkgo_tests_suite_test.go similarity index 100% rename from integration/_fixtures/failing_ginkgo_tests/failing_ginkgo_tests_suite_test.go rename to integration/_fixtures/failing_ginkgo_tests_fixture/failing_ginkgo_tests_suite_test.go diff --git a/integration/_fixtures/failing_ginkgo_tests/failing_ginkgo_tests_test.go b/integration/_fixtures/failing_ginkgo_tests_fixture/failing_ginkgo_tests_test.go similarity index 100% rename from integration/_fixtures/failing_ginkgo_tests/failing_ginkgo_tests_test.go rename to integration/_fixtures/failing_ginkgo_tests_fixture/failing_ginkgo_tests_test.go diff --git a/integration/_fixtures/flags_tests/flags.go b/integration/_fixtures/flags_fixture/flags.go similarity index 100% rename from integration/_fixtures/flags_tests/flags.go rename to integration/_fixtures/flags_fixture/flags.go diff --git a/integration/_fixtures/flags_tests/flags_suite_test.go b/integration/_fixtures/flags_fixture/flags_suite_test.go similarity index 100% rename from integration/_fixtures/flags_tests/flags_suite_test.go rename to integration/_fixtures/flags_fixture/flags_suite_test.go diff --git a/integration/_fixtures/flags_tests/flags_test.go b/integration/_fixtures/flags_fixture/flags_test.go similarity index 86% rename from integration/_fixtures/flags_tests/flags_test.go rename to integration/_fixtures/flags_fixture/flags_test.go index 27dadf19c8..bc88798a42 100644 --- a/integration/_fixtures/flags_tests/flags_test.go +++ b/integration/_fixtures/flags_fixture/flags_test.go @@ -8,7 +8,7 @@ import ( "time" . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/integration/_fixtures/flags_tests" + . "github.com/onsi/ginkgo/integration/_fixtures/flags_fixture" . "github.com/onsi/gomega" ) @@ -20,10 +20,6 @@ func init() { var _ = Describe("Testing various flags", func() { FDescribe("the focused set", func() { - Measure("a measurement", func(b Benchmarker) { - b.RecordValue("a value", 3) - }, 3) - It("should honor -cover", func() { Ω(Tested()).Should(Equal("tested")) }) @@ -44,14 +40,16 @@ var _ = Describe("Testing various flags", func() { }) }) - It("should detect races", func(done Done) { + It("should detect races", func() { var a string + c := make(chan interface{}, 0) go func() { a = "now you don't" - close(done) + close(c) }() a = "now you see me" println(a) + Eventually(c).Should(BeClosed()) }) It("should randomize A", func() { @@ -66,7 +64,7 @@ var _ = Describe("Testing various flags", func() { println("RANDOM_C") }) - It("should honor -slowSpecThreshold", func() { + It("should honor -slow-spec-threshold", func() { time.Sleep(100 * time.Millisecond) }) diff --git a/integration/_fixtures/focused_fixture/focused_fixture_test.go b/integration/_fixtures/focused_fixture/focused_fixture_test.go index ea500eaf04..3c2cd063a8 100644 --- a/integration/_fixtures/focused_fixture/focused_fixture_test.go +++ b/integration/_fixtures/focused_fixture/focused_fixture_test.go @@ -32,10 +32,6 @@ var _ = Describe("FocusedFixture", func() { }) - FMeasure("focused", func(b Benchmarker) { - - }, 2) - FDescribeTable("focused", func() {}, Entry("focused"), @@ -62,10 +58,6 @@ var _ = Describe("FocusedFixture", func() { }) - Measure("not focused", func(b Benchmarker) { - - }, 2) - DescribeTable("not focused", func() {}, Entry("not focused"), diff --git a/integration/_fixtures/focused_fixture/internal/focused_fixture_test.go b/integration/_fixtures/focused_fixture/internal/focused_fixture_test.go index ea500eaf04..3c2cd063a8 100644 --- a/integration/_fixtures/focused_fixture/internal/focused_fixture_test.go +++ b/integration/_fixtures/focused_fixture/internal/focused_fixture_test.go @@ -32,10 +32,6 @@ var _ = Describe("FocusedFixture", func() { }) - FMeasure("focused", func(b Benchmarker) { - - }, 2) - FDescribeTable("focused", func() {}, Entry("focused"), @@ -62,10 +58,6 @@ var _ = Describe("FocusedFixture", func() { }) - Measure("not focused", func(b Benchmarker) { - - }, 2) - DescribeTable("not focused", func() {}, Entry("not focused"), diff --git a/integration/_fixtures/focused_fixture_with_vendor/focused_fixture_suite_test.go b/integration/_fixtures/focused_with_vendor_fixture/focused_fixture_suite_test.go similarity index 100% rename from integration/_fixtures/focused_fixture_with_vendor/focused_fixture_suite_test.go rename to integration/_fixtures/focused_with_vendor_fixture/focused_fixture_suite_test.go diff --git a/integration/_fixtures/focused_fixture_with_vendor/focused_fixture_test.go b/integration/_fixtures/focused_with_vendor_fixture/focused_fixture_test.go similarity index 88% rename from integration/_fixtures/focused_fixture_with_vendor/focused_fixture_test.go rename to integration/_fixtures/focused_with_vendor_fixture/focused_fixture_test.go index ea500eaf04..3c2cd063a8 100644 --- a/integration/_fixtures/focused_fixture_with_vendor/focused_fixture_test.go +++ b/integration/_fixtures/focused_with_vendor_fixture/focused_fixture_test.go @@ -32,10 +32,6 @@ var _ = Describe("FocusedFixture", func() { }) - FMeasure("focused", func(b Benchmarker) { - - }, 2) - FDescribeTable("focused", func() {}, Entry("focused"), @@ -62,10 +58,6 @@ var _ = Describe("FocusedFixture", func() { }) - Measure("not focused", func(b Benchmarker) { - - }, 2) - DescribeTable("not focused", func() {}, Entry("not focused"), diff --git a/integration/_fixtures/focused_fixture_with_vendor/vendor/foo/bar/bar.go b/integration/_fixtures/focused_with_vendor_fixture/vendor/foo/bar/bar.go similarity index 100% rename from integration/_fixtures/focused_fixture_with_vendor/vendor/foo/bar/bar.go rename to integration/_fixtures/focused_with_vendor_fixture/vendor/foo/bar/bar.go diff --git a/integration/_fixtures/focused_fixture_with_vendor/vendor/foo/foo.go b/integration/_fixtures/focused_with_vendor_fixture/vendor/foo/foo.go similarity index 100% rename from integration/_fixtures/focused_fixture_with_vendor/vendor/foo/foo.go rename to integration/_fixtures/focused_with_vendor_fixture/vendor/foo/foo.go diff --git a/integration/_fixtures/focused_fixture_with_vendor/vendor/vendored.go b/integration/_fixtures/focused_with_vendor_fixture/vendor/vendored.go similarity index 100% rename from integration/_fixtures/focused_fixture_with_vendor/vendor/vendored.go rename to integration/_fixtures/focused_with_vendor_fixture/vendor/vendored.go diff --git a/integration/_fixtures/hanging_suite/hanging_suite_suite_test.go b/integration/_fixtures/hanging_fixture/hanging_suite_test.go similarity index 100% rename from integration/_fixtures/hanging_suite/hanging_suite_suite_test.go rename to integration/_fixtures/hanging_fixture/hanging_suite_test.go diff --git a/integration/_fixtures/hanging_suite/hanging_suite_test.go b/integration/_fixtures/hanging_fixture/hanging_test.go similarity index 64% rename from integration/_fixtures/hanging_suite/hanging_suite_test.go rename to integration/_fixtures/hanging_fixture/hanging_test.go index 6a5a070e12..7a11de526b 100644 --- a/integration/_fixtures/hanging_suite/hanging_suite_test.go +++ b/integration/_fixtures/hanging_fixture/hanging_test.go @@ -26,5 +26,19 @@ var _ = Describe("HangingSuite", func() { fmt.Println("Sleeping...") time.Sleep(time.Hour) }) + + AfterEach(func() { + fmt.Fprintln(GinkgoWriter, "Cleaning up once...") + }) + }) + + AfterEach(func() { + fmt.Fprintln(GinkgoWriter, "Cleaning up twice...") + fmt.Println("Sleeping again...") + time.Sleep(time.Hour) + }) + + AfterEach(func() { + fmt.Fprintln(GinkgoWriter, "Cleaning up thrice...") }) }) diff --git a/integration/_fixtures/malformed_fixture/malformed_fixture_suite_test.go b/integration/_fixtures/malformed_fixture/malformed_fixture_suite_test.go new file mode 100644 index 0000000000..5974add14e --- /dev/null +++ b/integration/_fixtures/malformed_fixture/malformed_fixture_suite_test.go @@ -0,0 +1,13 @@ +package malformed_fixture_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestMalformedFixture(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "MalformedFixture Suite") +} diff --git a/integration/_fixtures/malformed_fixture/malformed_fixture_test.go b/integration/_fixtures/malformed_fixture/malformed_fixture_test.go new file mode 100644 index 0000000000..cceb0c0bcd --- /dev/null +++ b/integration/_fixtures/malformed_fixture/malformed_fixture_test.go @@ -0,0 +1,13 @@ +package malformed_fixture_test + +import ( + . "github.com/onsi/ginkgo" +) + +var _ = Describe("MalformedFixture", func() { + It("tries to install a container within an It...", func() { + Context("...which is not allowed!", func() { + + }) + }) +}) diff --git a/integration/_fixtures/more_ginkgo_tests/more_ginkgo_tests.go b/integration/_fixtures/more_ginkgo_tests_fixture/more_ginkgo_tests.go similarity index 100% rename from integration/_fixtures/more_ginkgo_tests/more_ginkgo_tests.go rename to integration/_fixtures/more_ginkgo_tests_fixture/more_ginkgo_tests.go diff --git a/integration/_fixtures/more_ginkgo_tests/more_ginkgo_tests_suite_test.go b/integration/_fixtures/more_ginkgo_tests_fixture/more_ginkgo_tests_suite_test.go similarity index 100% rename from integration/_fixtures/more_ginkgo_tests/more_ginkgo_tests_suite_test.go rename to integration/_fixtures/more_ginkgo_tests_fixture/more_ginkgo_tests_suite_test.go diff --git a/integration/_fixtures/more_ginkgo_tests/more_ginkgo_tests_test.go b/integration/_fixtures/more_ginkgo_tests_fixture/more_ginkgo_tests_test.go similarity index 96% rename from integration/_fixtures/more_ginkgo_tests/more_ginkgo_tests_test.go rename to integration/_fixtures/more_ginkgo_tests_fixture/more_ginkgo_tests_test.go index 0549f62fb9..d960b70623 100644 --- a/integration/_fixtures/more_ginkgo_tests/more_ginkgo_tests_test.go +++ b/integration/_fixtures/more_ginkgo_tests_fixture/more_ginkgo_tests_test.go @@ -2,7 +2,7 @@ package more_ginkgo_tests_test import ( . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/integration/_fixtures/more_ginkgo_tests" + . "github.com/onsi/ginkgo/integration/_fixtures/more_ginkgo_tests_fixture" . "github.com/onsi/gomega" ) diff --git a/integration/_fixtures/no_test_fn/no_test_fn.go b/integration/_fixtures/no_test_fn_fixture/no_test_fn.go similarity index 100% rename from integration/_fixtures/no_test_fn/no_test_fn.go rename to integration/_fixtures/no_test_fn_fixture/no_test_fn.go diff --git a/integration/_fixtures/no_test_fn/no_test_fn_test.go b/integration/_fixtures/no_test_fn_fixture/no_test_fn_test.go similarity index 76% rename from integration/_fixtures/no_test_fn/no_test_fn_test.go rename to integration/_fixtures/no_test_fn_fixture/no_test_fn_test.go index 6c38b1e432..b8ad905926 100644 --- a/integration/_fixtures/no_test_fn/no_test_fn_test.go +++ b/integration/_fixtures/no_test_fn_fixture/no_test_fn_test.go @@ -2,7 +2,7 @@ package no_test_fn_test import ( . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/integration/_fixtures/no_test_fn" + . "github.com/onsi/ginkgo/integration/_fixtures/no_test_fn_fixture" . "github.com/onsi/gomega" ) diff --git a/integration/_fixtures/no_tests/no_tests.go b/integration/_fixtures/no_tests_fixture/no_tests.go similarity index 100% rename from integration/_fixtures/no_tests/no_tests.go rename to integration/_fixtures/no_tests_fixture/no_tests.go diff --git a/integration/_fixtures/passing_ginkgo_tests/passing_ginkgo_tests.go b/integration/_fixtures/passing_ginkgo_tests_fixture/passing_ginkgo_tests.go similarity index 100% rename from integration/_fixtures/passing_ginkgo_tests/passing_ginkgo_tests.go rename to integration/_fixtures/passing_ginkgo_tests_fixture/passing_ginkgo_tests.go diff --git a/integration/_fixtures/passing_ginkgo_tests/passing_ginkgo_tests_suite_test.go b/integration/_fixtures/passing_ginkgo_tests_fixture/passing_ginkgo_tests_suite_test.go similarity index 100% rename from integration/_fixtures/passing_ginkgo_tests/passing_ginkgo_tests_suite_test.go rename to integration/_fixtures/passing_ginkgo_tests_fixture/passing_ginkgo_tests_suite_test.go diff --git a/integration/_fixtures/passing_ginkgo_tests/passing_ginkgo_tests_test.go b/integration/_fixtures/passing_ginkgo_tests_fixture/passing_ginkgo_tests_test.go similarity index 97% rename from integration/_fixtures/passing_ginkgo_tests/passing_ginkgo_tests_test.go rename to integration/_fixtures/passing_ginkgo_tests_fixture/passing_ginkgo_tests_test.go index a5822fdd7f..f205f7021f 100644 --- a/integration/_fixtures/passing_ginkgo_tests/passing_ginkgo_tests_test.go +++ b/integration/_fixtures/passing_ginkgo_tests_fixture/passing_ginkgo_tests_test.go @@ -2,7 +2,7 @@ package passing_ginkgo_tests_test import ( . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/integration/_fixtures/passing_ginkgo_tests" + . "github.com/onsi/ginkgo/integration/_fixtures/passing_ginkgo_tests_fixture" . "github.com/onsi/gomega" ) diff --git a/integration/_fixtures/passing_suite_setup/passing_suite_setup_suite_test.go b/integration/_fixtures/passing_suite_setup/passing_suite_setup_suite_test.go deleted file mode 100644 index 86c9aa2abd..0000000000 --- a/integration/_fixtures/passing_suite_setup/passing_suite_setup_suite_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package passing_before_suite_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestPassingSuiteSetup(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "PassingSuiteSetup Suite") -} - -var a string -var b string - -var _ = BeforeSuite(func() { - a = "ran before suite" - println("BEFORE SUITE") -}) - -var _ = AfterSuite(func() { - b = "ran after suite" - println("AFTER SUITE") -}) diff --git a/integration/_fixtures/passing_suite_setup/passing_suite_test.go b/integration/_fixtures/passing_suite_setup/passing_suite_test.go deleted file mode 100644 index f139e1d22a..0000000000 --- a/integration/_fixtures/passing_suite_setup/passing_suite_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package passing_before_suite_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("PassingSuiteSetup", func() { - It("should pass", func() { - Ω(a).Should(Equal("ran before suite")) - Ω(b).Should(BeEmpty()) - }) - - It("should pass", func() { - Ω(a).Should(Equal("ran before suite")) - Ω(b).Should(BeEmpty()) - }) - - It("should pass", func() { - Ω(a).Should(Equal("ran before suite")) - Ω(b).Should(BeEmpty()) - }) - - It("should pass", func() { - Ω(a).Should(Equal("ran before suite")) - Ω(b).Should(BeEmpty()) - }) -}) diff --git a/integration/_fixtures/skip_fixture/skip_fixture_suite_test.go b/integration/_fixtures/skip_fixture/skip_fixture_suite_test.go index b2028cf559..1ab728a0ae 100644 --- a/integration/_fixtures/skip_fixture/skip_fixture_suite_test.go +++ b/integration/_fixtures/skip_fixture/skip_fixture_suite_test.go @@ -1,4 +1,4 @@ -package fail_fixture_test +package skip_fixture_test import ( . "github.com/onsi/ginkgo" @@ -7,7 +7,7 @@ import ( "testing" ) -func TestFail_fixture(t *testing.T) { +func TestSkip_fixture(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Skip_fixture Suite") } diff --git a/integration/_fixtures/skip_fixture/skip_fixture_test.go b/integration/_fixtures/skip_fixture/skip_fixture_test.go index e406aeb46d..c736d798f0 100644 --- a/integration/_fixtures/skip_fixture/skip_fixture_test.go +++ b/integration/_fixtures/skip_fixture/skip_fixture_test.go @@ -1,4 +1,4 @@ -package fail_fixture_test +package skip_fixture_test import ( . "github.com/onsi/ginkgo" @@ -10,42 +10,11 @@ var _ = It("handles top level skips", func() { println("NEVER SEE THIS") }) -var _ = It("handles async top level skips", func(done Done) { - Skip("an async top level skip on line 14") - println("NEVER SEE THIS") -}, 0.1) - -var _ = It("SKIP in a goroutine", func(done Done) { - go func() { - defer GinkgoRecover() - Skip("a top level goroutine skip on line 21") - println("NEVER SEE THIS") - }() -}, 0.1) - var _ = Describe("Excercising different skip modes", func() { It("synchronous skip", func() { Skip("a sync SKIP") println("NEVER SEE THIS") }) - - It("async skip", func(done Done) { - Skip("an async SKIP") - println("NEVER SEE THIS") - }, 0.1) - - It("SKIP in a goroutine", func(done Done) { - go func() { - defer GinkgoRecover() - Skip("a goroutine SKIP") - println("NEVER SEE THIS") - }() - }, 0.1) - - Measure("a SKIP measure", func(Benchmarker) { - Skip("a measure SKIP") - println("NEVER SEE THIS") - }, 1) }) var _ = Describe("SKIP in a BeforeEach", func() { diff --git a/integration/_fixtures/synchronized_setup_tests/synchronized_setup_tests_suite_test.go b/integration/_fixtures/synchronized_setup_tests_fixture/synchronized_setup_tests_suite_test.go similarity index 100% rename from integration/_fixtures/synchronized_setup_tests/synchronized_setup_tests_suite_test.go rename to integration/_fixtures/synchronized_setup_tests_fixture/synchronized_setup_tests_suite_test.go diff --git a/integration/_fixtures/tags_tests/ignored_test.go b/integration/_fixtures/tags_fixture/ignored_test.go similarity index 89% rename from integration/_fixtures/tags_tests/ignored_test.go rename to integration/_fixtures/tags_fixture/ignored_test.go index 5176235362..8f0461117f 100644 --- a/integration/_fixtures/tags_tests/ignored_test.go +++ b/integration/_fixtures/tags_fixture/ignored_test.go @@ -1,6 +1,6 @@ // +build complex_tests -package tags_tests_test +package tags_test import ( . "github.com/onsi/ginkgo" diff --git a/integration/_fixtures/tags_tests/tags_tests_suite_test.go b/integration/_fixtures/tags_fixture/tags_tests_suite_test.go similarity index 88% rename from integration/_fixtures/tags_tests/tags_tests_suite_test.go rename to integration/_fixtures/tags_fixture/tags_tests_suite_test.go index dcb11bb1b8..cd941db307 100644 --- a/integration/_fixtures/tags_tests/tags_tests_suite_test.go +++ b/integration/_fixtures/tags_fixture/tags_tests_suite_test.go @@ -1,4 +1,4 @@ -package tags_tests_test +package tags_test import ( . "github.com/onsi/ginkgo" diff --git a/integration/_fixtures/tags_tests/tags_tests_test.go b/integration/_fixtures/tags_fixture/tags_tests_test.go similarity index 83% rename from integration/_fixtures/tags_tests/tags_tests_test.go rename to integration/_fixtures/tags_fixture/tags_tests_test.go index b91a8923ae..dd32ec7726 100644 --- a/integration/_fixtures/tags_tests/tags_tests_test.go +++ b/integration/_fixtures/tags_fixture/tags_tests_test.go @@ -1,4 +1,4 @@ -package tags_tests_test +package tags_test import ( . "github.com/onsi/ginkgo" diff --git a/integration/_fixtures/test_description/test_description_suite_test.go b/integration/_fixtures/test_description/test_description_suite_test.go deleted file mode 100644 index 8976370d30..0000000000 --- a/integration/_fixtures/test_description/test_description_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package test_description_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestTestDescription(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "TestDescription Suite") -} diff --git a/integration/_fixtures/test_description/test_description_test.go b/integration/_fixtures/test_description/test_description_test.go deleted file mode 100644 index 53c2779ead..0000000000 --- a/integration/_fixtures/test_description/test_description_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package test_description_test - -import ( - "fmt" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("TestDescription", func() { - It("should pass", func() { - Ω(true).Should(BeTrue()) - }) - - It("should fail", func() { - Ω(true).Should(BeFalse()) - }) - - AfterEach(func() { - description := CurrentGinkgoTestDescription() - fmt.Printf("%s:%t\n", description.FullTestText, description.Failed) - }) -}) diff --git a/integration/_fixtures/watch_fixture/A/A.go b/integration/_fixtures/watch_fixture/A/A.go new file mode 100644 index 0000000000..67f1439fe2 --- /dev/null +++ b/integration/_fixtures/watch_fixture/A/A.go @@ -0,0 +1,7 @@ +package A + +import "github.com/onsi/ginkgo/integration/_fixtures/watch_fixture/B" + +func DoIt() string { + return B.DoIt() +} diff --git a/integration/_fixtures/watch_fixtures/A/A_suite_test.go b/integration/_fixtures/watch_fixture/A/A_suite_test.go similarity index 100% rename from integration/_fixtures/watch_fixtures/A/A_suite_test.go rename to integration/_fixtures/watch_fixture/A/A_suite_test.go diff --git a/integration/_fixtures/watch_fixtures/A/A_test.go b/integration/_fixtures/watch_fixture/A/A_test.go similarity index 74% rename from integration/_fixtures/watch_fixtures/A/A_test.go rename to integration/_fixtures/watch_fixture/A/A_test.go index 69dde9f4da..d73f342cb1 100644 --- a/integration/_fixtures/watch_fixtures/A/A_test.go +++ b/integration/_fixtures/watch_fixture/A/A_test.go @@ -1,7 +1,7 @@ package A_test import ( - . "$ROOT_PATH$/A" + . "github.com/onsi/ginkgo/integration/_fixtures/watch_fixture/A" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff --git a/integration/_fixtures/watch_fixture/B/B.go b/integration/_fixtures/watch_fixture/B/B.go new file mode 100644 index 0000000000..a5cbcefd95 --- /dev/null +++ b/integration/_fixtures/watch_fixture/B/B.go @@ -0,0 +1,7 @@ +package B + +import "github.com/onsi/ginkgo/integration/_fixtures/watch_fixture/C" + +func DoIt() string { + return C.DoIt() +} diff --git a/integration/_fixtures/watch_fixtures/B/B_suite_test.go b/integration/_fixtures/watch_fixture/B/B_suite_test.go similarity index 100% rename from integration/_fixtures/watch_fixtures/B/B_suite_test.go rename to integration/_fixtures/watch_fixture/B/B_suite_test.go diff --git a/integration/_fixtures/watch_fixtures/B/B_test.go b/integration/_fixtures/watch_fixture/B/B_test.go similarity index 74% rename from integration/_fixtures/watch_fixtures/B/B_test.go rename to integration/_fixtures/watch_fixture/B/B_test.go index d4620b3eb9..37e3525c0d 100644 --- a/integration/_fixtures/watch_fixtures/B/B_test.go +++ b/integration/_fixtures/watch_fixture/B/B_test.go @@ -1,7 +1,7 @@ package B_test import ( - . "$ROOT_PATH$/B" + . "github.com/onsi/ginkgo/integration/_fixtures/watch_fixture/B" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff --git a/integration/_fixtures/watch_fixtures/C/C.go b/integration/_fixtures/watch_fixture/C/C.go similarity index 100% rename from integration/_fixtures/watch_fixtures/C/C.go rename to integration/_fixtures/watch_fixture/C/C.go diff --git a/integration/_fixtures/watch_fixtures/C/C.json b/integration/_fixtures/watch_fixture/C/C.json similarity index 100% rename from integration/_fixtures/watch_fixtures/C/C.json rename to integration/_fixtures/watch_fixture/C/C.json diff --git a/integration/_fixtures/watch_fixtures/C/C_suite_test.go b/integration/_fixtures/watch_fixture/C/C_suite_test.go similarity index 100% rename from integration/_fixtures/watch_fixtures/C/C_suite_test.go rename to integration/_fixtures/watch_fixture/C/C_suite_test.go diff --git a/integration/_fixtures/watch_fixtures/C/C_test.go b/integration/_fixtures/watch_fixture/C/C_test.go similarity index 74% rename from integration/_fixtures/watch_fixtures/C/C_test.go rename to integration/_fixtures/watch_fixture/C/C_test.go index a6b41ce63a..6dc02667a2 100644 --- a/integration/_fixtures/watch_fixtures/C/C_test.go +++ b/integration/_fixtures/watch_fixture/C/C_test.go @@ -1,7 +1,7 @@ package C_test import ( - . "$ROOT_PATH$/C" + . "github.com/onsi/ginkgo/integration/_fixtures/watch_fixture/C" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff --git a/integration/_fixtures/watch_fixture/D/D.go b/integration/_fixtures/watch_fixture/D/D.go new file mode 100644 index 0000000000..13a77fa742 --- /dev/null +++ b/integration/_fixtures/watch_fixture/D/D.go @@ -0,0 +1,7 @@ +package D + +import "github.com/onsi/ginkgo/integration/_fixtures/watch_fixture/C" + +func DoIt() string { + return C.DoIt() +} diff --git a/integration/_fixtures/watch_fixtures/D/D_suite_test.go b/integration/_fixtures/watch_fixture/D/D_suite_test.go similarity index 100% rename from integration/_fixtures/watch_fixtures/D/D_suite_test.go rename to integration/_fixtures/watch_fixture/D/D_suite_test.go diff --git a/integration/_fixtures/watch_fixtures/D/D_test.go b/integration/_fixtures/watch_fixture/D/D_test.go similarity index 74% rename from integration/_fixtures/watch_fixtures/D/D_test.go rename to integration/_fixtures/watch_fixture/D/D_test.go index e4ff82daaa..1f05d2fb66 100644 --- a/integration/_fixtures/watch_fixtures/D/D_test.go +++ b/integration/_fixtures/watch_fixture/D/D_test.go @@ -1,7 +1,7 @@ package D_test import ( - . "$ROOT_PATH$/D" + . "github.com/onsi/ginkgo/integration/_fixtures/watch_fixture/D" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff --git a/integration/_fixtures/watch_fixtures/A/A.go b/integration/_fixtures/watch_fixtures/A/A.go deleted file mode 100644 index 5e37c94f38..0000000000 --- a/integration/_fixtures/watch_fixtures/A/A.go +++ /dev/null @@ -1,7 +0,0 @@ -package A - -import "$ROOT_PATH$/B" - -func DoIt() string { - return B.DoIt() -} diff --git a/integration/_fixtures/watch_fixtures/B/B.go b/integration/_fixtures/watch_fixtures/B/B.go deleted file mode 100644 index 855b93221b..0000000000 --- a/integration/_fixtures/watch_fixtures/B/B.go +++ /dev/null @@ -1,7 +0,0 @@ -package B - -import "$ROOT_PATH$/C" - -func DoIt() string { - return C.DoIt() -} diff --git a/integration/_fixtures/watch_fixtures/D/D.go b/integration/_fixtures/watch_fixtures/D/D.go deleted file mode 100644 index 1c6f495172..0000000000 --- a/integration/_fixtures/watch_fixtures/D/D.go +++ /dev/null @@ -1,7 +0,0 @@ -package D - -import "$ROOT_PATH$/C" - -func DoIt() string { - return C.DoIt() -} diff --git a/integration/_fixtures/xunit_tests/xunit_tests.go b/integration/_fixtures/xunit_fixture/xunit_tests.go similarity index 100% rename from integration/_fixtures/xunit_tests/xunit_tests.go rename to integration/_fixtures/xunit_fixture/xunit_tests.go diff --git a/integration/_fixtures/xunit_tests/xunit_tests_test.go b/integration/_fixtures/xunit_fixture/xunit_tests_test.go similarity index 100% rename from integration/_fixtures/xunit_tests/xunit_tests_test.go rename to integration/_fixtures/xunit_fixture/xunit_tests_test.go diff --git a/integration/convert_test.go b/integration/convert_test.go deleted file mode 100644 index f4fd678c5f..0000000000 --- a/integration/convert_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package integration_test - -import ( - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("ginkgo convert", func() { - var tmpDir string - - readConvertedFileNamed := func(pathComponents ...string) string { - pathToFile := filepath.Join(tmpDir, "convert_fixtures", filepath.Join(pathComponents...)) - bytes, err := ioutil.ReadFile(pathToFile) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - return string(bytes) - } - - readGoldMasterNamed := func(filename string) string { - bytes, err := ioutil.ReadFile(filepath.Join("_fixtures", "convert_goldmasters", filename)) - Ω(err).ShouldNot(HaveOccurred()) - - return string(bytes) - } - - BeforeEach(func() { - var err error - - tmpDir, err = ioutil.TempDir("", "ginkgo-convert") - Ω(err).ShouldNot(HaveOccurred()) - - err = exec.Command("cp", "-r", filepath.Join("_fixtures", "convert_fixtures"), tmpDir).Run() - Ω(err).ShouldNot(HaveOccurred()) - }) - - JustBeforeEach(func() { - cwd, err := os.Getwd() - Ω(err).ShouldNot(HaveOccurred()) - - relPath, err := filepath.Rel(cwd, filepath.Join(tmpDir, "convert_fixtures")) - Ω(err).ShouldNot(HaveOccurred()) - - cmd := exec.Command(pathToGinkgo, "convert", relPath) - cmd.Env = os.Environ() - for i, env := range cmd.Env { - if strings.HasPrefix(env, "PATH") { - cmd.Env[i] = cmd.Env[i] + ":" + filepath.Dir(pathToGinkgo) - break - } - } - err = cmd.Run() - Ω(err).ShouldNot(HaveOccurred()) - }) - - AfterEach(func() { - err := os.RemoveAll(tmpDir) - Ω(err).ShouldNot(HaveOccurred()) - }) - - It("rewrites xunit tests as ginkgo tests", func() { - convertedFile := readConvertedFileNamed("xunit_test.go") - goldMaster := readGoldMasterNamed("xunit_test.go") - Ω(convertedFile).Should(Equal(goldMaster)) - }) - - It("rewrites all usages of *testing.T as mr.T()", func() { - convertedFile := readConvertedFileNamed("extra_functions_test.go") - goldMaster := readGoldMasterNamed("extra_functions_test.go") - Ω(convertedFile).Should(Equal(goldMaster)) - }) - - It("rewrites tests in the package dir that belong to other packages", func() { - convertedFile := readConvertedFileNamed("outside_package_test.go") - goldMaster := readGoldMasterNamed("outside_package_test.go") - Ω(convertedFile).Should(Equal(goldMaster)) - }) - - It("rewrites tests in nested packages", func() { - convertedFile := readConvertedFileNamed("nested", "nested_test.go") - goldMaster := readGoldMasterNamed("nested_test.go") - Ω(convertedFile).Should(Equal(goldMaster)) - }) - - Context("ginkgo test suite files", func() { - It("creates a ginkgo test suite file for the package you specified", func() { - testsuite := readConvertedFileNamed("convert_fixtures_suite_test.go") - goldMaster := readGoldMasterNamed("suite_test.go") - Ω(testsuite).Should(Equal(goldMaster)) - }) - - It("converts go tests in deeply nested packages (some may not contain go files)", func() { - testsuite := readConvertedFileNamed("nested_without_gofiles", "subpackage", "nested_subpackage_test.go") - goldMaster := readGoldMasterNamed("nested_subpackage_test.go") - Ω(testsuite).Should(Equal(goldMaster)) - }) - - It("creates ginkgo test suites for all nested packages", func() { - testsuite := readConvertedFileNamed("nested", "nested_suite_test.go") - goldMaster := readGoldMasterNamed("nested_suite_test.go") - Ω(testsuite).Should(Equal(goldMaster)) - }) - }) - - Context("with an existing test suite file", func() { - BeforeEach(func() { - goldMaster := readGoldMasterNamed("fixtures_suite_test.go") - err := ioutil.WriteFile(filepath.Join(tmpDir, "convert_fixtures", "tmp_suite_test.go"), []byte(goldMaster), 0600) - Ω(err).ShouldNot(HaveOccurred()) - }) - - It("gracefully handles existing test suite files", func() { - //nothing should have gone wrong! - }) - }) -}) diff --git a/integration/coverage_test.go b/integration/coverage_test.go index 962538cb1a..0be930cdff 100644 --- a/integration/coverage_test.go +++ b/integration/coverage_test.go @@ -1,7 +1,6 @@ package integration_test import ( - "io/ioutil" "os/exec" "regexp" @@ -14,145 +13,108 @@ import ( ) var _ = Describe("Coverage Specs", func() { - Context("when it runs coverage analysis in series and in parallel", func() { - AfterEach(func() { - removeSuccessfully("./_fixtures/coverage_fixture/coverage_fixture.coverprofile") - }) - - It("works", func() { - session := startGinkgo("./_fixtures/coverage_fixture", "-cover") - Eventually(session).Should(gexec.Exit(0)) - - Ω(session.Out).Should(gbytes.Say(("coverage: 80.0% of statements"))) - - coverFile := "./_fixtures/coverage_fixture/coverage_fixture.coverprofile" - serialCoverProfileOutput, err := exec.Command("go", "tool", "cover", fmt.Sprintf("-func=%s", coverFile)).CombinedOutput() - Ω(err).ShouldNot(HaveOccurred()) - - removeSuccessfully(coverFile) - - Eventually(startGinkgo("./_fixtures/coverage_fixture", "-cover", "-nodes=4")).Should(gexec.Exit(0)) - - parallelCoverProfileOutput, err := exec.Command("go", "tool", "cover", fmt.Sprintf("-func=%s", coverFile)).CombinedOutput() - Ω(err).ShouldNot(HaveOccurred()) - - Ω(parallelCoverProfileOutput).Should(Equal(serialCoverProfileOutput)) - - By("handling external packages", func() { - session = startGinkgo("./_fixtures/coverage_fixture", "-coverpkg=github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture,github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture/external_coverage_fixture") - Eventually(session).Should(gexec.Exit(0)) - - Ω(session.Out).Should(gbytes.Say("coverage: 71.4% of statements in github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture, github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture/external_coverage_fixture")) - - serialCoverProfileOutput, err = exec.Command("go", "tool", "cover", fmt.Sprintf("-func=%s", coverFile)).CombinedOutput() - Ω(err).ShouldNot(HaveOccurred()) - - removeSuccessfully("./_fixtures/coverage_fixture/coverage_fixture.coverprofile") - - Eventually(startGinkgo("./_fixtures/coverage_fixture", "-coverpkg=github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture,github.com/onsi/ginkgo/integration/_fixtures/coverage_fixture/external_coverage_fixture", "-nodes=4")).Should(gexec.Exit(0)) - - parallelCoverProfileOutput, err = exec.Command("go", "tool", "cover", fmt.Sprintf("-func=%s", coverFile)).CombinedOutput() - Ω(err).ShouldNot(HaveOccurred()) - - Ω(parallelCoverProfileOutput).Should(Equal(serialCoverProfileOutput)) - }) - }) + BeforeEach(func() { + fm.MountFixture("coverage") }) - Context("when a custom profile name is specified", func() { - AfterEach(func() { - removeSuccessfully("./_fixtures/coverage_fixture/coverage.txt") - }) - - It("generates cover profiles with the specified name", func() { - session := startGinkgo("./_fixtures/coverage_fixture", "-cover", "-coverprofile=coverage.txt") - Eventually(session).Should(gexec.Exit(0)) - - Ω("./_fixtures/coverage_fixture/coverage.txt").Should(BeARegularFile()) + processCoverageProfile := func(path string) string { + profileOutput, err := exec.Command("go", "tool", "cover", fmt.Sprintf("-func=%s", path)).CombinedOutput() + ExpectWithOffset(1, err).ShouldNot(HaveOccurred()) + return string(profileOutput) + } + + Context("when running a single package in series or in parallel with -cover", func() { + It("emits the coverage pecentage and generates a cover profile", func() { + seriesSession := startGinkgo(fm.PathTo("coverage"), "--no-color", "-cover") + Eventually(seriesSession).Should(gexec.Exit(0)) + Ω(seriesSession.Out).Should(gbytes.Say(`coverage: 80\.0% of statements`)) + seriesCoverage := processCoverageProfile(fm.PathTo("coverage", "coverprofile.out")) + fm.RemoveFile("coverage", "coverprofile.out") + + parallelSession := startGinkgo(fm.PathTo("coverage"), "--no-color", "-nodes=2", "-cover") + Eventually(parallelSession).Should(gexec.Exit(0)) + parallelCoverage := processCoverageProfile(fm.PathTo("coverage", "coverprofile.out")) + + Ω(parallelCoverage).Should(Equal(seriesCoverage)) }) }) - Context("when run in recursive mode", func() { - AfterEach(func() { - removeSuccessfully("./_fixtures/combined_coverage_fixture/coverage-recursive.txt") - removeSuccessfully("./_fixtures/combined_coverage_fixture/first_package/coverage-recursive.txt") - removeSuccessfully("./_fixtures/combined_coverage_fixture/second_package/coverage-recursive.txt") - }) + Context("with -coverpkg", func() { + It("computes coverage of the passed-in additional packages", func() { + coverPkgFlag := fmt.Sprintf("-coverpkg=%s,%s", fm.PackageNameFor("coverage"), fm.PackageNameFor("coverage/external_coverage")) + seriesSession := startGinkgo(fm.PathTo("coverage"), coverPkgFlag) + Eventually(seriesSession).Should(gexec.Exit(0)) + Ω(seriesSession.Out).Should(gbytes.Say("coverage: 71.4% of statements in")) + seriesCoverage := processCoverageProfile(fm.PathTo("coverage", "coverprofile.out")) + fm.RemoveFile("coverage", "coverprofile.out") - It("generates a coverage file per package", func() { - session := startGinkgo("./_fixtures/combined_coverage_fixture", "-r", "-cover", "-coverprofile=coverage-recursive.txt") - Eventually(session).Should(gexec.Exit(0)) + parallelSession := startGinkgo(fm.PathTo("coverage"), "--no-color", "-nodes=2", coverPkgFlag) + Eventually(parallelSession).Should(gexec.Exit(0)) + parallelCoverage := processCoverageProfile(fm.PathTo("coverage", "coverprofile.out")) - Ω("./_fixtures/combined_coverage_fixture/first_package/coverage-recursive.txt").Should(BeARegularFile()) - Ω("./_fixtures/combined_coverage_fixture/second_package/coverage-recursive.txt").Should(BeARegularFile()) + Ω(parallelCoverage).Should(Equal(seriesCoverage)) }) }) - Context("when run in parallel mode", func() { - AfterEach(func() { - removeSuccessfully("./_fixtures/coverage_fixture/coverage-parallel.txt") - }) - - It("works", func() { - session := startGinkgo("./_fixtures/coverage_fixture", "-p", "-cover", "-coverprofile=coverage-parallel.txt") - + Context("with a custom profile name", func() { + It("generates cover profiles with the specified name", func() { + session := startGinkgo(fm.PathTo("coverage"), "--no-color", "-coverprofile=myprofile.out") Eventually(session).Should(gexec.Exit(0)) - - Ω("./_fixtures/coverage_fixture/coverage-parallel.txt").Should(BeARegularFile()) + Ω(session.Out).Should(gbytes.Say(`coverage: 80\.0% of statements`)) + Ω(fm.PathTo("coverage", "myprofile.out")).Should(BeAnExistingFile()) + Ω(fm.PathTo("coverage", "coverprofile.out")).ShouldNot(BeAnExistingFile()) }) }) - Context("when run in recursive mode specifying a coverprofile", func() { - AfterEach(func() { - removeSuccessfully("./_fixtures/combined_coverage_fixture/coverprofile-recursive.txt") - removeSuccessfully("./_fixtures/combined_coverage_fixture/first_package/coverprofile-recursive.txt") - removeSuccessfully("./_fixtures/combined_coverage_fixture/second_package/coverprofile-recursive.txt") + Context("when multiple suites are tested", func() { + BeforeEach(func() { + fm.MountFixture("combined_coverage") }) - It("combines the coverages", func() { - session := startGinkgo("./_fixtures/combined_coverage_fixture", "-outputdir=./", "-r", "-cover", "-coverprofile=coverprofile-recursive.txt") + It("generates a single cover profile", func() { + session := startGinkgo(fm.PathTo("combined_coverage"), "--no-color", "--cover", "-r", "--covermode=atomic") Eventually(session).Should(gexec.Exit(0)) + Ω(fm.PathTo("combined_coverage", "coverprofile.out")).Should(BeAnExistingFile()) + Ω(fm.PathTo("combined_coverage", "first_package/coverprofile.out")).ShouldNot(BeAnExistingFile()) + Ω(fm.PathTo("combined_coverage", "second_package/coverprofile.out")).ShouldNot(BeAnExistingFile()) + + By("ensuring there is only one 'mode:' line") + re := regexp.MustCompile(`mode: atomic`) + content := fm.ContentOf("combined_coverage", "coverprofile.out") + matches := re.FindAllStringIndex(content, -1) + Ω(len(matches)).Should(Equal(1)) + }) - By("generating a combined coverage file", func() { - Ω("./_fixtures/combined_coverage_fixture/coverprofile-recursive.txt").Should(BeARegularFile()) - }) - - By("and strips multiple mode specifier", func() { - re := regexp.MustCompile(`mode: atomic`) - bytes, err := ioutil.ReadFile("./_fixtures/combined_coverage_fixture/coverprofile-recursive.txt") - Ω(err).Should(BeNil()) - matches := re.FindAllIndex(bytes, -1) - Ω(len(matches)).Should(Equal(1)) - }) - - By("also generating the single package coverage files", func() { - Ω("./_fixtures/combined_coverage_fixture/first_package/coverprofile-recursive.txt").Should(BeARegularFile()) - Ω("./_fixtures/combined_coverage_fixture/second_package/coverprofile-recursive.txt").Should(BeARegularFile()) + Context("when -keep-separate-coverprofiles is set", func() { + It("generates separate coverprofiles", func() { + session := startGinkgo(fm.PathTo("combined_coverage"), "--no-color", "--cover", "-r", "--keep-separate-coverprofiles") + Eventually(session).Should(gexec.Exit(0)) + Ω(fm.PathTo("combined_coverage", "coverprofile.out")).ShouldNot(BeAnExistingFile()) + Ω(fm.PathTo("combined_coverage", "first_package/coverprofile.out")).Should(BeAnExistingFile()) + Ω(fm.PathTo("combined_coverage", "second_package/coverprofile.out")).Should(BeAnExistingFile()) }) }) }) - It("Fails with an error if output dir and coverprofile were set, but the output dir did not exist", func() { - session := startGinkgo("./_fixtures/combined_coverage_fixture", "-outputdir=./all/profiles/here", "-r", "-cover", "-coverprofile=coverage.txt") - - Eventually(session).Should(gexec.Exit(1)) - output := session.Out.Contents() - Ω(string(output)).Should(ContainSubstring("Unable to create combined profile, outputdir does not exist: ./all/profiles/here")) - }) - - Context("when only output dir was set", func() { - AfterEach(func() { - removeSuccessfully("./_fixtures/combined_coverage_fixture/first_package.coverprofile") - removeSuccessfully("./_fixtures/combined_coverage_fixture/first_package/coverage.txt") - removeSuccessfully("./_fixtures/combined_coverage_fixture/second_package.coverprofile") - removeSuccessfully("./_fixtures/combined_coverage_fixture/second_package/coverage.txt") + Context("when -output-dir is set", func() { + BeforeEach(func() { + fm.MountFixture("combined_coverage") }) - It("moves coverages", func() { - session := startGinkgo("./_fixtures/combined_coverage_fixture", "-outputdir=./", "-r", "-cover") + + It("puts the cover profile in -output-dir", func() { + session := startGinkgo(fm.PathTo("combined_coverage"), "--no-color", "--cover", "-r", "--output-dir=./output") Eventually(session).Should(gexec.Exit(0)) + Ω(fm.PathTo("combined_coverage", "output/coverprofile.out")).Should(BeAnExistingFile()) + }) - Ω("./_fixtures/combined_coverage_fixture/first_package.coverprofile").Should(BeARegularFile()) - Ω("./_fixtures/combined_coverage_fixture/second_package.coverprofile").Should(BeARegularFile()) + Context("when -keep-separate-coverprofiles is set", func() { + It("puts namespaced coverprofiels in the -output-dir", func() { + session := startGinkgo(fm.PathTo("combined_coverage"), "--no-color", "--cover", "-r", "--output-dir=./output", "--keep-separate-coverprofiles") + Eventually(session).Should(gexec.Exit(0)) + Ω(fm.PathTo("combined_coverage", "output/coverprofile.out")).ShouldNot(BeAnExistingFile()) + Ω(fm.PathTo("combined_coverage", "output/first_package_coverprofile.out")).Should(BeAnExistingFile()) + Ω(fm.PathTo("combined_coverage", "output/second_package_coverprofile.out")).Should(BeAnExistingFile()) + }) }) }) }) diff --git a/integration/deprecations_test.go b/integration/deprecations_test.go new file mode 100644 index 0000000000..657a07a03e --- /dev/null +++ b/integration/deprecations_test.go @@ -0,0 +1,24 @@ +package integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Deprecations", func() { + BeforeEach(func() { + fm.MountFixture("deprecated_features") + }) + + It("runs, succeeds, and emits deprecation warnings", func() { + session := startGinkgo(fm.PathTo("deprecated_features"), "--randomizeAllSpecs", "--stream") + Eventually(session).Should(gexec.Exit(0)) + contents := string(session.Out.Contents()) + string(session.Err.Contents()) + + Ω(contents).Should(ContainSubstring("You are passing a Done channel to a test node to test asynchronous behavior.")) + Ω(contents).Should(ContainSubstring("Measure is deprecated in Ginkgo V2")) + Ω(contents).Should(ContainSubstring("--stream is deprecated")) + Ω(contents).Should(ContainSubstring("--randomizeAllSpecs is deprecated")) + }) +}) diff --git a/integration/fail_test.go b/integration/fail_test.go index 9ec764b071..08becb5ba5 100644 --- a/integration/fail_test.go +++ b/integration/fail_test.go @@ -7,56 +7,53 @@ import ( ) var _ = Describe("Failing Specs", func() { - var pathToTest string - - BeforeEach(func() { - pathToTest = tmpPath("failing") - copyIn(fixturePath("fail_fixture"), pathToTest, false) + Describe("when the tests contain failures", func() { + BeforeEach(func() { + fm.MountFixture("fail") + }) + + It("should fail in all the possible ways", func() { + session := startGinkgo(fm.PathTo("fail"), "--no-color") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(output).ShouldNot(ContainSubstring("NEVER SEE THIS")) + + Ω(output).Should(ContainSubstring("a top level failure on line 10")) + Ω(output).Should(ContainSubstring("fail_fixture_test.go:10")) + + Ω(output).Should(ContainSubstring("a sync failure")) + Ω(output).Should(MatchRegexp(`Test Panicked`)) + Ω(output).Should(MatchRegexp(`a sync panic`)) + Ω(output).Should(ContainSubstring("a sync FAIL failure")) + + Ω(output).Should(ContainSubstring("a top level specify")) + Ω(output).ShouldNot(ContainSubstring("ginkgo_dsl.go")) + Ω(output).Should(ContainSubstring("fail_fixture_test.go:31")) + + Ω(output).ShouldNot(ContainSubstring("table.go")) + Ω(output).Should(MatchRegexp(`a top level DescribeTable\n.*fail_fixture_test\.go:35`), + "the output of a failing DescribeTable should include its file path and line number") + Ω(output).ShouldNot(ContainSubstring("table_entry.go")) + Ω(output).Should(MatchRegexp(`a TableEntry constructed by Entry \[It\]\n.*fail_fixture_test\.go:39`), + "the output of a failing Entry should include its file path and line number") + + Ω(output).Should(ContainSubstring("0 Passed | 7 Failed")) + }) }) - It("should fail in all the possible ways", func() { - session := startGinkgo(pathToTest, "--noColor") - Eventually(session).Should(gexec.Exit(1)) - output := string(session.Out.Contents()) - - Ω(output).ShouldNot(ContainSubstring("NEVER SEE THIS")) - - Ω(output).Should(ContainSubstring("a top level failure on line 10")) - Ω(output).Should(ContainSubstring("fail_fixture_test.go:10")) - Ω(output).Should(ContainSubstring("an async top level failure on line 15")) - Ω(output).Should(ContainSubstring("fail_fixture_test.go:15")) - Ω(output).Should(ContainSubstring("a top level goroutine failure on line 22")) - Ω(output).Should(ContainSubstring("fail_fixture_test.go:22")) - - Ω(output).Should(ContainSubstring("a sync failure")) - Ω(output).Should(MatchRegexp(`Test Panicked\n\s+a sync panic`)) - Ω(output).Should(ContainSubstring("a sync FAIL failure")) - Ω(output).Should(ContainSubstring("async timeout [It]")) - Ω(output).Should(ContainSubstring("Timed out")) - Ω(output).Should(ContainSubstring("an async failure")) - Ω(output).Should(MatchRegexp(`Test Panicked\n\s+an async panic`)) - Ω(output).Should(ContainSubstring("an async FAIL failure")) - Ω(output).Should(ContainSubstring("a goroutine FAIL failure")) - Ω(output).Should(ContainSubstring("a goroutine failure")) - Ω(output).Should(MatchRegexp(`Test Panicked\n\s+a goroutine panic`)) - Ω(output).Should(ContainSubstring("a measure failure")) - Ω(output).Should(ContainSubstring("a measure FAIL failure")) - Ω(output).Should(MatchRegexp(`Test Panicked\n\s+a measure panic`)) - - Ω(output).Should(ContainSubstring("a top level specify")) - Ω(output).ShouldNot(ContainSubstring("ginkgo_dsl.go")) - // depending on the go version this could be the first line of the Specify - // block (>= go1.9) or the last line of the Specify block (< go1.9) - Ω(output).Should(Or(ContainSubstring("fail_fixture_test.go:102"), ContainSubstring("fail_fixture_test.go:104"))) - Ω(output).Should(ContainSubstring("fail_fixture_test.go:103")) - - Ω(output).ShouldNot(ContainSubstring("table.go")) - Ω(output).Should(MatchRegexp(`a top level DescribeTable\n.*fail_fixture_test\.go:106`), - "the output of a failing DescribeTable should include its file path and line number") - Ω(output).ShouldNot(ContainSubstring("table_entry.go")) - Ω(output).Should(MatchRegexp(`a TableEntry constructed by Entry \[It\]\n.*fail_fixture_test\.go:110`), - "the output of a failing Entry should include its file path and line number") - - Ω(output).Should(ContainSubstring("0 Passed | 19 Failed")) + Describe("when the tests are incorrectly structured", func() { + BeforeEach(func() { + fm.MountFixture("malformed") + }) + + It("exits early with a helpful error message", func() { + session := startGinkgo(fm.PathTo("malformed"), "--no-color") + Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + + Ω(output).Should(ContainSubstring("Ginkgo detected an issue with your test structure")) + Ω(output).Should(ContainSubstring("malformed_fixture_test.go:9")) + }) }) }) diff --git a/integration/flags_test.go b/integration/flags_test.go index fb7b38a297..a217b1da8e 100644 --- a/integration/flags_test.go +++ b/integration/flags_test.go @@ -1,9 +1,6 @@ package integration_test import ( - "io/ioutil" - "os" - "path/filepath" "strings" . "github.com/onsi/ginkgo" @@ -13,27 +10,23 @@ import ( ) var _ = Describe("Flags Specs", func() { - var pathToTest string - BeforeEach(func() { - pathToTest = tmpPath("flags") - copyIn(fixturePath("flags_tests"), pathToTest, false) + fm.MountFixture("flags") }) getRandomOrders := func(output string) []int { return []int{strings.Index(output, "RANDOM_A"), strings.Index(output, "RANDOM_B"), strings.Index(output, "RANDOM_C")} } - It("normally passes, runs measurements, prints out noisy pendings, does not randomize tests, and honors the programmatic focus", func() { - session := startGinkgo(pathToTest, "--noColor") + It("normally passes, prints out noisy pendings, does not randomize tests, and honors the programmatic focus", func() { + session := startGinkgo(fm.PathTo("flags"), "--no-color") Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) output := string(session.Out.Contents()) - Ω(output).Should(ContainSubstring("Ran 3 samples:"), "has a measurement") - Ω(output).Should(ContainSubstring("11 Passed")) + Ω(output).Should(ContainSubstring("10 Passed")) Ω(output).Should(ContainSubstring("0 Failed")) Ω(output).Should(ContainSubstring("1 Pending")) - Ω(output).Should(ContainSubstring("3 Skipped")) + Ω(output).Should(ContainSubstring("4 Skipped")) Ω(output).Should(ContainSubstring("[PENDING]")) Ω(output).Should(ContainSubstring("marshmallow")) Ω(output).Should(ContainSubstring("chocolate")) @@ -41,44 +34,27 @@ var _ = Describe("Flags Specs", func() { Ω(output).Should(ContainSubstring("Detected Programmatic Focus - setting exit status to %d", types.GINKGO_FOCUS_EXIT_CODE)) Ω(output).ShouldNot(ContainSubstring("smores")) Ω(output).ShouldNot(ContainSubstring("SLOW TEST")) - Ω(output).ShouldNot(ContainSubstring("should honor -slowSpecThreshold")) + Ω(output).ShouldNot(ContainSubstring("should honor -slow-spec-threshold")) orders := getRandomOrders(output) Ω(orders[0]).Should(BeNumerically("<", orders[1])) Ω(orders[1]).Should(BeNumerically("<", orders[2])) }) - It("should run a coverprofile when passed -cover", func() { - session := startGinkgo(pathToTest, "--noColor", "--cover", "--focus=the focused set") - Eventually(session).Should(gexec.Exit(0)) - output := string(session.Out.Contents()) - - _, err := os.Stat(filepath.Join(pathToTest, "flags.coverprofile")) - Ω(err).ShouldNot(HaveOccurred()) - Ω(output).Should(ContainSubstring("coverage: ")) - }) - - It("should fail when there are pending tests and it is passed --failOnPending", func() { - session := startGinkgo(pathToTest, "--noColor", "--failOnPending") + It("should fail when there are pending tests and it is passed --fail-on-pending", func() { + session := startGinkgo(fm.PathTo("flags"), "--no-color", "--fail-on-pending") Eventually(session).Should(gexec.Exit(1)) + output := string(session.Out.Contents()) + Ω(output).Should(ContainSubstring("Detected pending specs and --fail-on-pending is set")) }) - It("should fail if the test suite takes longer than the timeout", func() { - session := startGinkgo(pathToTest, "--noColor", "--timeout=1ms") + PIt("should fail if the test suite takes longer than the timeout", func() { + session := startGinkgo(fm.PathTo("flags"), "--no-color", "--timeout=1ms") Eventually(session).Should(gexec.Exit(1)) }) - It("should not print out pendings when --noisyPendings=false", func() { - session := startGinkgo(pathToTest, "--noColor", "--noisyPendings=false") - Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) - output := string(session.Out.Contents()) - - Ω(output).ShouldNot(ContainSubstring("[PENDING]")) - Ω(output).Should(ContainSubstring("1 Pending")) - }) - It("should override the programmatic focus when told to focus", func() { - session := startGinkgo(pathToTest, "--noColor", "--focus=smores") + session := startGinkgo(fm.PathTo("flags"), "--no-color", "--focus=smores") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -87,40 +63,40 @@ var _ = Describe("Flags Specs", func() { Ω(output).Should(ContainSubstring("smores")) Ω(output).Should(ContainSubstring("3 Passed")) Ω(output).Should(ContainSubstring("0 Failed")) - Ω(output).Should(ContainSubstring("0 Pending")) - Ω(output).Should(ContainSubstring("12 Skipped")) + Ω(output).Should(ContainSubstring("1 Pending")) + Ω(output).Should(ContainSubstring("11 Skipped")) }) It("should override the programmatic focus when told to skip", func() { - session := startGinkgo(pathToTest, "--noColor", "--skip=marshmallow|failing|flaky") + session := startGinkgo(fm.PathTo("flags"), "--no-color", "--skip=marshmallow|failing|flaky") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) Ω(output).ShouldNot(ContainSubstring("marshmallow")) Ω(output).Should(ContainSubstring("chocolate")) Ω(output).Should(ContainSubstring("smores")) - Ω(output).Should(ContainSubstring("11 Passed")) + Ω(output).Should(ContainSubstring("10 Passed")) Ω(output).Should(ContainSubstring("0 Failed")) Ω(output).Should(ContainSubstring("1 Pending")) - Ω(output).Should(ContainSubstring("3 Skipped")) + Ω(output).Should(ContainSubstring("4 Skipped")) }) It("should override the programmatic focus when told to skip (multiple options)", func() { - session := startGinkgo(pathToTest, "--noColor", "--skip=marshmallow", "--skip=failing", "--skip=flaky") + session := startGinkgo(fm.PathTo("flags"), "--no-color", "--skip=marshmallow", "--skip=failing", "--skip=flaky") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) Ω(output).ShouldNot(ContainSubstring("marshmallow")) Ω(output).Should(ContainSubstring("chocolate")) Ω(output).Should(ContainSubstring("smores")) - Ω(output).Should(ContainSubstring("11 Passed")) + Ω(output).Should(ContainSubstring("10 Passed")) Ω(output).Should(ContainSubstring("0 Failed")) Ω(output).Should(ContainSubstring("1 Pending")) - Ω(output).Should(ContainSubstring("3 Skipped")) + Ω(output).Should(ContainSubstring("4 Skipped")) }) It("should ignore empty skip and focus variables", func() { - session := startGinkgo(pathToTest, "--noColor", "--skip=", "--focus=") + session := startGinkgo(fm.PathTo("flags"), "--noColor", "--skip=", "--focus=") Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("marshmallow")) @@ -131,7 +107,7 @@ var _ = Describe("Flags Specs", func() { if !raceDetectorSupported() { Skip("race detection is not supported") } - session := startGinkgo(pathToTest, "--noColor", "--race") + session := startGinkgo(fm.PathTo("flags"), "--no-color", "--race") Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) output := string(session.Out.Contents()) @@ -139,7 +115,7 @@ var _ = Describe("Flags Specs", func() { }) It("should randomize tests when told to", func() { - session := startGinkgo(pathToTest, "--noColor", "--randomizeAllSpecs", "--seed=17") + session := startGinkgo(fm.PathTo("flags"), "--no-color", "--randomize-all", "--seed=1") Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) output := string(session.Out.Contents()) @@ -147,26 +123,17 @@ var _ = Describe("Flags Specs", func() { Ω(orders[0]).ShouldNot(BeNumerically("<", orders[1])) }) - It("should skip measurements when told to", func() { - session := startGinkgo(pathToTest, "--skipMeasurements") - Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) - output := string(session.Out.Contents()) - - Ω(output).ShouldNot(ContainSubstring("Ran 3 samples:"), "has a measurement") - Ω(output).Should(ContainSubstring("4 Skipped")) - }) - It("should watch for slow specs", func() { - session := startGinkgo(pathToTest, "--slowSpecThreshold=0.05") + session := startGinkgo(fm.PathTo("flags"), "--slow-spec-threshold=0.05") Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("SLOW TEST")) - Ω(output).Should(ContainSubstring("should honor -slowSpecThreshold")) + Ω(output).Should(ContainSubstring("should honor -slow-spec-threshold")) }) It("should pass additional arguments in", func() { - session := startGinkgo(pathToTest, "--", "--customFlag=madagascar") + session := startGinkgo(fm.PathTo("flags"), "--", "--customFlag=madagascar") Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) output := string(session.Out.Contents()) @@ -174,7 +141,7 @@ var _ = Describe("Flags Specs", func() { }) It("should print out full stack traces for failures when told to", func() { - session := startGinkgo(pathToTest, "--focus=a failing test", "--trace") + session := startGinkgo(fm.PathTo("flags"), "--focus=a failing test", "--trace") Eventually(session).Should(gexec.Exit(1)) output := string(session.Out.Contents()) @@ -182,81 +149,64 @@ var _ = Describe("Flags Specs", func() { }) It("should fail fast when told to", func() { - pathToTest = tmpPath("fail") - copyIn(fixturePath("fail_fixture"), pathToTest, false) - session := startGinkgo(pathToTest, "--failFast") + fm.MountFixture("fail") + session := startGinkgo(fm.PathTo("fail"), "--fail-fast") Eventually(session).Should(gexec.Exit(1)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("1 Failed")) - Ω(output).Should(ContainSubstring("18 Skipped")) + Ω(output).Should(ContainSubstring("6 Skipped")) }) Context("with a flaky test", func() { It("should normally fail", func() { - session := startGinkgo(pathToTest, "--focus=flaky") + session := startGinkgo(fm.PathTo("flags"), "--focus=flaky") Eventually(session).Should(gexec.Exit(1)) }) It("should pass if retries are requested", func() { - session := startGinkgo(pathToTest, "--focus=flaky --flakeAttempts=2") + session := startGinkgo(fm.PathTo("flags"), "--focus=flaky --flake-attempts=2") Eventually(session).Should(gexec.Exit(0)) }) }) It("should perform a dry run when told to", func() { - pathToTest = tmpPath("fail") - copyIn(fixturePath("fail_fixture"), pathToTest, false) - session := startGinkgo(pathToTest, "--dryRun", "-v") + fm.MountFixture("fail") + session := startGinkgo(fm.PathTo("fail"), "--dry-run", "-v") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("synchronous failures")) - Ω(output).Should(ContainSubstring("19 Specs")) - Ω(output).Should(ContainSubstring("0 Passed")) + Ω(output).Should(ContainSubstring("7 Specs")) + Ω(output).Should(ContainSubstring("7 Passed")) Ω(output).Should(ContainSubstring("0 Failed")) }) regextest := func(regexOption string, skipOrFocus string) string { - pathToTest = tmpPath("passing") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) - session := startGinkgo(pathToTest, regexOption, "--dryRun", "-v", skipOrFocus) + fm.MountFixture("passing_ginkgo_tests") + session := startGinkgo(fm.PathTo("passing_ginkgo_tests"), regexOption, "--dry-run", "-v", skipOrFocus) Eventually(session).Should(gexec.Exit(0)) return string(session.Out.Contents()) } It("regexScansFilePath (enabled) should skip and focus on file names", func() { - output := regextest("-regexScansFilePath=true", "-skip=/passing/") // everything gets skipped (nothing runs) + output := regextest("-regexScansFilePath=true", "-skip=/passing") // everything gets skipped (nothing runs) Ω(output).Should(ContainSubstring("0 of 4 Specs")) - output = regextest("-regexScansFilePath=true", "-focus=/passing/") // everything gets focused (everything runs) + output = regextest("-regexScansFilePath=true", "-focus=/passing") // everything gets focused (everything runs) Ω(output).Should(ContainSubstring("4 of 4 Specs")) }) It("regexScansFilePath (disabled) should not effect normal filtering", func() { - output := regextest("-regexScansFilePath=false", "-skip=/passing/") // nothing gets skipped (everything runs) + output := regextest("-regexScansFilePath=false", "-skip=/passing") // nothing gets skipped (everything runs) Ω(output).Should(ContainSubstring("4 of 4 Specs")) - output = regextest("-regexScansFilePath=false", "-focus=/passing/") // nothing gets focused (nothing runs) + output = regextest("-regexScansFilePath=false", "-focus=/passing") // nothing gets focused (nothing runs) Ω(output).Should(ContainSubstring("0 of 4 Specs")) }) It("should honor compiler flags", func() { - session := startGinkgo(pathToTest, "-gcflags=-importmap 'math=math/cmplx'") + session := startGinkgo(fm.PathTo("flags"), "-gcflags=-importmap 'math=math/cmplx'") Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("NaN returns complex128")) }) - - It("should honor covermode flag", func() { - session := startGinkgo(pathToTest, "--noColor", "--covermode=count", "--focus=the focused set") - Eventually(session).Should(gexec.Exit(0)) - output := string(session.Out.Contents()) - Ω(output).Should(ContainSubstring("coverage: ")) - - coverageFile := filepath.Join(pathToTest, "flags.coverprofile") - _, err := os.Stat(coverageFile) - Ω(err).ShouldNot(HaveOccurred()) - contents, err := ioutil.ReadFile(coverageFile) - Ω(err).ShouldNot(HaveOccurred()) - Ω(contents).Should(ContainSubstring("mode: count")) - }) }) diff --git a/integration/integration_suite_test.go b/integration/integration_suite_test.go index e4d8b44733..1103f6e8b5 100644 --- a/integration/integration_suite_test.go +++ b/integration/integration_suite_test.go @@ -1,8 +1,9 @@ package integration_test import ( + "bytes" + "flag" "fmt" - "io" "io/ioutil" "os" "os/exec" @@ -11,17 +12,24 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" "github.com/onsi/gomega/gexec" "testing" "time" ) -var tmpDir string var pathToGinkgo string +var DEBUG bool +var fm FixtureManager + +func init() { + flag.BoolVar(&DEBUG, "debug", false, "keep assets around after test run") +} func TestIntegration(t *testing.T) { SetDefaultEventuallyTimeout(30 * time.Second) + format.TruncatedDiff = false RegisterFailHandler(Fail) RunSpecs(t, "Integration Suite") } @@ -35,57 +43,111 @@ var _ = SynchronizedBeforeSuite(func() []byte { }) var _ = BeforeEach(func() { - tmpDir = fmt.Sprintf("./ginko-run-%d", GinkgoParallelNode()) - err := os.Mkdir(tmpDir, 0700) - Ω(err).ShouldNot(HaveOccurred()) + fm = NewFixtureManager(fmt.Sprintf("tmp_%d", GinkgoParallelNode())) }) var _ = AfterEach(func() { - err := os.RemoveAll(tmpDir) - Ω(err).ShouldNot(HaveOccurred()) + if !DEBUG { + fm.Cleanup() + } }) var _ = SynchronizedAfterSuite(func() {}, func() { - os.RemoveAll(tmpDir) gexec.CleanupBuildArtifacts() }) -func tmpPath(destination string) string { - return filepath.Join(tmpDir, destination) +type FixtureManager struct { + TmpDir string + FixturePath string } -func fixturePath(name string) string { - return filepath.Join("_fixtures", name) +func NewFixtureManager(tmpDir string) FixtureManager { + err := os.MkdirAll(tmpDir, 0700) + Ω(err).ShouldNot(HaveOccurred()) + return FixtureManager{ + TmpDir: tmpDir, + FixturePath: "_fixtures", + } } -func copyIn(sourcePath, destinationPath string, recursive bool) { - err := os.MkdirAll(destinationPath, 0777) - Expect(err).NotTo(HaveOccurred()) +func (f FixtureManager) Cleanup() { + Ω(os.RemoveAll(f.TmpDir)).Should(Succeed()) +} - files, err := ioutil.ReadDir(sourcePath) +func (f FixtureManager) MountFixture(fixture string, subPackage ...string) { + src := filepath.Join(f.FixturePath, fixture+"_fixture") + dst := filepath.Join(f.TmpDir, fixture) + + if len(subPackage) > 0 { + src = filepath.Join(src, subPackage[0]) + dst = filepath.Join(dst, subPackage[0]) + } + + f.copyAndRewrite(src, dst) +} + +func (f FixtureManager) copyAndRewrite(src string, dst string) { + Expect(os.MkdirAll(dst, 0777)).To(Succeed()) + + files, err := ioutil.ReadDir(src) Expect(err).NotTo(HaveOccurred()) - for _, f := range files { - srcPath := filepath.Join(sourcePath, f.Name()) - dstPath := filepath.Join(destinationPath, f.Name()) - if f.IsDir() { - if recursive { - copyIn(srcPath, dstPath, recursive) - } + + for _, file := range files { + srcPath := filepath.Join(src, file.Name()) + dstPath := filepath.Join(dst, file.Name()) + if file.IsDir() { + f.copyAndRewrite(srcPath, dstPath) continue } - src, err := os.Open(srcPath) + srcContent, err := ioutil.ReadFile(srcPath) + Ω(err).ShouldNot(HaveOccurred()) + //rewrite import statements so that fixtures can work in the fixture folder when developing them, and in the tmp folder when under test + srcContent = bytes.ReplaceAll(srcContent, []byte("github.com/onsi/ginkgo/integration/_fixtures"), []byte(f.PackageRoot())) + srcContent = bytes.ReplaceAll(srcContent, []byte("_fixture"), []byte("")) + Ω(ioutil.WriteFile(dstPath, srcContent, 0666)).Should(Succeed()) + } +} + +func (f FixtureManager) PathTo(pkg string, target ...string) string { + if len(target) == 0 { + return filepath.Join(f.TmpDir, pkg) + } + return filepath.Join(f.TmpDir, pkg, target[0]) +} - Expect(err).NotTo(HaveOccurred()) - defer src.Close() +func (f FixtureManager) PathToFixtureFile(pkg string, target string) string { + return filepath.Join(f.FixturePath, pkg+"_fixture", target) +} - dst, err := os.Create(dstPath) - Expect(err).NotTo(HaveOccurred()) - defer dst.Close() +func (f FixtureManager) WriteFile(pkg string, target string, content string) { + dst := f.PathTo(pkg, target) + err := ioutil.WriteFile(dst, []byte(content), 0666) + Ω(err).ShouldNot(HaveOccurred()) +} - _, err = io.Copy(dst, src) - Expect(err).NotTo(HaveOccurred()) - } +func (f FixtureManager) ContentOf(pkg string, target string) string { + content, err := ioutil.ReadFile(f.PathTo(pkg, target)) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + return string(content) +} + +func (f FixtureManager) ContentOfFixture(pkg string, target string) string { + content, err := ioutil.ReadFile(f.PathToFixtureFile(pkg, target)) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + return string(content) +} + +func (f FixtureManager) RemoveFile(pkg string, target string) { + Expect(os.RemoveAll(f.PathTo(pkg, target))).To(Succeed()) +} + +func (f FixtureManager) PackageRoot() string { + return "github.com/onsi/ginkgo/integration/" + f.TmpDir +} + +func (f FixtureManager) PackageNameFor(target string) string { + return f.PackageRoot() + "/" + target } func sameFile(filePath, otherFilePath string) bool { @@ -126,11 +188,6 @@ func startGinkgo(dir string, args ...string) *gexec.Session { return session } -func removeSuccessfully(path string) { - err := os.RemoveAll(path) - Expect(err).NotTo(HaveOccurred()) -} - func raceDetectorSupported() bool { // https://github.com/golang/go/blob/1a370950/src/cmd/internal/sys/supported.go#L12 switch runtime.GOOS { diff --git a/integration/interrupt_test.go b/integration/interrupt_test.go index d4158b806c..3be6f14d92 100644 --- a/integration/interrupt_test.go +++ b/integration/interrupt_test.go @@ -10,31 +10,28 @@ import ( ) var _ = Describe("Interrupt", func() { - var pathToTest string BeforeEach(func() { - pathToTest = tmpPath("hanging") - copyIn(fixturePath("hanging_suite"), pathToTest, false) + fm.MountFixture("hanging") }) Context("when interrupting a suite", func() { var session *gexec.Session BeforeEach(func() { //we need to signal the actual process, so we must compile the test first - var err error - cmd := exec.Command("go", "test", "-c") - cmd.Dir = pathToTest - session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter) - Ω(err).ShouldNot(HaveOccurred()) + session = startGinkgo(fm.PathTo("hanging"), "build") Eventually(session).Should(gexec.Exit(0)) //then run the compiled test directly - cmd = exec.Command("./hanging.test", "--test.v=true", "--ginkgo.noColor") - cmd.Dir = pathToTest + cmd := exec.Command("./hanging.test", "--test.v", "--ginkgo.no-color") + cmd.Dir = fm.PathTo("hanging") + var err error session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter) Ω(err).ShouldNot(HaveOccurred()) Eventually(session).Should(gbytes.Say("Sleeping...")) session.Interrupt() + Eventually(session).Should(gbytes.Say("Sleeping again...")) + session.Interrupt() Eventually(session, 1000).Should(gexec.Exit(1)) }) @@ -44,7 +41,15 @@ var _ = Describe("Interrupt", func() { Ω(session).Should(gbytes.Say("Hanging Out")) }) - It("should run the AfterSuite", func() { + It("should report where the suite was interrupted", func() { + Ω(session).Should(gbytes.Say(`\[INTERRUPTED\]`)) + Ω(session).Should(gbytes.Say(`\[It\] .*hanging_test.go:24`)) + }) + + It("should run the AfterEach and the AfterSuite", func() { + Ω(session).Should(gbytes.Say("Cleaning up once...")) + Ω(session).Should(gbytes.Say("Cleaning up twice...")) + Ω(session).Should(gbytes.Say("Cleaning up thrice...")) Ω(session).Should(gbytes.Say("Heading Out After Suite")) }) }) diff --git a/integration/precompiled_test.go b/integration/precompiled_test.go index 55724a9b8d..2d0ab175b1 100644 --- a/integration/precompiled_test.go +++ b/integration/precompiled_test.go @@ -1,9 +1,7 @@ package integration_test import ( - "os" "os/exec" - "path/filepath" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -12,12 +10,9 @@ import ( ) var _ = Describe("ginkgo build", func() { - var pathToTest string - BeforeEach(func() { - pathToTest = tmpPath("passing_ginkgo_tests") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) - session := startGinkgo(pathToTest, "build") + fm.MountFixture("passing_ginkgo_tests") + session := startGinkgo(fm.PathTo("passing_ginkgo_tests"), "build") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("Compiling passing_ginkgo_tests")) @@ -25,13 +20,12 @@ var _ = Describe("ginkgo build", func() { }) It("should build a test binary", func() { - _, err := os.Stat(filepath.Join(pathToTest, "passing_ginkgo_tests.test")) - Ω(err).ShouldNot(HaveOccurred()) + Ω(fm.PathTo("passing_ginkgo_tests", "passing_ginkgo_tests.test")).Should(BeAnExistingFile()) }) It("should be possible to run the test binary directly", func() { cmd := exec.Command("./passing_ginkgo_tests.test") - cmd.Dir = pathToTest + cmd.Dir = fm.PathTo("passing_ginkgo_tests") session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) Ω(err).ShouldNot(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) @@ -39,15 +33,15 @@ var _ = Describe("ginkgo build", func() { }) It("should be possible to run the test binary via ginkgo", func() { - session := startGinkgo(pathToTest, "./passing_ginkgo_tests.test") + session := startGinkgo(fm.PathTo("passing_ginkgo_tests"), "./passing_ginkgo_tests.test") Eventually(session).Should(gexec.Exit(0)) Ω(session).Should(gbytes.Say("Running Suite: Passing_ginkgo_tests Suite")) }) It("should be possible to run the test binary in parallel", func() { - session := startGinkgo(pathToTest, "--nodes=4", "--noColor", "./passing_ginkgo_tests.test") + session := startGinkgo(fm.PathTo("passing_ginkgo_tests"), "--nodes=2", "--no-color", "./passing_ginkgo_tests.test") Eventually(session).Should(gexec.Exit(0)) Ω(session).Should(gbytes.Say("Running Suite: Passing_ginkgo_tests Suite")) - Ω(session).Should(gbytes.Say("Running in parallel across 4 nodes")) + Ω(session).Should(gbytes.Say("Running in parallel across 2 nodes")) }) }) diff --git a/integration/progress_test.go b/integration/progress_test.go index cda86b6ea5..2f6117a419 100644 --- a/integration/progress_test.go +++ b/integration/progress_test.go @@ -8,18 +8,16 @@ import ( ) var _ = Describe("Emitting progress", func() { - var pathToTest string var session *gexec.Session var args []string BeforeEach(func() { - args = []string{"--noColor"} - pathToTest = tmpPath("progress") - copyIn(fixturePath("progress_fixture"), pathToTest, false) + args = []string{"--no-color"} + fm.MountFixture("progress") }) JustBeforeEach(func() { - session = startGinkgo(pathToTest, args...) + session = startGinkgo(fm.PathTo("progress"), args...) Eventually(session).Should(gexec.Exit(0)) }) diff --git a/integration/run_test.go b/integration/run_test.go index c300e1c2c4..e6e5f32e51 100644 --- a/integration/run_test.go +++ b/integration/run_test.go @@ -2,7 +2,6 @@ package integration_test import ( "fmt" - "io/ioutil" "os" "regexp" "runtime" @@ -16,23 +15,18 @@ import ( ) var _ = Describe("Running Specs", func() { - var pathToTest string - - isWindows := (runtime.GOOS == "windows") denoter := "•" - - if isWindows { + if runtime.GOOS == "windows" { denoter = "+" } Context("when pointed at the current directory", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) + fm.MountFixture("passing_ginkgo_tests") }) It("should run the tests in the working directory", func() { - session := startGinkgo(pathToTest, "--noColor") + session := startGinkgo(fm.PathTo("passing_ginkgo_tests"), "--no-color") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -45,12 +39,11 @@ var _ = Describe("Running Specs", func() { Context("when passed an explicit package to run", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) + fm.MountFixture("passing_ginkgo_tests") }) It("should run the ginkgo style tests", func() { - session := startGinkgo(tmpDir, "--noColor", "ginkgo") + session := startGinkgo(fm.TmpDir, "--no-color", "passing_ginkgo_tests") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -63,14 +56,12 @@ var _ = Describe("Running Specs", func() { Context("when passed a number of packages to run", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - otherPathToTest := tmpPath("other") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) - copyIn(fixturePath("more_ginkgo_tests"), otherPathToTest, false) + fm.MountFixture("passing_ginkgo_tests") + fm.MountFixture("more_ginkgo_tests") }) It("should run the ginkgo style tests", func() { - session := startGinkgo(tmpDir, "--noColor", "--succinct=false", "ginkgo", "./other") + session := startGinkgo(fm.TmpDir, "--no-color", "--succinct=false", "passing_ginkgo_tests", "more_ginkgo_tests") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -82,16 +73,13 @@ var _ = Describe("Running Specs", func() { Context("when passed a number of packages to run, some of which have focused tests", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - otherPathToTest := tmpPath("other") - focusedPathToTest := tmpPath("focused") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) - copyIn(fixturePath("more_ginkgo_tests"), otherPathToTest, false) - copyIn(fixturePath("focused_fixture"), focusedPathToTest, false) + fm.MountFixture("passing_ginkgo_tests") + fm.MountFixture("more_ginkgo_tests") + fm.MountFixture("focused") }) It("should exit with a status code of 2 and explain why", func() { - session := startGinkgo(tmpDir, "--noColor", "--succinct=false", "-r") + session := startGinkgo(fm.TmpDir, "--no-color", "--succinct=false", "-r") Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) output := string(session.Out.Contents()) @@ -109,7 +97,7 @@ var _ = Describe("Running Specs", func() { os.Setenv("GINKGO_EDITOR_INTEGRATION", "") }) It("should exit with a status code of 0 to allow a coverage file to be generated", func() { - session := startGinkgo(tmpDir, "--noColor", "--succinct=false", "-r") + session := startGinkgo(fm.TmpDir, "--no-color", "--succinct=false", "-r") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -122,16 +110,13 @@ var _ = Describe("Running Specs", func() { Context("when told to skipPackages", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - otherPathToTest := tmpPath("other") - focusedPathToTest := tmpPath("focused") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) - copyIn(fixturePath("more_ginkgo_tests"), otherPathToTest, false) - copyIn(fixturePath("focused_fixture"), focusedPathToTest, false) + fm.MountFixture("passing_ginkgo_tests") + fm.MountFixture("more_ginkgo_tests") + fm.MountFixture("focused") }) It("should skip packages that match the list", func() { - session := startGinkgo(tmpDir, "--noColor", "--skipPackage=other,focused", "-r") + session := startGinkgo(fm.TmpDir, "--no-color", "--skip-package=more_ginkgo_tests,focused", "-r") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -143,11 +128,12 @@ var _ = Describe("Running Specs", func() { Context("when all packages are skipped", func() { It("should not run anything, but still exit 0", func() { - session := startGinkgo(tmpDir, "--noColor", "--skipPackage=other,focused,ginkgo", "-r") + session := startGinkgo(fm.TmpDir, "--no-color", "--skip-package=passing_ginkgo_tests,more_ginkgo_tests,focused", "-r") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) + errOutput := string(session.Err.Contents()) - Ω(output).Should(ContainSubstring("All tests skipped!")) + Ω(errOutput).Should(ContainSubstring("All tests skipped!")) Ω(output).ShouldNot(ContainSubstring("Passing_ginkgo_tests Suite")) Ω(output).ShouldNot(ContainSubstring("More_ginkgo_tests Suite")) Ω(output).ShouldNot(ContainSubstring("Focused_fixture Suite")) @@ -158,7 +144,7 @@ var _ = Describe("Running Specs", func() { Context("when there are no tests to run", func() { It("should exit 1", func() { - session := startGinkgo(tmpDir, "--noColor", "--skipPackage=other,focused", "-r") + session := startGinkgo(fm.TmpDir, "--no-color", "-r") Eventually(session).Should(gexec.Exit(1)) output := string(session.Err.Contents()) @@ -168,12 +154,11 @@ var _ = Describe("Running Specs", func() { Context("when there are test files but `go test` reports there are no tests to run", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - copyIn(fixturePath("no_test_fn"), pathToTest, false) + fm.MountFixture("no_test_fn") }) It("suggests running ginkgo bootstrap", func() { - session := startGinkgo(tmpDir, "--noColor", "--skipPackage=other,focused", "-r") + session := startGinkgo(fm.TmpDir, "--no-color", "-r") Eventually(session).Should(gexec.Exit(0)) output := string(session.Err.Contents()) @@ -181,7 +166,7 @@ var _ = Describe("Running Specs", func() { }) It("fails if told to requireSuite", func() { - session := startGinkgo(tmpDir, "--noColor", "--skipPackage=other,focused", "-r", "-requireSuite") + session := startGinkgo(fm.TmpDir, "--no-color", "-r", "-require-suite") Eventually(session).Should(gexec.Exit(1)) output := string(session.Err.Contents()) @@ -189,22 +174,20 @@ var _ = Describe("Running Specs", func() { }) }) - Context("when told to randomizeSuites", func() { + Context("when told to randomize-suites", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - otherPathToTest := tmpPath("other") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) - copyIn(fixturePath("more_ginkgo_tests"), otherPathToTest, false) + fm.MountFixture("passing_ginkgo_tests") + fm.MountFixture("more_ginkgo_tests") }) - It("should skip packages that match the regexp", func() { - session := startGinkgo(tmpDir, "--noColor", "--randomizeSuites", "-r", "--seed=2") + It("should mix up the order of the test suites", func() { + session := startGinkgo(fm.TmpDir, "--no-color", "--randomize-suites", "-r", "--seed=1") Eventually(session).Should(gexec.Exit(0)) Ω(session).Should(gbytes.Say("More_ginkgo_tests Suite")) Ω(session).Should(gbytes.Say("Passing_ginkgo_tests Suite")) - session = startGinkgo(tmpDir, "--noColor", "--randomizeSuites", "-r", "--seed=3") + session = startGinkgo(fm.TmpDir, "--no-color", "--randomize-suites", "-r", "--seed=4") Eventually(session).Should(gexec.Exit(0)) Ω(session).Should(gbytes.Say("Passing_ginkgo_tests Suite")) @@ -214,12 +197,11 @@ var _ = Describe("Running Specs", func() { Context("when pointed at a package with xunit style tests", func() { BeforeEach(func() { - pathToTest = tmpPath("xunit") - copyIn(fixturePath("xunit_tests"), pathToTest, false) + fm.MountFixture("xunit") }) It("should run the xunit style tests", func() { - session := startGinkgo(pathToTest) + session := startGinkgo(fm.PathTo("xunit")) Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -230,12 +212,11 @@ var _ = Describe("Running Specs", func() { Context("when pointed at a package with no tests", func() { BeforeEach(func() { - pathToTest = tmpPath("no_tests") - copyIn(fixturePath("no_tests"), pathToTest, false) + fm.MountFixture("no_tests") }) It("should fail", func() { - session := startGinkgo(pathToTest, "--noColor") + session := startGinkgo(fm.PathTo("no_tests"), "--no-color") Eventually(session).Should(gexec.Exit(1)) Ω(session.Err.Contents()).Should(ContainSubstring("Found no test suites")) @@ -244,12 +225,11 @@ var _ = Describe("Running Specs", func() { Context("when pointed at a package that fails to compile", func() { BeforeEach(func() { - pathToTest = tmpPath("does_not_compile") - copyIn(fixturePath("does_not_compile"), pathToTest, false) + fm.MountFixture("does_not_compile") }) It("should fail", func() { - session := startGinkgo(pathToTest, "--noColor") + session := startGinkgo(fm.PathTo("does_not_compile"), "--no-color") Eventually(session).Should(gexec.Exit(1)) output := string(session.Out.Contents()) @@ -259,24 +239,23 @@ var _ = Describe("Running Specs", func() { Context("when running in parallel", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) + fm.MountFixture("passing_ginkgo_tests") }) Context("with a specific number of -nodes", func() { It("should use the specified number of nodes", func() { - session := startGinkgo(pathToTest, "--noColor", "-succinct", "-nodes=2") + session := startGinkgo(fm.PathTo("passing_ginkgo_tests"), "--no-color", "-succinct", "-nodes=2") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) - Ω(output).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4 specs - 2 nodes [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]s`, regexp.QuoteMeta(denoter))) + Ω(output).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs - 2 nodes [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]s`, regexp.QuoteMeta(denoter))) Ω(output).Should(ContainSubstring("Test Suite Passed")) }) }) Context("with -p", func() { It("it should autocompute the number of nodes", func() { - session := startGinkgo(pathToTest, "--noColor", "-succinct", "-p") + session := startGinkgo(fm.PathTo("passing_ginkgo_tests"), "--no-color", "-succinct", "-p") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -287,28 +266,25 @@ var _ = Describe("Running Specs", func() { if nodes > 4 { nodes = nodes - 1 } - Ω(output).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4 specs - %d nodes [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]?s`, nodes, regexp.QuoteMeta(denoter))) + Ω(output).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs - %d nodes [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]?s`, nodes, regexp.QuoteMeta(denoter))) Ω(output).Should(ContainSubstring("Test Suite Passed")) }) }) }) - Context("when running in parallel with -debug", func() { + Context("when running in parallel with -debug-parallel", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - copyIn(fixturePath("debug_parallel_fixture"), pathToTest, false) + fm.MountFixture("debug_parallel") }) Context("without -v", func() { It("should emit node output to files on disk", func() { - session := startGinkgo(pathToTest, "--nodes=2", "--debug") + session := startGinkgo(fm.PathTo("debug_parallel"), "--nodes=2", "--debug-parallel") Eventually(session).Should(gexec.Exit(0)) - f0, err := ioutil.ReadFile(pathToTest + "/ginkgo-node-1.log") - Ω(err).ShouldNot(HaveOccurred()) - f1, err := ioutil.ReadFile(pathToTest + "/ginkgo-node-2.log") - Ω(err).ShouldNot(HaveOccurred()) - content := string(append(f0, f1...)) + f0 := fm.ContentOf("debug_parallel", "ginkgo-node-1.log") + f1 := fm.ContentOf("debug_parallel", "ginkgo-node-2.log") + content := f0 + f1 for i := 0; i < 10; i += 1 { Ω(content).Should(ContainSubstring("StdOut %d\n", i)) @@ -317,16 +293,14 @@ var _ = Describe("Running Specs", func() { }) }) - Context("without -v", func() { + Context("with -v", func() { It("should emit node output to files on disk, without duplicating the GinkgoWriter output", func() { - session := startGinkgo(pathToTest, "--nodes=2", "--debug", "-v") + session := startGinkgo(fm.PathTo("debug_parallel"), "--nodes=2", "--debug-parallel", "-v") Eventually(session).Should(gexec.Exit(0)) - f0, err := ioutil.ReadFile(pathToTest + "/ginkgo-node-1.log") - Ω(err).ShouldNot(HaveOccurred()) - f1, err := ioutil.ReadFile(pathToTest + "/ginkgo-node-2.log") - Ω(err).ShouldNot(HaveOccurred()) - content := string(append(f0, f1...)) + f0 := fm.ContentOf("debug_parallel", "ginkgo-node-1.log") + f1 := fm.ContentOf("debug_parallel", "ginkgo-node-2.log") + content := f0 + f1 out := strings.Split(content, "GinkgoWriter 2") Ω(out).Should(HaveLen(2)) @@ -334,55 +308,34 @@ var _ = Describe("Running Specs", func() { }) }) - Context("when streaming in parallel", func() { + Context("when running multiple tests", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) - }) - - It("should print output in realtime", func() { - session := startGinkgo(pathToTest, "--noColor", "-stream", "-nodes=2") - Eventually(session).Should(gexec.Exit(0)) - output := string(session.Out.Contents()) - - Ω(output).Should(ContainSubstring(`[1] Parallel test node 1/2.`)) - Ω(output).Should(ContainSubstring(`[2] Parallel test node 2/2.`)) - Ω(output).Should(ContainSubstring(`[1] SUCCESS!`)) - Ω(output).Should(ContainSubstring(`[2] SUCCESS!`)) - Ω(output).Should(ContainSubstring("Test Suite Passed")) - }) - }) - - Context("when running recursively", func() { - BeforeEach(func() { - passingTest := tmpPath("A") - otherPassingTest := tmpPath("E") - copyIn(fixturePath("passing_ginkgo_tests"), passingTest, false) - copyIn(fixturePath("more_ginkgo_tests"), otherPassingTest, false) + fm.MountFixture("passing_ginkgo_tests") + fm.MountFixture("more_ginkgo_tests") }) Context("when all the tests pass", func() { Context("with the -r flag", func() { It("should run all the tests (in succinct mode) and succeed", func() { - session := startGinkgo(tmpDir, "--noColor", "-r", ".") + session := startGinkgo(fm.TmpDir, "--no-color", "-r", ".") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) outputLines := strings.Split(output, "\n") - Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) - Ω(outputLines[1]).Should(MatchRegexp(`\[\d+\] More_ginkgo_tests Suite - 2/2 specs [%s]{2} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) + Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] More_ginkgo_tests Suite - 2/2 specs [%s]{2} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) + Ω(outputLines[1]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) Ω(output).Should(ContainSubstring("Test Suite Passed")) }) }) Context("with a trailing /...", func() { It("should run all the tests (in succinct mode) and succeed", func() { - session := startGinkgo(tmpDir, "--noColor", "./...") + session := startGinkgo(fm.TmpDir, "--no-color", "./...") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) outputLines := strings.Split(output, "\n") - Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) - Ω(outputLines[1]).Should(MatchRegexp(`\[\d+\] More_ginkgo_tests Suite - 2/2 specs [%s]{2} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) + Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] More_ginkgo_tests Suite - 2/2 specs [%s]{2} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) + Ω(outputLines[1]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) Ω(output).Should(ContainSubstring("Test Suite Passed")) }) }) @@ -390,78 +343,70 @@ var _ = Describe("Running Specs", func() { Context("when one of the packages has a failing tests", func() { BeforeEach(func() { - failingTest := tmpPath("C") - copyIn(fixturePath("failing_ginkgo_tests"), failingTest, false) + fm.MountFixture("failing_ginkgo_tests") }) It("should fail and stop running tests", func() { - session := startGinkgo(tmpDir, "--noColor", "-r") + session := startGinkgo(fm.TmpDir, "--no-color", "passing_ginkgo_tests", "failing_ginkgo_tests", "more_ginkgo_tests") Eventually(session).Should(gexec.Exit(1)) output := string(session.Out.Contents()) outputLines := strings.Split(output, "\n") Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) Ω(outputLines[1]).Should(MatchRegexp(`\[\d+\] Failing_ginkgo_tests Suite - 2/2 specs`)) - Ω(output).Should(ContainSubstring(fmt.Sprintf("%s Failure", denoter))) + Ω(output).Should(ContainSubstring(fmt.Sprintf("%s [FAILED]", denoter))) Ω(output).ShouldNot(ContainSubstring("More_ginkgo_tests Suite")) Ω(output).Should(ContainSubstring("Test Suite Failed")) - - Ω(output).Should(ContainSubstring("Summarizing 1 Failure:")) - Ω(output).Should(ContainSubstring("[Fail] FailingGinkgoTests [It] should fail")) }) }) Context("when one of the packages fails to compile", func() { BeforeEach(func() { - doesNotCompileTest := tmpPath("C") - copyIn(fixturePath("does_not_compile"), doesNotCompileTest, false) + fm.MountFixture("does_not_compile") }) It("should fail and stop running tests", func() { - session := startGinkgo(tmpDir, "--noColor", "-r") + session := startGinkgo(fm.TmpDir, "--no-color", "passing_ginkgo_tests", "does_not_compile", "more_ginkgo_tests") Eventually(session).Should(gexec.Exit(1)) output := string(session.Out.Contents()) outputLines := strings.Split(output, "\n") Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) - Ω(outputLines[1]).Should(ContainSubstring("Failed to compile C:")) + Ω(outputLines[1]).Should(ContainSubstring("Failed to compile does_not_compile:")) Ω(output).ShouldNot(ContainSubstring("More_ginkgo_tests Suite")) Ω(output).Should(ContainSubstring("Test Suite Failed")) }) }) - Context("when either is the case, but the keepGoing flag is set", func() { + Context("when either is the case, but the keep-going flag is set", func() { BeforeEach(func() { - doesNotCompileTest := tmpPath("B") - copyIn(fixturePath("does_not_compile"), doesNotCompileTest, false) - - failingTest := tmpPath("C") - copyIn(fixturePath("failing_ginkgo_tests"), failingTest, false) + fm.MountFixture("does_not_compile") + fm.MountFixture("failing_ginkgo_tests") }) It("should soldier on", func() { - session := startGinkgo(tmpDir, "--noColor", "-r", "-keepGoing") + session := startGinkgo(fm.TmpDir, "--no-color", "-keep-going", "passing_ginkgo_tests", "does_not_compile", "failing_ginkgo_tests", "more_ginkgo_tests") Eventually(session).Should(gexec.Exit(1)) output := string(session.Out.Contents()) outputLines := strings.Split(output, "\n") Ω(outputLines[0]).Should(MatchRegexp(`\[\d+\] Passing_ginkgo_tests Suite - 4/4 specs [%s]{4} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) - Ω(outputLines[1]).Should(ContainSubstring("Failed to compile B:")) + Ω(outputLines[1]).Should(ContainSubstring("Failed to compile does_not_compile:")) Ω(output).Should(MatchRegexp(`\[\d+\] Failing_ginkgo_tests Suite - 2/2 specs`)) - Ω(output).Should(ContainSubstring(fmt.Sprintf("%s Failure", denoter))) + Ω(output).Should(ContainSubstring(fmt.Sprintf("%s [FAILED]", denoter))) Ω(output).Should(MatchRegexp(`\[\d+\] More_ginkgo_tests Suite - 2/2 specs [%s]{2} SUCCESS! \d+(\.\d+)?[muµ]s PASS`, regexp.QuoteMeta(denoter))) Ω(output).Should(ContainSubstring("Test Suite Failed")) }) }) }) - Context("when told to keep going --untilItFails", func() { + Context("when told to keep going --until-it-fails", func() { BeforeEach(func() { - copyIn(fixturePath("eventually_failing"), tmpDir, false) + fm.MountFixture("eventually_failing") }) It("should keep rerunning the tests, until a failure occurs", func() { - session := startGinkgo(tmpDir, "--untilItFails", "--noColor") + session := startGinkgo(fm.PathTo("eventually_failing"), "--until-it-fails", "--no-color") Eventually(session).Should(gexec.Exit(1)) Ω(session).Should(gbytes.Say("This was attempt #1")) Ω(session).Should(gbytes.Say("This was attempt #2")) diff --git a/integration/skip_test.go b/integration/skip_test.go index f0fc9d5ee9..b0ea91e62e 100644 --- a/integration/skip_test.go +++ b/integration/skip_test.go @@ -7,15 +7,12 @@ import ( ) var _ = Describe("Skipping Specs", func() { - var pathToTest string - BeforeEach(func() { - pathToTest = tmpPath("skipping") - copyIn(fixturePath("skip_fixture"), pathToTest, false) + fm.MountFixture("skip") }) It("should skip in all the possible ways", func() { - session := startGinkgo(pathToTest, "--noColor") + session := startGinkgo(fm.PathTo("skip"), "--no-color") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -23,21 +20,14 @@ var _ = Describe("Skipping Specs", func() { Ω(output).Should(ContainSubstring("a top level skip on line 9")) Ω(output).Should(ContainSubstring("skip_fixture_test.go:9")) - Ω(output).Should(ContainSubstring("an async top level skip on line 14")) - Ω(output).Should(ContainSubstring("skip_fixture_test.go:14")) - Ω(output).Should(ContainSubstring("a top level goroutine skip on line 21")) - Ω(output).Should(ContainSubstring("skip_fixture_test.go:21")) Ω(output).Should(ContainSubstring("a sync SKIP")) - Ω(output).Should(ContainSubstring("an async SKIP")) - Ω(output).Should(ContainSubstring("a goroutine SKIP")) - Ω(output).Should(ContainSubstring("a measure SKIP")) - Ω(output).Should(ContainSubstring("S [SKIPPING] in Spec Setup (BeforeEach) [")) + Ω(output).Should(ContainSubstring("S [SKIPPED] [")) Ω(output).Should(ContainSubstring("a BeforeEach SKIP")) - Ω(output).Should(ContainSubstring("S [SKIPPING] in Spec Teardown (AfterEach) [")) + Ω(output).Should(ContainSubstring("S [SKIPPED] [")) Ω(output).Should(ContainSubstring("an AfterEach SKIP")) - Ω(output).Should(ContainSubstring("0 Passed | 0 Failed | 0 Pending | 9 Skipped")) + Ω(output).Should(ContainSubstring("0 Passed | 0 Failed | 0 Pending | 4 Skipped")) }) }) diff --git a/integration/subcommand_test.go b/integration/subcommand_test.go index b92e85fb4f..3753080b37 100644 --- a/integration/subcommand_test.go +++ b/integration/subcommand_test.go @@ -16,8 +16,8 @@ var _ = Describe("Subcommand", func() { Describe("ginkgo bootstrap", func() { var pkgPath string BeforeEach(func() { - pkgPath = tmpPath("foo") - os.Mkdir(pkgPath, 0777) + pkgPath = fm.TmpDir + "/foo" + Ω(os.Mkdir(pkgPath, 0777)).Should(Succeed()) }) It("should generate a bootstrap file, as long as one does not exist", func() { @@ -39,8 +39,9 @@ var _ = Describe("Subcommand", func() { session = startGinkgo(pkgPath, "bootstrap") Eventually(session).Should(gexec.Exit(1)) - output = session.Out.Contents() - Ω(output).Should(ContainSubstring("foo_suite_test.go already exists")) + output = session.Err.Contents() + Ω(output).Should(ContainSubstring("foo_suite_test.go")) + Ω(output).Should(ContainSubstring("already exists")) }) It("should import nodot declarations when told to", func() { @@ -64,25 +65,6 @@ var _ = Describe("Subcommand", func() { Ω(content).Should(ContainSubstring("\t" + `"github.com/onsi/gomega"`)) }) - It("should generate an agouti bootstrap file when told to", func() { - session := startGinkgo(pkgPath, "bootstrap", "--agouti") - Eventually(session).Should(gexec.Exit(0)) - output := session.Out.Contents() - - Ω(output).Should(ContainSubstring("foo_suite_test.go")) - - content, err := ioutil.ReadFile(filepath.Join(pkgPath, "foo_suite_test.go")) - Ω(err).ShouldNot(HaveOccurred()) - Ω(content).Should(ContainSubstring("package foo_test")) - Ω(content).Should(ContainSubstring("func TestFoo(t *testing.T) {")) - Ω(content).Should(ContainSubstring("RegisterFailHandler")) - Ω(content).Should(ContainSubstring("RunSpecs")) - - Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/ginkgo"`)) - Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/gomega"`)) - Ω(content).Should(ContainSubstring("\t" + `"github.com/sclevine/agouti"`)) - }) - It("should generate a bootstrap file using a template when told to", func() { templateFile := filepath.Join(pkgPath, ".bootstrap") ioutil.WriteFile(templateFile, []byte(`package {{.Package}} @@ -146,8 +128,8 @@ var _ = Describe("Subcommand", func() { Describe("nodot", func() { It("should update the declarations in the bootstrap file", func() { - pkgPath := tmpPath("foo") - os.Mkdir(pkgPath, 0777) + pkgPath := fm.TmpDir + "/foo" + Ω(os.Mkdir(pkgPath, 0777)).Should(Succeed()) session := startGinkgo(pkgPath, "bootstrap", "--nodot") Eventually(session).Should(gexec.Exit(0)) @@ -178,8 +160,8 @@ var _ = Describe("Subcommand", func() { var pkgPath string BeforeEach(func() { - pkgPath = tmpPath("foo_bar") - os.Mkdir(pkgPath, 0777) + pkgPath = fm.TmpDir + "/foo_bar" + Ω(os.Mkdir(pkgPath, 0777)).Should(Succeed()) }) Context("with no arguments", func() { @@ -199,9 +181,10 @@ var _ = Describe("Subcommand", func() { session = startGinkgo(pkgPath, "generate") Eventually(session).Should(gexec.Exit(1)) - output = session.Out.Contents() + output = session.Err.Contents() - Ω(output).Should(ContainSubstring("foo_bar_test.go already exists")) + Ω(output).Should(ContainSubstring("foo_bar_test.go")) + Ω(output).Should(ContainSubstring("already exists")) }) }) @@ -375,32 +358,13 @@ var _ = Describe("Subcommand", func() { Ω(content).ShouldNot(ContainSubstring("\t" + `. "github.com/onsi/gomega"`)) }) }) - - Context("with agouti", func() { - It("should generate an agouti test file", func() { - session := startGinkgo(pkgPath, "generate", "--agouti") - Eventually(session).Should(gexec.Exit(0)) - output := session.Out.Contents() - - Ω(output).Should(ContainSubstring("foo_bar_test.go")) - - content, err := ioutil.ReadFile(filepath.Join(pkgPath, "foo_bar_test.go")) - Ω(err).ShouldNot(HaveOccurred()) - Ω(content).Should(ContainSubstring("package foo_bar_test")) - Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/ginkgo"`)) - Ω(content).Should(ContainSubstring("\t" + `. "github.com/onsi/gomega"`)) - Ω(content).Should(ContainSubstring("\t" + `. "github.com/sclevine/agouti/matchers"`)) - Ω(content).Should(ContainSubstring("\t" + `"github.com/sclevine/agouti"`)) - Ω(content).Should(ContainSubstring("page, err = agoutiDriver.NewPage()")) - }) - }) }) Describe("ginkgo bootstrap/generate", func() { var pkgPath string BeforeEach(func() { - pkgPath = tmpPath("some-crazy-thing") - os.Mkdir(pkgPath, 0777) + pkgPath = fm.TmpDir + "/some-crazy-thing" + Ω(os.Mkdir(pkgPath, 0777)).Should(Succeed()) }) Context("when the working directory is empty", func() { @@ -448,15 +412,15 @@ var _ = Describe("Subcommand", func() { }) }) - Describe("Go module and sginkgo bootstrap/generate", func() { + Describe("Go module and ginkgo bootstrap/generate", func() { var ( pkgPath string savedGoPath string ) BeforeEach(func() { - pkgPath = tmpPath("myamazingmodule") - os.Mkdir(pkgPath, 0777) + pkgPath = fm.TmpDir + "/myamazingmodule" + Ω(os.Mkdir(pkgPath, 0777)).Should(Succeed()) Expect(ioutil.WriteFile(filepath.Join(pkgPath, "go.mod"), []byte("module fake.com/me/myamazingmodule\n"), 0777)).To(Succeed()) savedGoPath = os.Getenv("GOPATH") Expect(os.Setenv("GOPATH", "")).To(Succeed()) @@ -488,50 +452,49 @@ var _ = Describe("Subcommand", func() { }) }) - Describe("ginkgo blur", func() { + Describe("ginkgo unfocus", func() { It("should unfocus tests", func() { - pathToTest := tmpPath("focused") - fixture := fixturePath("focused_fixture") - copyIn(fixture, pathToTest, true) + fm.MountFixture("focused") - session := startGinkgo(pathToTest, "--noColor", "-r") + session := startGinkgo(fm.PathTo("focused"), "--no-color", "-r") Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE)) output := session.Out.Contents() Ω(string(output)).Should(ContainSubstring("Detected Programmatic Focus")) - session = startGinkgo(pathToTest, "blur") + session = startGinkgo(fm.PathTo("focused"), "unfocus") Eventually(session).Should(gexec.Exit(0)) output = session.Out.Contents() Ω(string(output)).ShouldNot(ContainSubstring("expected 'package'")) - session = startGinkgo(pathToTest, "--noColor", "-r") + session = startGinkgo(fm.PathTo("focused"), "--no-color", "-r") Eventually(session).Should(gexec.Exit(0)) output = session.Out.Contents() Ω(string(output)).Should(ContainSubstring("Ginkgo ran 2 suites")) Ω(string(output)).Should(ContainSubstring("Test Suite Passed")) Ω(string(output)).ShouldNot(ContainSubstring("Detected Programmatic Focus")) - Expect(sameFile(filepath.Join(pathToTest, "README.md"), filepath.Join(fixture, "README.md"))).To(BeTrue()) + original := fm.ContentOfFixture("focused", "README.md") + updated := fm.ContentOf("focused", "README.md") + Ω(original).Should(Equal(updated)) }) It("should ignore the 'vendor' folder", func() { - pathToTest := tmpPath("focused_fixture_with_vendor") - copyIn(fixturePath("focused_fixture_with_vendor"), pathToTest, true) + fm.MountFixture("focused_with_vendor") - session := startGinkgo(pathToTest, "blur") + session := startGinkgo(fm.PathTo("focused_with_vendor"), "unfocus") Eventually(session).Should(gexec.Exit(0)) - session = startGinkgo(pathToTest, "--noColor") + session = startGinkgo(fm.PathTo("focused_with_vendor"), "--no-color") Eventually(session).Should(gexec.Exit(0)) output := session.Out.Contents() - Expect(string(output)).To(ContainSubstring("13 Passed")) + Expect(string(output)).To(ContainSubstring("11 Passed")) Expect(string(output)).To(ContainSubstring("0 Skipped")) - vendorPath := fixturePath("focused_fixture_with_vendor/vendor") - otherVendorPath := filepath.Join(pathToTest, "vendor") + originalVendorPath := fm.PathToFixtureFile("focused_with_vendor", "vendor") + updatedVendorPath := fm.PathTo("focused_with_vendor", "vendor") - Expect(sameFolder(vendorPath, otherVendorPath)).To(BeTrue()) + Expect(sameFolder(originalVendorPath, updatedVendorPath)).To(BeTrue()) }) }) @@ -552,11 +515,18 @@ var _ = Describe("Subcommand", func() { output := string(session.Out.Contents()) Ω(output).Should(MatchRegexp(`Ginkgo Version \d+\.\d+\.\d+`)) - Ω(output).Should(ContainSubstring("ginkgo watch")) + Ω(output).Should(ContainSubstring("watch")) + Ω(output).Should(ContainSubstring("generate")) + Ω(output).Should(ContainSubstring("run")) + }) + + It("should print out usage information for subcommands", func() { + session := startGinkgo("", "help", "run") + Eventually(session).Should(gexec.Exit(0)) + output := string(session.Out.Contents()) + Ω(output).Should(ContainSubstring("-succinct")) Ω(output).Should(ContainSubstring("-nodes")) - Ω(output).Should(ContainSubstring("ginkgo generate")) - Ω(output).Should(ContainSubstring("ginkgo help ")) }) }) }) diff --git a/integration/suite_command_test.go b/integration/suite_command_test.go index 4aec1bc415..43de663b71 100644 --- a/integration/suite_command_test.go +++ b/integration/suite_command_test.go @@ -6,57 +6,54 @@ import ( "github.com/onsi/gomega/gexec" ) -var _ = Describe("Suite Command Specs", func() { - var pathToTest string - +var _ = Describe("After Run Hook Specs", func() { BeforeEach(func() { - pathToTest = tmpPath("suite_command") - copyIn(fixturePath("suite_command_tests"), pathToTest, false) + fm.MountFixture("after_run_hook") }) It("Runs command after suite echoing out suite data, properly reporting suite name and passing status in successful command output", func() { - command := "-afterSuiteHook=echo THIS IS A (ginkgo-suite-passed) TEST OF THE (ginkgo-suite-name) SYSTEM, THIS IS ONLY A TEST" - expected := "THIS IS A [PASS] TEST OF THE suite_command SYSTEM, THIS IS ONLY A TEST" - session := startGinkgo(pathToTest, command) + command := "-after-run-hook=echo THIS IS A (ginkgo-suite-passed) TEST OF THE (ginkgo-suite-name) SYSTEM, THIS IS ONLY A TEST" + expected := "THIS IS A [PASS] TEST OF THE after_run_hook SYSTEM, THIS IS ONLY A TEST" + session := startGinkgo(fm.PathTo("after_run_hook"), command) Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("1 Passed")) Ω(output).Should(ContainSubstring("0 Failed")) Ω(output).Should(ContainSubstring("1 Pending")) - Ω(output).Should(ContainSubstring("0 Skipped")) + Ω(output).Should(ContainSubstring("1 Skipped")) Ω(output).Should(ContainSubstring("Test Suite Passed")) - Ω(output).Should(ContainSubstring("Post-suite command succeeded:")) + Ω(output).Should(ContainSubstring("After-run-hook succeeded:")) Ω(output).Should(ContainSubstring(expected)) }) It("Runs command after suite reporting that command failed", func() { - command := "-afterSuiteHook=exit 1" - session := startGinkgo(pathToTest, command) + command := "-after-run-hook=exit 1" + session := startGinkgo(fm.PathTo("after_run_hook"), command) Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("1 Passed")) Ω(output).Should(ContainSubstring("0 Failed")) Ω(output).Should(ContainSubstring("1 Pending")) - Ω(output).Should(ContainSubstring("0 Skipped")) + Ω(output).Should(ContainSubstring("1 Skipped")) Ω(output).Should(ContainSubstring("Test Suite Passed")) - Ω(output).Should(ContainSubstring("Post-suite command failed:")) + Ω(output).Should(ContainSubstring("After-run-hook failed:")) }) It("Runs command after suite echoing out suite data, properly reporting suite name and failing status in successful command output", func() { - command := "-afterSuiteHook=echo THIS IS A (ginkgo-suite-passed) TEST OF THE (ginkgo-suite-name) SYSTEM, THIS IS ONLY A TEST" - expected := "THIS IS A [FAIL] TEST OF THE suite_command SYSTEM, THIS IS ONLY A TEST" - session := startGinkgo(pathToTest, "-failOnPending=true", command) + command := "-after-run-hook=echo THIS IS A (ginkgo-suite-passed) TEST OF THE (ginkgo-suite-name) SYSTEM, THIS IS ONLY A TEST" + expected := "THIS IS A [FAIL] TEST OF THE after_run_hook SYSTEM, THIS IS ONLY A TEST" + session := startGinkgo(fm.PathTo("after_run_hook"), "-fail-on-pending=true", command) Eventually(session).Should(gexec.Exit(1)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("1 Passed")) Ω(output).Should(ContainSubstring("0 Failed")) Ω(output).Should(ContainSubstring("1 Pending")) - Ω(output).Should(ContainSubstring("0 Skipped")) + Ω(output).Should(ContainSubstring("1 Skipped")) Ω(output).Should(ContainSubstring("Test Suite Failed")) - Ω(output).Should(ContainSubstring("Post-suite command succeeded:")) + Ω(output).Should(ContainSubstring("After-run-hook succeeded:")) Ω(output).Should(ContainSubstring(expected)) }) diff --git a/integration/suite_setup_test.go b/integration/suite_setup_test.go index 33ff5b9832..43d4ee4413 100644 --- a/integration/suite_setup_test.go +++ b/integration/suite_setup_test.go @@ -1,108 +1,20 @@ package integration_test import ( - "strings" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" ) var _ = Describe("SuiteSetup", func() { - var pathToTest string - - Context("when the BeforeSuite and AfterSuite pass", func() { - BeforeEach(func() { - pathToTest = tmpPath("suite_setup") - copyIn(fixturePath("passing_suite_setup"), pathToTest, false) - }) - - It("should run the BeforeSuite once, then run all the tests", func() { - session := startGinkgo(pathToTest, "--noColor") - Eventually(session).Should(gexec.Exit(0)) - output := string(session.Out.Contents()) - - Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(1)) - Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(1)) - }) - - It("should run the BeforeSuite once per parallel node, then run all the tests", func() { - session := startGinkgo(pathToTest, "--noColor", "--nodes=2") - Eventually(session).Should(gexec.Exit(0)) - output := string(session.Out.Contents()) - - Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(2)) - Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(2)) - }) - }) - - Context("when the BeforeSuite fails", func() { - BeforeEach(func() { - pathToTest = tmpPath("suite_setup") - copyIn(fixturePath("failing_before_suite"), pathToTest, false) - }) - - It("should run the BeforeSuite once, none of the tests, but it should run the AfterSuite", func() { - session := startGinkgo(pathToTest, "--noColor") - Eventually(session).Should(gexec.Exit(1)) - output := string(session.Out.Contents()) - - Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(1)) - Ω(strings.Count(output, "Test Panicked")).Should(Equal(1)) - Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(1)) - Ω(output).ShouldNot(ContainSubstring("NEVER SEE THIS")) - }) - - It("should run the BeforeSuite once per parallel node, none of the tests, but it should run the AfterSuite for each node", func() { - session := startGinkgo(pathToTest, "--noColor", "--nodes=2") - Eventually(session).Should(gexec.Exit(1)) - output := string(session.Out.Contents()) - - Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(2)) - Ω(strings.Count(output, "Test Panicked")).Should(Equal(2)) - Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(2)) - Ω(output).ShouldNot(ContainSubstring("NEVER SEE THIS")) - }) - }) - - Context("when the AfterSuite fails", func() { - BeforeEach(func() { - pathToTest = tmpPath("suite_setup") - copyIn(fixturePath("failing_after_suite"), pathToTest, false) - }) - - It("should run the BeforeSuite once, none of the tests, but it should run the AfterSuite", func() { - session := startGinkgo(pathToTest, "--noColor") - Eventually(session).Should(gexec.Exit(1)) - output := string(session.Out.Contents()) - - Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(1)) - Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(1)) - Ω(strings.Count(output, "Test Panicked")).Should(Equal(1)) - Ω(strings.Count(output, "A TEST")).Should(Equal(2)) - }) - - It("should run the BeforeSuite once per parallel node, none of the tests, but it should run the AfterSuite for each node", func() { - session := startGinkgo(pathToTest, "--noColor", "--nodes=2") - Eventually(session).Should(gexec.Exit(1)) - output := string(session.Out.Contents()) - - Ω(strings.Count(output, "BEFORE SUITE")).Should(Equal(2)) - Ω(strings.Count(output, "AFTER SUITE")).Should(Equal(2)) - Ω(strings.Count(output, "Test Panicked")).Should(Equal(2)) - Ω(strings.Count(output, "A TEST")).Should(Equal(2)) - }) - }) - Context("With passing synchronized before and after suites", func() { BeforeEach(func() { - pathToTest = tmpPath("suite_setup") - copyIn(fixturePath("synchronized_setup_tests"), pathToTest, false) + fm.MountFixture("synchronized_setup_tests") }) Context("when run with one node", func() { It("should do all the work on that one node", func() { - session := startGinkgo(pathToTest, "--noColor") + session := startGinkgo(fm.PathTo("synchronized_setup_tests"), "--no-color") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -113,7 +25,7 @@ var _ = Describe("SuiteSetup", func() { Context("when run across multiple nodes", func() { It("should run the first BeforeSuite function (BEFORE_A) on node 1, the second (BEFORE_B) on all the nodes, the first AfterSuite (AFTER_A) on all the nodes, and then the second (AFTER_B) on Node 1 *after* everything else is finished", func() { - session := startGinkgo(pathToTest, "--noColor", "--nodes=3") + session := startGinkgo(fm.PathTo("synchronized_setup_tests"), "--no-color", "--nodes=3") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) @@ -134,45 +46,20 @@ var _ = Describe("SuiteSetup", func() { Ω(output).ShouldNot(ContainSubstring("AFTER_B_3")) }) }) - - Context("when streaming across multiple nodes", func() { - It("should run the first BeforeSuite function (BEFORE_A) on node 1, the second (BEFORE_B) on all the nodes, the first AfterSuite (AFTER_A) on all the nodes, and then the second (AFTER_B) on Node 1 *after* everything else is finished", func() { - session := startGinkgo(pathToTest, "--noColor", "--nodes=3", "--stream") - Eventually(session).Should(gexec.Exit(0)) - output := string(session.Out.Contents()) - - Ω(output).Should(ContainSubstring("[1] BEFORE_A_1")) - Ω(output).Should(ContainSubstring("[1] BEFORE_B_1: DATA")) - Ω(output).Should(ContainSubstring("[2] BEFORE_B_2: DATA")) - Ω(output).Should(ContainSubstring("[3] BEFORE_B_3: DATA")) - - Ω(output).ShouldNot(ContainSubstring("BEFORE_A_2")) - Ω(output).ShouldNot(ContainSubstring("BEFORE_A_3")) - - Ω(output).Should(ContainSubstring("[1] AFTER_A_1")) - Ω(output).Should(ContainSubstring("[2] AFTER_A_2")) - Ω(output).Should(ContainSubstring("[3] AFTER_A_3")) - Ω(output).Should(ContainSubstring("[1] AFTER_B_1")) - - Ω(output).ShouldNot(ContainSubstring("AFTER_B_2")) - Ω(output).ShouldNot(ContainSubstring("AFTER_B_3")) - }) - }) }) Context("With a failing synchronized before suite", func() { BeforeEach(func() { - pathToTest = tmpPath("suite_setup") - copyIn(fixturePath("exiting_synchronized_setup_tests"), pathToTest, false) + fm.MountFixture("exiting_synchronized_setup") }) It("should fail and let the user know that node 1 disappeared prematurely", func() { - session := startGinkgo(pathToTest, "--noColor", "--nodes=3") + session := startGinkgo(fm.PathTo("exiting_synchronized_setup"), "--no-color", "--nodes=3") Eventually(session).Should(gexec.Exit(1)) - output := string(session.Out.Contents()) + output := string(session.Out.Contents()) + string(session.Err.Contents()) - Ω(output).Should(ContainSubstring("Node 1 disappeared before completing BeforeSuite")) - Ω(output).Should(ContainSubstring("Ginkgo timed out waiting for all parallel nodes to report back!")) + Ω(output).Should(ContainSubstring("SynchronizedBeforeSuite on Node 1 disappeared before it could report back")) + Ω(output).Should(ContainSubstring("Ginkgo timed out waiting for all parallel nodes to report back")) }) }) }) diff --git a/integration/tags_test.go b/integration/tags_test.go index fc2ff5e5cd..e598d818ba 100644 --- a/integration/tags_test.go +++ b/integration/tags_test.go @@ -7,19 +7,17 @@ import ( ) var _ = Describe("Tags", func() { - var pathToTest string BeforeEach(func() { - pathToTest = tmpPath("tags") - copyIn(fixturePath("tags_tests"), pathToTest, false) + fm.MountFixture("tags") }) It("should honor the passed in -tags flag", func() { - session := startGinkgo(pathToTest, "--noColor") + session := startGinkgo(fm.PathTo("tags"), "--no-color") Eventually(session).Should(gexec.Exit(0)) output := string(session.Out.Contents()) Ω(output).Should(ContainSubstring("Ran 1 of 1 Specs")) - session = startGinkgo(pathToTest, "--noColor", "-tags=complex_tests") + session = startGinkgo(fm.PathTo("tags"), "--no-color", "-tags=complex_tests") Eventually(session).Should(gexec.Exit(0)) output = string(session.Out.Contents()) Ω(output).Should(ContainSubstring("Ran 3 of 3 Specs")) diff --git a/integration/test_description_test.go b/integration/test_description_test.go deleted file mode 100644 index 6739871fbf..0000000000 --- a/integration/test_description_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package integration_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/gbytes" - "github.com/onsi/gomega/gexec" -) - -var _ = Describe("TestDescription", func() { - var pathToTest string - - BeforeEach(func() { - pathToTest = tmpPath("test_description") - copyIn(fixturePath("test_description"), pathToTest, false) - }) - - It("should capture and emit information about the current test", func() { - session := startGinkgo(pathToTest, "--noColor") - Eventually(session).Should(gexec.Exit(1)) - - Ω(session).Should(gbytes.Say("TestDescription should pass:false")) - Ω(session).Should(gbytes.Say("TestDescription should fail:true")) - }) -}) diff --git a/integration/verbose_and_succinct_test.go b/integration/verbose_and_succinct_test.go index 9b78f0b6ca..9f17224b63 100644 --- a/integration/verbose_and_succinct_test.go +++ b/integration/verbose_and_succinct_test.go @@ -10,24 +10,19 @@ import ( ) var _ = Describe("Verbose And Succinct Mode", func() { - var pathToTest string - var otherPathToTest string - - isWindows := (runtime.GOOS == "windows") denoter := "•" - if isWindows { + if runtime.GOOS == "windows" { denoter = "+" } Context("when running one package", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) + fm.MountFixture("passing_ginkgo_tests") }) It("should default to non-succinct mode", func() { - session := startGinkgo(pathToTest, "--noColor") + session := startGinkgo(fm.PathTo("passing_ginkgo_tests"), "--no-color") Eventually(session).Should(gexec.Exit(0)) output := session.Out.Contents() @@ -37,15 +32,13 @@ var _ = Describe("Verbose And Succinct Mode", func() { Context("when running more than one package", func() { BeforeEach(func() { - pathToTest = tmpPath("ginkgo") - copyIn(fixturePath("passing_ginkgo_tests"), pathToTest, false) - otherPathToTest = tmpPath("more_ginkgo") - copyIn(fixturePath("more_ginkgo_tests"), otherPathToTest, false) + fm.MountFixture("passing_ginkgo_tests") + fm.MountFixture("more_ginkgo_tests") }) Context("with no flags set", func() { It("should default to succinct mode", func() { - session := startGinkgo(tmpDir, "--noColor", "ginkgo", "more_ginkgo") + session := startGinkgo(fm.TmpDir, "--no-color", "passing_ginkgo_tests", "more_ginkgo_tests") Eventually(session).Should(gexec.Exit(0)) output := session.Out.Contents() @@ -56,7 +49,7 @@ var _ = Describe("Verbose And Succinct Mode", func() { Context("with --succinct=false", func() { It("should not be in succinct mode", func() { - session := startGinkgo(tmpDir, "--noColor", "--succinct=false", "ginkgo", "more_ginkgo") + session := startGinkgo(fm.TmpDir, "--no-color", "--succinct=false", "passing_ginkgo_tests", "more_ginkgo_tests") Eventually(session).Should(gexec.Exit(0)) output := session.Out.Contents() @@ -67,7 +60,7 @@ var _ = Describe("Verbose And Succinct Mode", func() { Context("with -v", func() { It("should not be in succinct mode, but should be verbose", func() { - session := startGinkgo(tmpDir, "--noColor", "-v", "ginkgo", "more_ginkgo") + session := startGinkgo(fm.TmpDir, "--no-color", "-v", "passing_ginkgo_tests", "more_ginkgo_tests") Eventually(session).Should(gexec.Exit(0)) output := session.Out.Contents() @@ -78,7 +71,7 @@ var _ = Describe("Verbose And Succinct Mode", func() { }) It("should emit output from Bys", func() { - session := startGinkgo(tmpDir, "--noColor", "-v", "ginkgo") + session := startGinkgo(fm.PathTo("passing_ginkgo_tests"), "--no-color", "-v") Eventually(session).Should(gexec.Exit(0)) output := session.Out.Contents() diff --git a/integration/watch_test.go b/integration/watch_test.go index 6bbbe65b71..f4e826b1d8 100644 --- a/integration/watch_test.go +++ b/integration/watch_test.go @@ -2,9 +2,7 @@ package integration_test import ( "io/ioutil" - "os" "path/filepath" - "strings" "time" . "github.com/onsi/ginkgo" @@ -14,53 +12,12 @@ import ( ) var _ = Describe("Watch", func() { - var rootPath string - var pathA string - var pathB string - var pathC string var session *gexec.Session - fixUpImportPath := func(path string) { - files, err := ioutil.ReadDir(path) - Expect(err).NotTo(HaveOccurred()) - - for _, f := range files { - filePath := filepath.Join(path, f.Name()) - - r, err := os.Open(filePath) - Ω(err).ShouldNot(HaveOccurred()) - - src, err := ioutil.ReadAll(r) - Ω(err).ShouldNot(HaveOccurred()) - out := strings.ReplaceAll(string(src), "$ROOT_PATH$", "github.com/onsi/ginkgo/integration/"+rootPath) - r.Close() - - err = ioutil.WriteFile(filePath, []byte(out), 0666) - Ω(err).ShouldNot(HaveOccurred()) - } - } - BeforeEach(func() { - rootPath = tmpPath("root") - pathA = filepath.Join(rootPath, "A") - pathB = filepath.Join(rootPath, "B") - pathC = filepath.Join(rootPath, "C") - - err := os.MkdirAll(pathA, 0700) - Ω(err).ShouldNot(HaveOccurred()) - - err = os.MkdirAll(pathB, 0700) - Ω(err).ShouldNot(HaveOccurred()) - - err = os.MkdirAll(pathC, 0700) - Ω(err).ShouldNot(HaveOccurred()) - - copyIn(fixturePath(filepath.Join("watch_fixtures", "A")), pathA, false) - fixUpImportPath(pathA) - copyIn(fixturePath(filepath.Join("watch_fixtures", "B")), pathB, false) - fixUpImportPath(pathB) - copyIn(fixturePath(filepath.Join("watch_fixtures", "C")), pathC, false) - fixUpImportPath(pathC) + fm.MountFixture("watch", "A") + fm.MountFixture("watch", "B") + fm.MountFixture("watch", "C") }) modifyFile := func(path string) { @@ -73,15 +30,18 @@ var _ = Describe("Watch", func() { } modifyCode := func(pkgToModify string) { - modifyFile(filepath.Join(rootPath, pkgToModify, pkgToModify+".go")) + path := filepath.Join(pkgToModify, pkgToModify+".go") + modifyFile(fm.PathTo("watch", path)) } modifyJSON := func(pkgToModify string) { - modifyFile(filepath.Join(rootPath, pkgToModify, pkgToModify+".json")) + path := filepath.Join(pkgToModify, pkgToModify+".json") + modifyFile(fm.PathTo("watch", path)) } modifyTest := func(pkgToModify string) { - modifyFile(filepath.Join(rootPath, pkgToModify, pkgToModify+"_test.go")) + path := filepath.Join(pkgToModify, pkgToModify+"_test.go") + modifyFile(fm.PathTo("watch", path)) } AfterEach(func() { @@ -91,7 +51,7 @@ var _ = Describe("Watch", func() { }) It("should be set up correctly", func() { - session = startGinkgo(rootPath, "-r") + session = startGinkgo(fm.PathTo("watch"), "-r") Eventually(session).Should(gexec.Exit(0)) Ω(session.Out.Contents()).Should(ContainSubstring("A Suite")) Ω(session.Out.Contents()).Should(ContainSubstring("B Suite")) @@ -101,7 +61,7 @@ var _ = Describe("Watch", func() { Context("when watching just one test suite", func() { It("should immediately run, and should rerun when the test suite changes", func() { - session = startGinkgo(rootPath, "watch", "-succinct", "A") + session = startGinkgo(fm.PathTo("watch"), "watch", "-succinct", "A") Eventually(session).Should(gbytes.Say("A Suite")) modifyCode("A") Eventually(session).Should(gbytes.Say("Detected changes in")) @@ -112,7 +72,7 @@ var _ = Describe("Watch", func() { Context("when watching several test suites", func() { It("should not immediately run, but should rerun a test when its code changes", func() { - session = startGinkgo(rootPath, "watch", "-succinct", "-r") + session = startGinkgo(fm.PathTo("watch"), "watch", "-succinct", "-r") Eventually(session).Should(gbytes.Say("Identified 3 test suites")) Consistently(session).ShouldNot(gbytes.Say("A Suite|B Suite|C Suite")) modifyCode("A") @@ -126,7 +86,7 @@ var _ = Describe("Watch", func() { Describe("watching dependencies", func() { Context("with a depth of 2", func() { It("should watch down to that depth", func() { - session = startGinkgo(rootPath, "watch", "-succinct", "-r", "-depth=2") + session = startGinkgo(fm.PathTo("watch"), "watch", "-succinct", "-r", "-depth=2") Eventually(session).Should(gbytes.Say("Identified 3 test suites")) Eventually(session).Should(gbytes.Say(`A \[`)) Eventually(session).Should(gbytes.Say(`B \[`)) @@ -153,7 +113,7 @@ var _ = Describe("Watch", func() { Context("with a depth of 1", func() { It("should watch down to that depth", func() { - session = startGinkgo(rootPath, "watch", "-succinct", "-r", "-depth=1") + session = startGinkgo(fm.PathTo("watch"), "watch", "-succinct", "-r", "-depth=1") Eventually(session).Should(gbytes.Say("Identified 3 test suites")) Eventually(session).Should(gbytes.Say(`A \[`)) Eventually(session).Should(gbytes.Say(`B \[`)) @@ -180,7 +140,7 @@ var _ = Describe("Watch", func() { Context("with a depth of 0", func() { It("should not watch any dependencies", func() { - session = startGinkgo(rootPath, "watch", "-succinct", "-r", "-depth=0") + session = startGinkgo(fm.PathTo("watch"), "watch", "-succinct", "-r", "-depth=0") Eventually(session).Should(gbytes.Say("Identified 3 test suites")) Eventually(session).Should(gbytes.Say(`A \[`)) Eventually(session).Should(gbytes.Say(`B \[`)) @@ -204,7 +164,7 @@ var _ = Describe("Watch", func() { }) It("should not trigger dependents when tests are changed", func() { - session = startGinkgo(rootPath, "watch", "-succinct", "-r", "-depth=2") + session = startGinkgo(fm.PathTo("watch"), "watch", "-succinct", "-r", "-depth=2") Eventually(session).Should(gbytes.Say("Identified 3 test suites")) Eventually(session).Should(gbytes.Say(`A \[`)) Eventually(session).Should(gbytes.Say(`B \[`)) @@ -230,7 +190,7 @@ var _ = Describe("Watch", func() { Describe("adjusting the watch regular expression", func() { Describe("the default regular expression", func() { It("should only trigger when go files are changed", func() { - session = startGinkgo(rootPath, "watch", "-succinct", "-r", "-depth=2") + session = startGinkgo(fm.PathTo("watch"), "watch", "-succinct", "-r", "-depth=2") Eventually(session).Should(gbytes.Say("Identified 3 test suites")) Eventually(session).Should(gbytes.Say(`A \[`)) Eventually(session).Should(gbytes.Say(`B \[`)) @@ -244,7 +204,7 @@ var _ = Describe("Watch", func() { Describe("modifying the regular expression", func() { It("should trigger if the regexp matches", func() { - session = startGinkgo(rootPath, "watch", "-succinct", "-r", "-depth=2", `-watchRegExp=\.json$`) + session = startGinkgo(fm.PathTo("watch"), "watch", "-succinct", "-r", "-depth=2", `-watch-regexp=\.json$`) Eventually(session).Should(gbytes.Say("Identified 3 test suites")) Eventually(session).Should(gbytes.Say(`A \[`)) Eventually(session).Should(gbytes.Say(`B \[`)) @@ -261,17 +221,10 @@ var _ = Describe("Watch", func() { Describe("when new test suite is added", func() { It("should start monitoring that test suite", func() { - session = startGinkgo(rootPath, "watch", "-succinct", "-r") - + session = startGinkgo(fm.PathTo("watch"), "watch", "-succinct", "-r", "-depth=1") Eventually(session).Should(gbytes.Say("Watching 3 suites")) - pathD := filepath.Join(rootPath, "D") - - err := os.MkdirAll(pathD, 0700) - Ω(err).ShouldNot(HaveOccurred()) - - copyIn(fixturePath(filepath.Join("watch_fixtures", "D")), pathD, false) - fixUpImportPath(pathD) + fm.MountFixture("watch", "D") Eventually(session).Should(gbytes.Say("Detected 1 new suite")) Eventually(session).Should(gbytes.Say(`D \[`)) diff --git a/internal/codelocation/code_location.go b/internal/codelocation/code_location.go deleted file mode 100644 index aa89d6cba8..0000000000 --- a/internal/codelocation/code_location.go +++ /dev/null @@ -1,48 +0,0 @@ -package codelocation - -import ( - "regexp" - "runtime" - "runtime/debug" - "strings" - - "github.com/onsi/ginkgo/types" -) - -func New(skip int) types.CodeLocation { - _, file, line, _ := runtime.Caller(skip + 1) - stackTrace := PruneStack(string(debug.Stack()), skip+1) - return types.CodeLocation{FileName: file, LineNumber: line, FullStackTrace: stackTrace} -} - -// PruneStack removes references to functions that are internal to Ginkgo -// and the Go runtime from a stack string and a certain number of stack entries -// at the beginning of the stack. The stack string has the format -// as returned by runtime/debug.Stack. The leading goroutine information is -// optional and always removed if present. Beware that runtime/debug.Stack -// adds itself as first entry, so typically skip must be >= 1 to remove that -// entry. -func PruneStack(fullStackTrace string, skip int) string { - stack := strings.Split(fullStackTrace, "\n") - // Ensure that the even entries are the method names and the - // the odd entries the source code information. - if len(stack) > 0 && strings.HasPrefix(stack[0], "goroutine ") { - // Ignore "goroutine 29 [running]:" line. - stack = stack[1:] - } - // The "+1" is for skipping over the initial entry, which is - // runtime/debug.Stack() itself. - if len(stack) > 2*(skip+1) { - stack = stack[2*(skip+1):] - } - prunedStack := []string{} - re := regexp.MustCompile(`\/ginkgo\/|\/pkg\/testing\/|\/pkg\/runtime\/`) - for i := 0; i < len(stack)/2; i++ { - // We filter out based on the source code file name. - if !re.Match([]byte(stack[i*2+1])) { - prunedStack = append(prunedStack, stack[i*2]) - prunedStack = append(prunedStack, stack[i*2+1]) - } - } - return strings.Join(prunedStack, "\n") -} diff --git a/internal/codelocation/code_location_suite_test.go b/internal/codelocation/code_location_suite_test.go deleted file mode 100644 index f06abf3c56..0000000000 --- a/internal/codelocation/code_location_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package codelocation_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestCodelocation(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "CodeLocation Suite") -} diff --git a/internal/codelocation/code_location_test.go b/internal/codelocation/code_location_test.go deleted file mode 100644 index 850d0d2b9f..0000000000 --- a/internal/codelocation/code_location_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package codelocation_test - -import ( - "fmt" - "runtime" - "runtime/debug" - "strings" - - . "github.com/onsi/ginkgo" - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/types" - . "github.com/onsi/gomega" -) - -var ( - codeLocation types.CodeLocation - expectedFileName string - expectedLineNumber int - fullStackTrace string -) - -func caller0() { - fullStackTrace = string(debug.Stack()) - codeLocation = codelocation.New(1) -} - -func caller1() { - _, expectedFileName, expectedLineNumber, _ = runtime.Caller(0) - expectedLineNumber += 2 - caller0() -} - -var _ = Describe("CodeLocation", func() { - BeforeEach(func() { - caller1() - }) - - It("should use the passed in skip parameter to pick out the correct file & line number", func() { - Ω(codeLocation.FileName).Should(Equal(expectedFileName)) - Ω(codeLocation.LineNumber).Should(Equal(expectedLineNumber)) - }) - - Describe("stringer behavior", func() { - It("should stringify nicely", func() { - Ω(codeLocation.String()).Should(ContainSubstring("code_location_test.go:%d", expectedLineNumber)) - }) - }) - - Describe("PruneStack", func() { - It("should remove any references to ginkgo and pkg/testing and pkg/runtime", func() { - // Hard-coded string, loosely based on what debug.Stack() produces. - input := `Skip: skip() -/Skip/me -Skip: skip() -/Skip/me -Something: Func() -/Users/whoever/gospace/src/github.com/onsi/ginkgo/whatever.go:10 (0x12314) -SomethingInternalToGinkgo: Func() -/Users/whoever/gospace/src/github.com/onsi/ginkgo/whatever_else.go:10 (0x12314) -Oops: BlowUp() -/usr/goroot/pkg/strings/oops.go:10 (0x12341) -MyCode: Func() -/Users/whoever/gospace/src/mycode/code.go:10 (0x12341) -MyCodeTest: Func() -/Users/whoever/gospace/src/mycode/code_test.go:10 (0x12341) -TestFoo: RunSpecs(t, "Foo Suite") -/Users/whoever/gospace/src/mycode/code_suite_test.go:12 (0x37f08) -TestingT: Blah() -/usr/goroot/pkg/testing/testing.go:12 (0x37f08) -Something: Func() -/usr/goroot/pkg/runtime/runtime.go:12 (0x37f08) -` - prunedStack := codelocation.PruneStack(input, 1) - Ω(prunedStack).Should(Equal(`Oops: BlowUp() -/usr/goroot/pkg/strings/oops.go:10 (0x12341) -MyCode: Func() -/Users/whoever/gospace/src/mycode/code.go:10 (0x12341) -MyCodeTest: Func() -/Users/whoever/gospace/src/mycode/code_test.go:10 (0x12341) -TestFoo: RunSpecs(t, "Foo Suite") -/Users/whoever/gospace/src/mycode/code_suite_test.go:12 (0x37f08)`)) - }) - - It("should skip correctly for a Go runtime stack", func() { - // Actual string from debug.Stack(), something like: - // "goroutine 5 [running]:", - // "runtime/debug.Stack(0x0, 0x0, 0x0)", - // "\t/nvme/gopath/go/src/runtime/debug/stack.go:24 +0xa1", - // "github.com/onsi/ginkgo/internal/codelocation_test.caller0()", - // "\t/work/gopath.ginkgo/src/github.com/onsi/XXXXXX/internal/codeloc...+36 more", - // "github.com/onsi/ginkgo/internal/codelocation_test.caller1()", - // "\t/work/gopath.ginkgo/src/github.com/onsi/XXXXXX/internal/codeloc...+36 more", - // "github.com/onsi/ginkgo/internal/codelocation_test.glob..func1.1(...+1 more", - // "\t/work/gopath.ginkgo/src/github.com/onsi/XXXXXX/internal/codeloc...+36 more", - // - // To avoid pruning of our test functions - // above, we change the expected filename (= this file) into - // something that isn't special for PruneStack(). - fakeFileName := "github.com/onsi/XXXXXX/internal/codelocation/code_location_test.go" - mangledStackTrace := strings.Replace(fullStackTrace, - expectedFileName, - fakeFileName, - -1) - stack := strings.Split(codelocation.PruneStack(mangledStackTrace, 1), "\n") - Ω(len(stack)).To(BeNumerically(">=", 2), "not enough entries in stack: %s", stack) - Ω(stack[0]).To(Equal("github.com/onsi/ginkgo/internal/codelocation_test.caller1()")) - Ω(strings.TrimLeft(stack[1], " \t")).To(HavePrefix(fmt.Sprintf("%s:%d ", fakeFileName, expectedLineNumber))) - }) - }) -}) diff --git a/internal/containernode/container_node.go b/internal/containernode/container_node.go deleted file mode 100644 index 0737746dcf..0000000000 --- a/internal/containernode/container_node.go +++ /dev/null @@ -1,151 +0,0 @@ -package containernode - -import ( - "math/rand" - "sort" - - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/types" -) - -type subjectOrContainerNode struct { - containerNode *ContainerNode - subjectNode leafnodes.SubjectNode -} - -func (n subjectOrContainerNode) text() string { - if n.containerNode != nil { - return n.containerNode.Text() - } else { - return n.subjectNode.Text() - } -} - -type CollatedNodes struct { - Containers []*ContainerNode - Subject leafnodes.SubjectNode -} - -type ContainerNode struct { - text string - flag types.FlagType - codeLocation types.CodeLocation - - setupNodes []leafnodes.BasicNode - subjectAndContainerNodes []subjectOrContainerNode -} - -func New(text string, flag types.FlagType, codeLocation types.CodeLocation) *ContainerNode { - return &ContainerNode{ - text: text, - flag: flag, - codeLocation: codeLocation, - } -} - -func (container *ContainerNode) Shuffle(r *rand.Rand) { - sort.Sort(container) - permutation := r.Perm(len(container.subjectAndContainerNodes)) - shuffledNodes := make([]subjectOrContainerNode, len(container.subjectAndContainerNodes)) - for i, j := range permutation { - shuffledNodes[i] = container.subjectAndContainerNodes[j] - } - container.subjectAndContainerNodes = shuffledNodes -} - -func (node *ContainerNode) BackPropagateProgrammaticFocus() bool { - if node.flag == types.FlagTypePending { - return false - } - - shouldUnfocus := false - for _, subjectOrContainerNode := range node.subjectAndContainerNodes { - if subjectOrContainerNode.containerNode != nil { - shouldUnfocus = subjectOrContainerNode.containerNode.BackPropagateProgrammaticFocus() || shouldUnfocus - } else { - shouldUnfocus = (subjectOrContainerNode.subjectNode.Flag() == types.FlagTypeFocused) || shouldUnfocus - } - } - - if shouldUnfocus { - if node.flag == types.FlagTypeFocused { - node.flag = types.FlagTypeNone - } - return true - } - - return node.flag == types.FlagTypeFocused -} - -func (node *ContainerNode) Collate() []CollatedNodes { - return node.collate([]*ContainerNode{}) -} - -func (node *ContainerNode) collate(enclosingContainers []*ContainerNode) []CollatedNodes { - collated := make([]CollatedNodes, 0) - - containers := make([]*ContainerNode, len(enclosingContainers)) - copy(containers, enclosingContainers) - containers = append(containers, node) - - for _, subjectOrContainer := range node.subjectAndContainerNodes { - if subjectOrContainer.containerNode != nil { - collated = append(collated, subjectOrContainer.containerNode.collate(containers)...) - } else { - collated = append(collated, CollatedNodes{ - Containers: containers, - Subject: subjectOrContainer.subjectNode, - }) - } - } - - return collated -} - -func (node *ContainerNode) PushContainerNode(container *ContainerNode) { - node.subjectAndContainerNodes = append(node.subjectAndContainerNodes, subjectOrContainerNode{containerNode: container}) -} - -func (node *ContainerNode) PushSubjectNode(subject leafnodes.SubjectNode) { - node.subjectAndContainerNodes = append(node.subjectAndContainerNodes, subjectOrContainerNode{subjectNode: subject}) -} - -func (node *ContainerNode) PushSetupNode(setupNode leafnodes.BasicNode) { - node.setupNodes = append(node.setupNodes, setupNode) -} - -func (node *ContainerNode) SetupNodesOfType(nodeType types.SpecComponentType) []leafnodes.BasicNode { - nodes := []leafnodes.BasicNode{} - for _, setupNode := range node.setupNodes { - if setupNode.Type() == nodeType { - nodes = append(nodes, setupNode) - } - } - return nodes -} - -func (node *ContainerNode) Text() string { - return node.text -} - -func (node *ContainerNode) CodeLocation() types.CodeLocation { - return node.codeLocation -} - -func (node *ContainerNode) Flag() types.FlagType { - return node.flag -} - -//sort.Interface - -func (node *ContainerNode) Len() int { - return len(node.subjectAndContainerNodes) -} - -func (node *ContainerNode) Less(i, j int) bool { - return node.subjectAndContainerNodes[i].text() < node.subjectAndContainerNodes[j].text() -} - -func (node *ContainerNode) Swap(i, j int) { - node.subjectAndContainerNodes[i], node.subjectAndContainerNodes[j] = node.subjectAndContainerNodes[j], node.subjectAndContainerNodes[i] -} diff --git a/internal/containernode/container_node_suite_test.go b/internal/containernode/container_node_suite_test.go deleted file mode 100644 index c6fc314ff5..0000000000 --- a/internal/containernode/container_node_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package containernode_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestContainernode(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Containernode Suite") -} diff --git a/internal/containernode/container_node_test.go b/internal/containernode/container_node_test.go deleted file mode 100644 index 11ac9b70b1..0000000000 --- a/internal/containernode/container_node_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package containernode_test - -import ( - "math/rand" - - "github.com/onsi/ginkgo/internal/leafnodes" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "github.com/onsi/ginkgo/internal/codelocation" - . "github.com/onsi/ginkgo/internal/containernode" - "github.com/onsi/ginkgo/types" -) - -var _ = Describe("Container Node", func() { - var ( - codeLocation types.CodeLocation - container *ContainerNode - ) - - BeforeEach(func() { - codeLocation = codelocation.New(0) - container = New("description text", types.FlagTypeFocused, codeLocation) - }) - - Describe("creating a container node", func() { - It("can answer questions about itself", func() { - Ω(container.Text()).Should(Equal("description text")) - Ω(container.Flag()).Should(Equal(types.FlagTypeFocused)) - Ω(container.CodeLocation()).Should(Equal(codeLocation)) - }) - }) - - Describe("pushing setup nodes", func() { - It("can append setup nodes of various types and fetch them by type", func() { - befA := leafnodes.NewBeforeEachNode(func() {}, codelocation.New(0), 0, nil, 0) - befB := leafnodes.NewBeforeEachNode(func() {}, codelocation.New(0), 0, nil, 0) - aftA := leafnodes.NewAfterEachNode(func() {}, codelocation.New(0), 0, nil, 0) - aftB := leafnodes.NewAfterEachNode(func() {}, codelocation.New(0), 0, nil, 0) - jusBefA := leafnodes.NewJustBeforeEachNode(func() {}, codelocation.New(0), 0, nil, 0) - jusBefB := leafnodes.NewJustBeforeEachNode(func() {}, codelocation.New(0), 0, nil, 0) - - container.PushSetupNode(befA) - container.PushSetupNode(befB) - container.PushSetupNode(aftA) - container.PushSetupNode(aftB) - container.PushSetupNode(jusBefA) - container.PushSetupNode(jusBefB) - - subject := leafnodes.NewItNode("subject", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) - container.PushSubjectNode(subject) - - Ω(container.SetupNodesOfType(types.SpecComponentTypeBeforeEach)).Should(Equal([]leafnodes.BasicNode{befA, befB})) - Ω(container.SetupNodesOfType(types.SpecComponentTypeAfterEach)).Should(Equal([]leafnodes.BasicNode{aftA, aftB})) - Ω(container.SetupNodesOfType(types.SpecComponentTypeJustBeforeEach)).Should(Equal([]leafnodes.BasicNode{jusBefA, jusBefB})) - Ω(container.SetupNodesOfType(types.SpecComponentTypeIt)).Should(BeEmpty()) //subjects are not setup nodes - }) - }) - - Context("With appended containers and subject nodes", func() { - var ( - itA, itB, innerItA, innerItB leafnodes.SubjectNode - innerContainer *ContainerNode - ) - - BeforeEach(func() { - itA = leafnodes.NewItNode("Banana", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) - itB = leafnodes.NewItNode("Apple", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) - - innerItA = leafnodes.NewItNode("inner A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) - innerItB = leafnodes.NewItNode("inner B", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) - - innerContainer = New("Orange", types.FlagTypeNone, codelocation.New(0)) - - container.PushSubjectNode(itA) - container.PushContainerNode(innerContainer) - innerContainer.PushSubjectNode(innerItA) - innerContainer.PushSubjectNode(innerItB) - container.PushSubjectNode(itB) - }) - - Describe("Collating", func() { - It("should return a collated set of containers and subject nodes in the correct order", func() { - collated := container.Collate() - Ω(collated).Should(HaveLen(4)) - - Ω(collated[0]).Should(Equal(CollatedNodes{ - Containers: []*ContainerNode{container}, - Subject: itA, - })) - - Ω(collated[1]).Should(Equal(CollatedNodes{ - Containers: []*ContainerNode{container, innerContainer}, - Subject: innerItA, - })) - - Ω(collated[2]).Should(Equal(CollatedNodes{ - Containers: []*ContainerNode{container, innerContainer}, - Subject: innerItB, - })) - - Ω(collated[3]).Should(Equal(CollatedNodes{ - Containers: []*ContainerNode{container}, - Subject: itB, - })) - }) - }) - - Describe("Backpropagating Programmatic Focus", func() { - //This allows inner focused specs to override the focus of outer focussed - //specs and more closely maps to what a developer wants to happen - //when debugging a test suite - - Context("when a parent is focused *and* an inner subject is focused", func() { - BeforeEach(func() { - container = New("description text", types.FlagTypeFocused, codeLocation) - itA = leafnodes.NewItNode("A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) - container.PushSubjectNode(itA) - - innerContainer = New("Orange", types.FlagTypeNone, codelocation.New(0)) - container.PushContainerNode(innerContainer) - innerItA = leafnodes.NewItNode("inner A", func() {}, types.FlagTypeFocused, codelocation.New(0), 0, nil, 0) - innerContainer.PushSubjectNode(innerItA) - }) - - It("should unfocus the parent", func() { - container.BackPropagateProgrammaticFocus() - - Ω(container.Flag()).Should(Equal(types.FlagTypeNone)) - Ω(itA.Flag()).Should(Equal(types.FlagTypeNone)) - Ω(innerContainer.Flag()).Should(Equal(types.FlagTypeNone)) - Ω(innerItA.Flag()).Should(Equal(types.FlagTypeFocused)) - }) - }) - - Context("when a parent is focused *and* an inner container is focused", func() { - BeforeEach(func() { - container = New("description text", types.FlagTypeFocused, codeLocation) - itA = leafnodes.NewItNode("A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) - container.PushSubjectNode(itA) - - innerContainer = New("Orange", types.FlagTypeFocused, codelocation.New(0)) - container.PushContainerNode(innerContainer) - innerItA = leafnodes.NewItNode("inner A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) - innerContainer.PushSubjectNode(innerItA) - }) - - It("should unfocus the parent", func() { - container.BackPropagateProgrammaticFocus() - - Ω(container.Flag()).Should(Equal(types.FlagTypeNone)) - Ω(itA.Flag()).Should(Equal(types.FlagTypeNone)) - Ω(innerContainer.Flag()).Should(Equal(types.FlagTypeFocused)) - Ω(innerItA.Flag()).Should(Equal(types.FlagTypeNone)) - }) - }) - - Context("when a parent is pending and a child is focused", func() { - BeforeEach(func() { - container = New("description text", types.FlagTypeFocused, codeLocation) - itA = leafnodes.NewItNode("A", func() {}, types.FlagTypeNone, codelocation.New(0), 0, nil, 0) - container.PushSubjectNode(itA) - - innerContainer = New("Orange", types.FlagTypePending, codelocation.New(0)) - container.PushContainerNode(innerContainer) - innerItA = leafnodes.NewItNode("inner A", func() {}, types.FlagTypeFocused, codelocation.New(0), 0, nil, 0) - innerContainer.PushSubjectNode(innerItA) - }) - - It("should not do anything", func() { - container.BackPropagateProgrammaticFocus() - - Ω(container.Flag()).Should(Equal(types.FlagTypeFocused)) - Ω(itA.Flag()).Should(Equal(types.FlagTypeNone)) - Ω(innerContainer.Flag()).Should(Equal(types.FlagTypePending)) - Ω(innerItA.Flag()).Should(Equal(types.FlagTypeFocused)) - }) - }) - }) - - Describe("Shuffling", func() { - var unshuffledCollation []CollatedNodes - BeforeEach(func() { - unshuffledCollation = container.Collate() - - r := rand.New(rand.NewSource(17)) - container.Shuffle(r) - }) - - It("should sort, and then shuffle, the top level contents of the container", func() { - shuffledCollation := container.Collate() - Ω(shuffledCollation).Should(HaveLen(len(unshuffledCollation))) - Ω(shuffledCollation).ShouldNot(Equal(unshuffledCollation)) - - for _, entry := range unshuffledCollation { - Ω(shuffledCollation).Should(ContainElement(entry)) - } - - innerAIndex, innerBIndex := 0, 0 - for i, entry := range shuffledCollation { - if entry.Subject == innerItA { - innerAIndex = i - } else if entry.Subject == innerItB { - innerBIndex = i - } - } - - Ω(innerAIndex).Should(Equal(innerBIndex - 1)) - }) - }) - }) -}) diff --git a/internal/counter.go b/internal/counter.go new file mode 100644 index 0000000000..ead5fb2190 --- /dev/null +++ b/internal/counter.go @@ -0,0 +1,44 @@ +package internal + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/onsi/ginkgo/config" +) + +type Counter struct { + Index int `json:"index"` +} + +func MakeNextIndexCounter(config config.GinkgoConfigType) func() (int, error) { + if config.ParallelTotal > 1 { + client := &http.Client{} + return func() (int, error) { + resp, err := client.Get(config.ParallelHost + "/counter") + if err != nil { + return -1, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return -1, fmt.Errorf("unexpected status code %d", resp.StatusCode) + } + + var counter Counter + err = json.NewDecoder(resp.Body).Decode(&counter) + if err != nil { + return -1, err + } + + return counter.Index, nil + } + } else { + idx := -1 + return func() (int, error) { + idx += 1 + return idx, nil + } + } +} diff --git a/internal/counter_test.go b/internal/counter_test.go new file mode 100644 index 0000000000..322b1f7f9b --- /dev/null +++ b/internal/counter_test.go @@ -0,0 +1,107 @@ +package internal_test + +import ( + "net/http" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/ghttp" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal" +) + +var _ = Describe("Counter", func() { + var counter func() (int, error) + var conf config.GinkgoConfigType + + BeforeEach(func() { + conf = config.GinkgoConfigType{} + }) + + JustBeforeEach(func() { + counter = internal.MakeNextIndexCounter(conf) + }) + + Context("when running in series", func() { + BeforeEach(func() { + conf.ParallelTotal = 1 + }) + + It("returns a counter that grows by one with each invocation", func() { + for i := 0; i < 10; i += 1 { + Ω(counter()).Should(Equal(i)) + } + }) + }) + + Context("when running in parallel", func() { + var server *ghttp.Server + BeforeEach(func() { + conf.ParallelTotal = 2 + server = ghttp.NewServer() + conf.ParallelHost = server.URL() + }) + + AfterEach(func() { + server.Close() + }) + + Context("when the server returns with a counter", func() { + BeforeEach(func() { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/counter"), + ghttp.RespondWithJSONEncoded(http.StatusOK, internal.Counter{Index: 1138}), + ), + ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/counter"), + ghttp.RespondWithJSONEncoded(http.StatusOK, internal.Counter{Index: 27}), + ), + ) + }) + + It("forwards along the returned counter", func() { + Ω(counter()).Should(Equal(1138)) + Ω(counter()).Should(Equal(27)) + }) + }) + + Context("when an error occurs trying to get", func() { + BeforeEach(func() { + server.Close() + }) + + It("returns an error", func() { + index, err := counter() + Ω(index).Should(Equal(-1)) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when the server returns a non-ok status code", func() { + BeforeEach(func() { + server.AppendHandlers(ghttp.RespondWithJSONEncoded(http.StatusInternalServerError, internal.Counter{Index: 1138})) + }) + + It("returns an error", func() { + index, err := counter() + Ω(index).Should(Equal(-1)) + Ω(err).Should(MatchError("unexpected status code 500")) + }) + }) + + Context("when the server returns garbage json", func() { + BeforeEach(func() { + server.AppendHandlers(ghttp.RespondWith(http.StatusOK, "∫")) + }) + + It("returns an error", func() { + index, err := counter() + Ω(index).Should(Equal(-1)) + Ω(err).Should(HaveOccurred()) + }) + + }) + }) +}) diff --git a/internal/failer/failer.go b/internal/failer.go similarity index 59% rename from internal/failer/failer.go rename to internal/failer.go index 678ea2514a..4c4fcd6e5b 100644 --- a/internal/failer/failer.go +++ b/internal/failer.go @@ -1,4 +1,4 @@ -package failer +package internal import ( "fmt" @@ -9,24 +9,36 @@ import ( type Failer struct { lock *sync.Mutex - failure types.SpecFailure + failure types.Failure state types.SpecState } -func New() *Failer { +func NewFailer() *Failer { return &Failer{ lock: &sync.Mutex{}, state: types.SpecStatePassed, } } +func (f *Failer) GetState() types.SpecState { + f.lock.Lock() + defer f.lock.Unlock() + return f.state +} + +func (f *Failer) GetFailure() types.Failure { + f.lock.Lock() + defer f.lock.Unlock() + return f.failure +} + func (f *Failer) Panic(location types.CodeLocation, forwardedPanic interface{}) { f.lock.Lock() defer f.lock.Unlock() if f.state == types.SpecStatePassed { f.state = types.SpecStatePanicked - f.failure = types.SpecFailure{ + f.failure = types.Failure{ Message: "Test Panicked", Location: location, ForwardedPanic: fmt.Sprintf("%v", forwardedPanic), @@ -34,59 +46,41 @@ func (f *Failer) Panic(location types.CodeLocation, forwardedPanic interface{}) } } -func (f *Failer) Timeout(location types.CodeLocation) { +func (f *Failer) Fail(message string, location types.CodeLocation) { f.lock.Lock() defer f.lock.Unlock() if f.state == types.SpecStatePassed { - f.state = types.SpecStateTimedOut - f.failure = types.SpecFailure{ - Message: "Timed out", + f.state = types.SpecStateFailed + f.failure = types.Failure{ + Message: message, Location: location, } } } -func (f *Failer) Fail(message string, location types.CodeLocation) { +func (f *Failer) Skip(message string, location types.CodeLocation) { f.lock.Lock() defer f.lock.Unlock() if f.state == types.SpecStatePassed { - f.state = types.SpecStateFailed - f.failure = types.SpecFailure{ + f.state = types.SpecStateSkipped + f.failure = types.Failure{ Message: message, Location: location, } } } -func (f *Failer) Drain(componentType types.SpecComponentType, componentIndex int, componentCodeLocation types.CodeLocation) (types.SpecFailure, types.SpecState) { +func (f *Failer) Drain() (types.SpecState, types.Failure) { f.lock.Lock() defer f.lock.Unlock() failure := f.failure outcome := f.state - if outcome != types.SpecStatePassed { - failure.ComponentType = componentType - failure.ComponentIndex = componentIndex - failure.ComponentCodeLocation = componentCodeLocation - } f.state = types.SpecStatePassed - f.failure = types.SpecFailure{} - - return failure, outcome -} - -func (f *Failer) Skip(message string, location types.CodeLocation) { - f.lock.Lock() - defer f.lock.Unlock() + f.failure = types.Failure{} - if f.state == types.SpecStatePassed { - f.state = types.SpecStateSkipped - f.failure = types.SpecFailure{ - Message: message, - Location: location, - } - } + return outcome, failure } diff --git a/internal/failer/failer_test.go b/internal/failer/failer_test.go deleted file mode 100644 index 65210a40a7..0000000000 --- a/internal/failer/failer_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package failer_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/failer" - . "github.com/onsi/gomega" - - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/types" -) - -var _ = Describe("Failer", func() { - var ( - failer *Failer - codeLocationA types.CodeLocation - codeLocationB types.CodeLocation - ) - - BeforeEach(func() { - codeLocationA = codelocation.New(0) - codeLocationB = codelocation.New(0) - failer = New() - }) - - Context("with no failures", func() { - It("should return success when drained", func() { - failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - Ω(failure).Should(BeZero()) - Ω(state).Should(Equal(types.SpecStatePassed)) - }) - }) - - Describe("Skip", func() { - It("should handle failures", func() { - failer.Skip("something skipped", codeLocationA) - failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - Ω(failure).Should(Equal(types.SpecFailure{ - Message: "something skipped", - Location: codeLocationA, - ForwardedPanic: "", - ComponentType: types.SpecComponentTypeIt, - ComponentIndex: 3, - ComponentCodeLocation: codeLocationB, - })) - Ω(state).Should(Equal(types.SpecStateSkipped)) - }) - }) - - Describe("Fail", func() { - It("should handle failures", func() { - failer.Fail("something failed", codeLocationA) - failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - Ω(failure).Should(Equal(types.SpecFailure{ - Message: "something failed", - Location: codeLocationA, - ForwardedPanic: "", - ComponentType: types.SpecComponentTypeIt, - ComponentIndex: 3, - ComponentCodeLocation: codeLocationB, - })) - Ω(state).Should(Equal(types.SpecStateFailed)) - }) - }) - - Describe("Panic", func() { - It("should handle panics", func() { - failer.Panic(codeLocationA, "some forwarded panic") - failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - Ω(failure).Should(Equal(types.SpecFailure{ - Message: "Test Panicked", - Location: codeLocationA, - ForwardedPanic: "some forwarded panic", - ComponentType: types.SpecComponentTypeIt, - ComponentIndex: 3, - ComponentCodeLocation: codeLocationB, - })) - Ω(state).Should(Equal(types.SpecStatePanicked)) - }) - }) - - Describe("Timeout", func() { - It("should handle timeouts", func() { - failer.Timeout(codeLocationA) - failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - Ω(failure).Should(Equal(types.SpecFailure{ - Message: "Timed out", - Location: codeLocationA, - ForwardedPanic: "", - ComponentType: types.SpecComponentTypeIt, - ComponentIndex: 3, - ComponentCodeLocation: codeLocationB, - })) - Ω(state).Should(Equal(types.SpecStateTimedOut)) - }) - }) - - Context("when multiple failures are registered", func() { - BeforeEach(func() { - failer.Fail("something failed", codeLocationA) - failer.Fail("something else failed", codeLocationA) - }) - - It("should only report the first one when drained", func() { - failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - - Ω(failure).Should(Equal(types.SpecFailure{ - Message: "something failed", - Location: codeLocationA, - ForwardedPanic: "", - ComponentType: types.SpecComponentTypeIt, - ComponentIndex: 3, - ComponentCodeLocation: codeLocationB, - })) - Ω(state).Should(Equal(types.SpecStateFailed)) - }) - - It("should report subsequent failures after being drained", func() { - failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - failer.Fail("yet another thing failed", codeLocationA) - - failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - - Ω(failure).Should(Equal(types.SpecFailure{ - Message: "yet another thing failed", - Location: codeLocationA, - ForwardedPanic: "", - ComponentType: types.SpecComponentTypeIt, - ComponentIndex: 3, - ComponentCodeLocation: codeLocationB, - })) - Ω(state).Should(Equal(types.SpecStateFailed)) - }) - - It("should report sucess on subsequent drains if no errors occur", func() { - failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - failure, state := failer.Drain(types.SpecComponentTypeIt, 3, codeLocationB) - Ω(failure).Should(BeZero()) - Ω(state).Should(Equal(types.SpecStatePassed)) - }) - }) -}) diff --git a/internal/failer_test.go b/internal/failer_test.go new file mode 100644 index 0000000000..8f6675228f --- /dev/null +++ b/internal/failer_test.go @@ -0,0 +1,140 @@ +package internal_test + +import ( + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/internal" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("Failer", func() { + var failer *internal.Failer + var clA types.CodeLocation + var clB types.CodeLocation + + BeforeEach(func() { + clA = CL("file_a.go") + clB = CL("file_b.go") + failer = internal.NewFailer() + }) + + Context("with no failures", func() { + It("should return success when drained", func() { + state, failure := failer.Drain() + Ω(state).Should(Equal(types.SpecStatePassed)) + Ω(failure).Should(BeZero()) + }) + }) + + Describe("when told of a failure", func() { + BeforeEach(func() { + failer.Fail("something failed", clA) + }) + + It("should record the failure", func() { + state, failure := failer.Drain() + Ω(state).Should(Equal(types.SpecStateFailed)) + Ω(failure).Should(Equal(types.Failure{ + Message: "something failed", + Location: clA, + })) + }) + + Context("when told of anotehr failure", func() { + It("discards the second failure, preserving the original", func() { + failer.Fail("something else failed", clB) + + state, failure := failer.Drain() + Ω(state).Should(Equal(types.SpecStateFailed)) + Ω(failure).Should(Equal(types.Failure{ + Message: "something failed", + Location: clA, + })) + }) + }) + }) + + Describe("when told to skip", func() { + Context("when no failure has occured", func() { + It("registers the test as skipped", func() { + failer.Skip("something skipped", clA) + state, failure := failer.Drain() + Ω(state).Should(Equal(types.SpecStateSkipped)) + Ω(failure).Should(Equal(types.Failure{ + Message: "something skipped", + Location: clA, + })) + }) + }) + + Context("when a failure has already occured", func() { + BeforeEach(func() { + failer.Fail("something failed", clA) + }) + + It("does not modify the failure", func() { + failer.Skip("something skipped", clB) + state, failure := failer.Drain() + Ω(state).Should(Equal(types.SpecStateFailed)) + Ω(failure).Should(Equal(types.Failure{ + Message: "something failed", + Location: clA, + })) + }) + }) + }) + + Describe("when told to panic", func() { + BeforeEach(func() { + failer.Panic(clA, 17) + }) + + It("should record the panic", func() { + state, failure := failer.Drain() + Ω(state).Should(Equal(types.SpecStatePanicked)) + Ω(failure).Should(Equal(types.Failure{ + Message: "Test Panicked", + Location: clA, + ForwardedPanic: "17", + })) + }) + + Context("when told of another panic", func() { + It("discards the second panic, preserving the original", func() { + failer.Panic(clB, 23) + + state, failure := failer.Drain() + Ω(state).Should(Equal(types.SpecStatePanicked)) + Ω(failure).Should(Equal(types.Failure{ + Message: "Test Panicked", + Location: clA, + ForwardedPanic: "17", + })) + }) + }) + }) + + Context("when drained", func() { + BeforeEach(func() { + failer.Fail("something failed", clA) + state, _ := failer.Drain() + Ω(state).Should(Equal(types.SpecStateFailed)) + }) + + It("resets the failer such that subsequent drains pass", func() { + state, failure := failer.Drain() + Ω(state).Should(Equal(types.SpecStatePassed)) + Ω(failure).Should(BeZero()) + }) + + It("allows subsequent failures to be recorded", func() { + failer.Fail("something else failed", clB) + state, failure := failer.Drain() + Ω(state).Should(Equal(types.SpecStateFailed)) + Ω(failure).Should(Equal(types.Failure{ + Message: "something else failed", + Location: clB, + })) + }) + }) +}) diff --git a/internal/focus.go b/internal/focus.go new file mode 100644 index 0000000000..9b0c17a8a8 --- /dev/null +++ b/internal/focus.go @@ -0,0 +1,119 @@ +package internal + +import ( + "regexp" + "strings" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +/* + If a container marked as focus has a descendant that is also marked as focus, Ginkgo's policy is to + unmark the container's focus. This gives developers a more intuitive experience when debugging specs. + It is common to focus a container to just run a subset of specs, then identify the specific specs within the container to focus - + this policy allows the developer to simply focus those specific specs and not need to go back and turn the focus off of the container: + + As a common example, consider: + + FDescribe("something to debug", function() { + It("works", function() {...}) + It("works", function() {...}) + FIt("doesn't work", function() {...}) + It("works", function() {...}) + }) + + here the developer's intent is to focus in on the `"doesn't work"` spec and not to run the adjacent specs in the focused `"something to debug"` container. + The nested policy applied by this function enables this behavior. +*/ +func ApplyNestedFocusPolicyToTree(tree TreeNode) TreeNode { + var walkTree func(tree TreeNode) (TreeNode, bool) + walkTree = func(tree TreeNode) (TreeNode, bool) { + if tree.Node.MarkedPending { + return tree, false + } + hasFocusedDescendant := false + processedTree := TreeNode{Node: tree.Node} + for _, child := range tree.Children { + processedChild, childHasFocus := walkTree(child) + hasFocusedDescendant = hasFocusedDescendant || childHasFocus + processedTree = AppendTreeNodeChild(processedTree, processedChild) + } + processedTree.Node.MarkedFocus = processedTree.Node.MarkedFocus && !hasFocusedDescendant + return processedTree, processedTree.Node.MarkedFocus || hasFocusedDescendant + } + + out, _ := walkTree(tree) + return out +} + +/* + Ginkgo supports focussing specs using `FIt`, `FDescribe`, etc. - this is called "programmatic focus" + It also supports focussing specs using regular expressions on the command line (`-focus=`, `-skip=`). + The CLI regular expressions take precedence. + + This function sets the `Skip` property on specs by applying Ginkgo's focus policy: + - If there are no CLI arguments and no programmatic focus, do nothing. + - If there are no CLi arguments but a spec somewhere has programmatic focus, skip any specs that have no programmatic focus. + - If there are CLI arguments parse them and skip any specs that either don't match the filter regexp or do match* the skip regexp. + + Lastly, `config.RegexScansFilePath` allows the regular exprressions to match against the spec's filepath as well as the spec's text. + + *Note:* specs with pending nodes are Skipped when created by NewSpec. +*/ +func ApplyFocusToSpecs(specs Specs, description string, config config.GinkgoConfigType) (Specs, bool) { + focusString := strings.Join(config.FocusStrings, "|") + skipString := strings.Join(config.SkipStrings, "|") + + type SkipCheck func(spec Spec) bool + + // by default, skip any specs marked pending + skipChecks := []SkipCheck{func(spec Spec) bool { return spec.Nodes.HasNodeMarkedPending() }} + hasProgrammaticFocus := false + + if focusString == "" && skipString == "" { + // check for programmatic focus + for _, spec := range specs { + if spec.Nodes.HasNodeMarkedFocus() && !spec.Nodes.HasNodeMarkedPending() { + skipChecks = append(skipChecks, func(spec Spec) bool { return !spec.Nodes.HasNodeMarkedFocus() }) + hasProgrammaticFocus = true + break + } + } + } + + //the text to match when applying regexp filtering + textToMatch := func(spec Spec) string { + textToMatch := description + " " + spec.Text() + if config.RegexScansFilePath { + textToMatch += " " + spec.FirstNodeWithType(types.NodeTypeIt).CodeLocation.FileName + } + return textToMatch + } + + if focusString != "" { + // skip specs that don't match the focus string + re := regexp.MustCompile(focusString) + skipChecks = append(skipChecks, func(spec Spec) bool { return !re.MatchString(textToMatch(spec)) }) + } + + if skipString != "" { + // skip specs that match the skip string + re := regexp.MustCompile(skipString) + skipChecks = append(skipChecks, func(spec Spec) bool { return re.MatchString(textToMatch(spec)) }) + } + + // skip specs if shouldSkip() is true. note that we do nothing if shouldSkip() is false to avoid overwriting skip status established by the node's pending status + processedSpecs := Specs{} + for _, spec := range specs { + for _, skipCheck := range skipChecks { + if skipCheck(spec) { + spec.Skip = true + break + } + } + processedSpecs = append(processedSpecs, spec) + } + + return processedSpecs, hasProgrammaticFocus +} diff --git a/internal/focus_test.go b/internal/focus_test.go new file mode 100644 index 0000000000..3cab31294a --- /dev/null +++ b/internal/focus_test.go @@ -0,0 +1,234 @@ +package internal_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal" +) + +var _ = Describe("Focus", func() { + Describe("ApplyNestedFocusToTree", func() { + It("unfocuses parent nodes that have a focused child node somewhere in their tree", func() { + tree := TN(N(ntCon, "root", MarkedFocus(true)), //should lose focus + TN(N(ntCon, "A", MarkedFocus(true)), //should stay focused + TN(N(ntIt)), + TN(N(ntIt)), + ), + TN(N(ntCon), + TN(N(ntIt)), + TN(N(ntIt, "B", MarkedFocus(true))), //should stay focused + ), + TN(N(ntCon, "C", MarkedFocus(true)), //should lose focus + TN(N(ntIt)), + TN(N(ntIt, "D", MarkedFocus(true))), //should stay focused + ), + TN(N(ntCon, "E", MarkedFocus(true)), //should lose focus + TN(N(ntIt)), + TN(N(ntCon), + TN(N(ntIt)), + TN(N(ntIt, "F", MarkedFocus(true))), // should stay focused + ), + ), + TN(N(ntCon, "G", MarkedFocus(true)), //should lose focus + TN(N(ntIt)), + TN(N(ntCon, "H", MarkedFocus(true)), //should lose focus + TN(N(ntIt)), + TN(N(ntIt, "I", MarkedFocus(true))), //should stay focused + ), + TN(N(ntCon, "J", MarkedFocus(true)), // should stay focused + TN(N(ntIt)), + ), + ), + ) + + tree = internal.ApplyNestedFocusPolicyToTree(tree) + Ω(mustFindNodeWithText(tree, "root").MarkedFocus).Should(BeFalse()) + Ω(mustFindNodeWithText(tree, "A").MarkedFocus).Should(BeTrue()) + Ω(mustFindNodeWithText(tree, "B").MarkedFocus).Should(BeTrue()) + Ω(mustFindNodeWithText(tree, "C").MarkedFocus).Should(BeFalse()) + Ω(mustFindNodeWithText(tree, "D").MarkedFocus).Should(BeTrue()) + Ω(mustFindNodeWithText(tree, "E").MarkedFocus).Should(BeFalse()) + Ω(mustFindNodeWithText(tree, "F").MarkedFocus).Should(BeTrue()) + Ω(mustFindNodeWithText(tree, "G").MarkedFocus).Should(BeFalse()) + Ω(mustFindNodeWithText(tree, "H").MarkedFocus).Should(BeFalse()) + Ω(mustFindNodeWithText(tree, "I").MarkedFocus).Should(BeTrue()) + Ω(mustFindNodeWithText(tree, "J").MarkedFocus).Should(BeTrue()) + }) + + It("does not unfocus parent nodes if a focused child is the child of a pending child", func() { + tree := TN(N(ntCon), + TN(N(ntCon, "A", MarkedFocus(true)), //should stay focused + TN(N(ntIt)), + TN(N(ntCon, "B", MarkedPending(true)), //should stay pending + TN(N(ntIt)), + TN(N(ntIt, "C", MarkedFocus(true))), //should stay focused + ), + ), + ) + + tree = internal.ApplyNestedFocusPolicyToTree(tree) + Ω(mustFindNodeWithText(tree, "A").MarkedFocus).Should(BeTrue()) + Ω(mustFindNodeWithText(tree, "B").MarkedPending).Should(BeTrue()) + Ω(mustFindNodeWithText(tree, "C").MarkedFocus).Should(BeTrue()) + }) + }) + + Describe("ApplyFocusToSpecs", func() { + var specs Specs + var description string + var conf config.GinkgoConfigType + + harvestSkips := func(specs Specs) []bool { + out := []bool{} + for _, spec := range specs { + out = append(out, spec.Skip) + } + return out + } + + BeforeEach(func() { + description = "Silmarillion Suite" + conf = config.GinkgoConfigType{} + }) + + Context("when there are specs with nodes marked pending", func() { + BeforeEach(func() { + specs = Specs{ + S(N(), N()), + S(N(), N()), + S(N(), N(MarkedPending(true))), + S(N(), N()), + S(N(MarkedPending(true))), + } + }) + + It("skips those specs", func() { + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, conf) + Ω(harvestSkips(specs)).Should(Equal([]bool{false, false, true, false, true})) + Ω(hasProgrammaticFocus).Should(BeFalse()) + }) + }) + + Context("when there are specs with nodes marked focused", func() { + BeforeEach(func() { + specs = Specs{ + S(N(), N()), + S(N(), N()), + S(N(), N(MarkedFocus(true))), + S(N()), + S(N(MarkedFocus(true))), + } + }) + It("skips any other specs and notes that it has programmatic focus", func() { + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, conf) + Ω(harvestSkips(specs)).Should(Equal([]bool{true, true, false, true, false})) + Ω(hasProgrammaticFocus).Should(BeTrue()) + }) + + Context("when the specs with nodes marked focused also have nodes marked pending ", func() { + BeforeEach(func() { + specs = Specs{ + S(N(), N()), + S(N(), N()), + S(N(MarkedPending(true)), N(MarkedFocus(true))), + S(N()), + } + }) + It("does not skip any other specs and notes that it does not have programmatic focus", func() { + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, conf) + Ω(harvestSkips(specs)).Should(Equal([]bool{false, false, true, false})) + Ω(hasProgrammaticFocus).Should(BeFalse()) + }) + }) + }) + + Context("when there are focus strings and/or skip strings configured", func() { + BeforeEach(func() { + specs = Specs{ + S(N("blue"), N("dragon")), + S(N("blue"), N("Dragon")), + S(N("red dragon"), N()), + S(N("green dragon"), N()), + S(N(MarkedPending(true)), N("blue Dragon")), + S(N("yellow dragon")), + S(N(MarkedFocus(true), "yellow dragon")), + } + }) + + Context("when there are focus strings configured", func() { + BeforeEach(func() { + conf.FocusStrings = []string{"blue [dD]ra", "(red|green) dragon"} + }) + + It("overrides any programmatic focus, runs only specs that match the focus string, and continues to skip specs with nodes marked pending", func() { + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, conf) + Ω(harvestSkips(specs)).Should(Equal([]bool{false, false, false, false, true, true, true})) + Ω(hasProgrammaticFocus).Should(BeFalse()) + }) + + It("includes the description string in the search", func() { + conf.FocusStrings = []string{"Silmaril"} + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, conf) + Ω(harvestSkips(specs)).Should(Equal([]bool{false, false, false, false, true, false, false})) + Ω(hasProgrammaticFocus).Should(BeFalse()) + }) + }) + + Context("when there are skip strings configured", func() { + BeforeEach(func() { + conf.SkipStrings = []string{"blue [dD]ragon", "red dragon"} + }) + + It("overrides any programmatic focus, and runs specs that don't match the skip strings, and continues to skip specs with nodes marked pending", func() { + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, conf) + Ω(harvestSkips(specs)).Should(Equal([]bool{true, true, true, false, true, false, false})) + Ω(hasProgrammaticFocus).Should(BeFalse()) + }) + + It("includes the description string in the search", func() { + conf.SkipStrings = []string{"Silmaril"} + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, conf) + Ω(harvestSkips(specs)).Should(Equal([]bool{true, true, true, true, true, true, true})) + Ω(hasProgrammaticFocus).Should(BeFalse()) + }) + }) + + Context("when skip and focus are configured", func() { + BeforeEach(func() { + conf.FocusStrings = []string{"blue [dD]ragon", "(red|green) dragon"} + conf.SkipStrings = []string{"red dragon", "Dragon"} + }) + + It("ORs both together", func() { + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, conf) + Ω(harvestSkips(specs)).Should(Equal([]bool{false, true, true, false, true, true, true})) + Ω(hasProgrammaticFocus).Should(BeFalse()) + }) + }) + }) + + Context("when configured to RegexScansFilePath", func() { + BeforeEach(func() { + specs = Specs{ + S(N(CL("file_a"))), + S(N(CL("file_b"))), + S(N(CL("file_b"), MarkedPending(true))), + S(N(CL("c", MarkedFocus(true)))), + } + + conf.RegexScansFilePath = true + conf.FocusStrings = []string{"file_"} + conf.SkipStrings = []string{"_a"} + }) + + It("includes the codelocation filename in the search for focus and skip strings", func() { + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, conf) + Ω(harvestSkips(specs)).Should(Equal([]bool{true, false, true, true})) + Ω(hasProgrammaticFocus).Should(BeFalse()) + + }) + }) + }) +}) diff --git a/internal/global/init.go b/internal/global/init.go index 109f617a5e..131a0b0e89 100644 --- a/internal/global/init.go +++ b/internal/global/init.go @@ -1,22 +1,17 @@ package global import ( - "time" - - "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/internal/suite" + "github.com/onsi/ginkgo/internal" ) -const DefaultTimeout = time.Duration(1 * time.Second) - -var Suite *suite.Suite -var Failer *failer.Failer +var Suite *internal.Suite +var Failer *internal.Failer func init() { InitializeGlobals() } func InitializeGlobals() { - Failer = failer.New() - Suite = suite.New(Failer) + Failer = internal.NewFailer() + Suite = internal.NewSuite() } diff --git a/internal/internal_integration/config_dry_run_test.go b/internal/internal_integration/config_dry_run_test.go new file mode 100644 index 0000000000..91e5138172 --- /dev/null +++ b/internal/internal_integration/config_dry_run_test.go @@ -0,0 +1,49 @@ +package internal_integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/test_helpers" + . "github.com/onsi/gomega" +) + +var _ = Describe("when config.DryRun is enabled", func() { + BeforeEach(func() { + conf.DryRun = true + conf.SkipStrings = []string{"E"} + + RunFixture("dry run", func() { + BeforeSuite(rt.T("before-suite")) + BeforeEach(rt.T("bef")) + Describe("container", func() { + It("A", rt.T("A")) + It("B", rt.T("B", func() { F() })) + PIt("C", rt.T("C", func() { F() })) + It("D", rt.T("D")) + It("E", rt.T("D")) + }) + AfterEach(rt.T("aft")) + AfterSuite(rt.T("after-suite")) + }) + }) + + It("does not run any tests", func() { + Ω(rt).Should(HaveTrackedNothing()) + }) + + It("reports on the tests (both that they will run and that they did run) and honors skip state", func() { + Ω(reporter.Will.Names()).Should(Equal([]string{"A", "B", "C", "D", "E"})) + Ω(reporter.Will.Find("C")).Should(BePending()) + Ω(reporter.Will.Find("E")).Should(HaveBeenSkipped()) + + Ω(reporter.Did.Names()).Should(Equal([]string{"A", "B", "C", "D", "E"})) + Ω(reporter.Did.Find("A")).Should(HavePassed()) + Ω(reporter.Did.Find("B")).Should(HavePassed()) + Ω(reporter.Did.Find("C")).Should(BePending()) + Ω(reporter.Did.Find("D")).Should(HavePassed()) + Ω(reporter.Did.Find("E")).Should(HaveBeenSkipped()) + }) + + It("reports the correct statistics", func() { + Ω(reporter.End).Should(BeASuiteSummary(NSpecs(5), NPassed(3), NPending(1), NSkipped(2))) + }) +}) diff --git a/internal/internal_integration/config_fail_fast_test.go b/internal/internal_integration/config_fail_fast_test.go new file mode 100644 index 0000000000..799e1c88a1 --- /dev/null +++ b/internal/internal_integration/config_fail_fast_test.go @@ -0,0 +1,44 @@ +package internal_integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/test_helpers" + . "github.com/onsi/gomega" +) + +var _ = Describe("when config.FailFast is enabled", func() { + BeforeEach(func() { + conf.FailFast = true + + RunFixture("fail fast", func() { + Describe("a container", func() { + BeforeEach(rt.T("bef")) + It("A", rt.T("A")) + It("B", rt.T("B", func() { F() })) + It("C", rt.T("C", func() { F() })) + It("D", rt.T("D")) + AfterEach(rt.T("aft")) + }) + AfterSuite(rt.T("after-suite")) + }) + }) + + It("does not run any tests after the failure occurs, but does run the failed tests's after each and the after suite", func() { + Ω(rt).Should(HaveTracked( + "bef", "A", "aft", + "bef", "B", "aft", + "after-suite", + )) + }) + + It("reports that the tests were skipped", func() { + Ω(reporter.Did.Find("A")).Should(HavePassed()) + Ω(reporter.Did.Find("B")).Should(HaveFailed()) + Ω(reporter.Did.Find("C")).Should(HaveBeenSkipped()) + Ω(reporter.Did.Find("D")).Should(HaveBeenSkipped()) + }) + + It("reports the correct statistics", func() { + Ω(reporter.End).Should(BeASuiteSummary(NSpecs(4), NPassed(1), NFailed(1), NSkipped(2))) + }) +}) diff --git a/internal/internal_integration/config_flake_attempts_test.go b/internal/internal_integration/config_flake_attempts_test.go new file mode 100644 index 0000000000..e446a04ab6 --- /dev/null +++ b/internal/internal_integration/config_flake_attempts_test.go @@ -0,0 +1,69 @@ +package internal_integration_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/test_helpers" + . "github.com/onsi/gomega" +) + +var _ = Describe("when config.FlakeAttempts is greater than 1", func() { + var success bool + JustBeforeEach(func() { + var counterA, counterC int + + success, _ = RunFixture("flakey success", func() { + It("A", rt.T("A", func() { + counterA += 1 + if counterA < 2 { + F(fmt.Sprintf("A - %d", counterA)) + } + })) + It("B", func() {}) + It("C", rt.T("C", func() { + counterC += 1 + writer.Write([]byte(fmt.Sprintf("C - attempt #%d\n", counterC))) + if counterC < 3 { + F(fmt.Sprintf("C - %d", counterC)) + } + })) + }) + }) + + Context("when a test succeeds within the correct number of attempts", func() { + BeforeEach(func() { + conf.FlakeAttempts = 3 + }) + + It("reports that the suite passed, but with flaked specs", func() { + Ω(success).Should(BeTrue()) + Ω(reporter.End).Should(BeASuiteSummary(NSpecs(3), NFailed(0), NPassed(3), NFlaked(2))) + }) + + It("reports that the test passed with the correct number of attempts", func() { + Ω(reporter.Did.Find("A")).Should(HavePassed(NumAttempts(2))) + Ω(reporter.Did.Find("B")).Should(HavePassed(NumAttempts(1))) + Ω(reporter.Did.Find("C")).Should(HavePassed(NumAttempts(3), + CapturedOutput("C - attempt #1\n\nGinkgo: Attempt #1 Failed. Retrying...\nC - attempt #2\n\nGinkgo: Attempt #2 Failed. Retrying...\nC - attempt #3\n"))) + }) + }) + + Context("when the test fails", func() { + BeforeEach(func() { + conf.FlakeAttempts = 2 + }) + + It("reports that the suite failed", func() { + Ω(success).Should(BeFalse()) + Ω(reporter.End).Should(BeASuiteSummary(NSpecs(3), NFailed(1), NPassed(2), NFlaked(1))) + }) + + It("reports that the test failed with the correct number of attempts", func() { + Ω(reporter.Did.Find("A")).Should(HavePassed(NumAttempts(2))) + Ω(reporter.Did.Find("B")).Should(HavePassed(NumAttempts(1))) + Ω(reporter.Did.Find("C")).Should(HaveFailed("C - 2", NumAttempts(2), + CapturedOutput("C - attempt #1\n\nGinkgo: Attempt #1 Failed. Retrying...\nC - attempt #2\n"))) + }) + }) +}) diff --git a/internal/internal_integration/config_progress_test.go b/internal/internal_integration/config_progress_test.go new file mode 100644 index 0000000000..16752f7282 --- /dev/null +++ b/internal/internal_integration/config_progress_test.go @@ -0,0 +1,48 @@ +package internal_integration_test + +import ( + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("when config.EmitSpecProgress is enabled", func() { + BeforeEach(func() { + writer.SetStream(true) + conf.EmitSpecProgress = true + }) + + It("emits progress to the writer as it goes", func() { + l := types.NewCodeLocation(0) + RunFixture("emitting spec progress", func() { + BeforeSuite(func() { + Ω(writerBuffer).Should(gbytes.Say(`\[BeforeSuite\] TOP-LEVEL`)) + Ω(writerBuffer).Should(gbytes.Say(`%s:%d`, l.FileName, l.LineNumber+2)) + }) + Describe("a container", func() { + BeforeEach(func() { + Ω(writerBuffer).Should(gbytes.Say(`\[BeforeEach\] a container`)) + Ω(writerBuffer).Should(gbytes.Say(`%s:\d+`, l.FileName)) + }) + It("A", func() { + Ω(writerBuffer).Should(gbytes.Say(`\[It\] A`)) + Ω(writerBuffer).Should(gbytes.Say(`%s:\d+`, l.FileName)) + }) + It("B", func() { + Ω(writerBuffer).Should(gbytes.Say(`\[It\] B`)) + Ω(writerBuffer).Should(gbytes.Say(`%s:\d+`, l.FileName)) + }) + AfterEach(func() { + Ω(writerBuffer).Should(gbytes.Say(`\[AfterEach\] a container`)) + Ω(writerBuffer).Should(gbytes.Say(`%s:\d+`, l.FileName)) + }) + }) + AfterSuite(func() { + Ω(writerBuffer).Should(gbytes.Say(`\[AfterSuite\] TOP-LEVEL`)) + Ω(writerBuffer).Should(gbytes.Say(`%s:\d+`, l.FileName)) + }) + }) + + }) +}) diff --git a/internal/internal_integration/current_test_description_test.go b/internal/internal_integration/current_test_description_test.go new file mode 100644 index 0000000000..8d5b3c6fcb --- /dev/null +++ b/internal/internal_integration/current_test_description_test.go @@ -0,0 +1,71 @@ +package internal_integration_test + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Current Test Descriptions", func() { + var descriptions map[string]GinkgoTestDescription + BeforeEach(func() { + descriptions = map[string]GinkgoTestDescription{} + logDescription := func(key string, andRun ...func()) func() { + return func() { + descriptions[key] = CurrentGinkgoTestDescription() + if len(andRun) > 0 { + andRun[0]() + } + } + } + + RunFixture("current test description", func() { + BeforeSuite(logDescription("before-suite")) + Context("a passing test", func() { + BeforeEach(logDescription("bef-A")) + It("A", logDescription("it-A", func() { + time.Sleep(20 * time.Millisecond) + })) + AfterEach(logDescription("aft-A")) + }) + Context("a failing test", func() { + BeforeEach(logDescription("bef-B")) + It("B", logDescription("it-B", func() { + F("failed") + })) + AfterEach(logDescription("aft-B")) + }) + AfterSuite(logDescription("after-suite")) + }) + }) + + It("returns an empty GinkgoTestDescription in the before suite and after suite", func() { + Ω(descriptions["before-suite"]).Should(BeZero()) + Ω(descriptions["after-suite"]).Should(BeZero()) + }) + + It("reports as passed while the test is passing", func() { + Ω(descriptions["bef-A"].Failed).Should(BeFalse()) + Ω(descriptions["it-A"].Failed).Should(BeFalse()) + Ω(descriptions["aft-A"].Failed).Should(BeFalse()) + }) + + It("reports as failed when the test fails", func() { + Ω(descriptions["bef-B"].Failed).Should(BeFalse()) + Ω(descriptions["it-B"].Failed).Should(BeFalse()) + Ω(descriptions["aft-B"].Failed).Should(BeTrue()) + }) + + It("captures test details correctly", func() { + description := descriptions["aft-A"] + Ω(description.ComponentTexts).Should(Equal([]string{"a passing test", "A"})) + Ω(description.FullTestText).Should(Equal("a passing test A")) + Ω(description.TestText).Should(Equal("A")) + locations := reporter.Did.Find("A").NodeLocations + location := locations[len(locations)-1] + Ω(description.FileName).Should(Equal(location.FileName)) + Ω(description.LineNumber).Should(Equal(location.LineNumber)) + Ω(description.Duration).Should(BeNumerically(">=", time.Millisecond*20)) + }) +}) diff --git a/internal/internal_integration/fail_test.go b/internal/internal_integration/fail_test.go new file mode 100644 index 0000000000..509c55aeff --- /dev/null +++ b/internal/internal_integration/fail_test.go @@ -0,0 +1,341 @@ +package internal_integration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/onsi/ginkgo/internal/test_helpers" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("handling test failures", func() { + Describe("when BeforeSuite fails", func() { + BeforeEach(func() { + success, _ := RunFixture("failed beforesuite", func() { + BeforeSuite(rt.T("before-suite", func() { + writer.Write([]byte("before-suite")) + F("fail", cl) + })) + It("A", rt.T("A")) + It("B", rt.T("B")) + AfterSuite(rt.T("after-suite")) + }) + Ω(success).Should(BeFalse()) + }) + + It("reports a suite failure", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(2), NSkipped(2))) + }) + + It("reports a failure for the BeforeSuite", func() { + Ω(reporter.Did.FindByLeafNodeType(types.NodeTypeBeforeSuite)).Should(HaveFailed("fail", cl, CapturedOutput("before-suite"))) + Ω(reporter.Did.FindByLeafNodeType(types.NodeTypeAfterSuite)).Should(HavePassed()) + }) + + It("does not run any of the Its", func() { + Ω(rt).ShouldNot(HaveRun("A")) + Ω(rt).ShouldNot(HaveRun("B")) + }) + + It("does run the AfterSuite", func() { + Ω(rt).Should(HaveTracked("before-suite", "after-suite")) + }) + }) + + Describe("when BeforeSuite panics", func() { + BeforeEach(func() { + success, _ := RunFixture("panicked beforesuite", func() { + BeforeSuite(rt.T("before-suite", func() { + writer.Write([]byte("before-suite")) + panic("boom") + })) + It("A", rt.T("A")) + It("B", rt.T("B")) + AfterSuite(rt.T("after-suite")) + }) + Ω(success).Should(BeFalse()) + }) + + It("reports a suite failure", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(2), NSkipped(2))) + }) + + It("reports a failure for the BeforeSuite", func() { + Ω(reporter.Did.FindByLeafNodeType(types.NodeTypeBeforeSuite)).Should(HavePanicked("boom", CapturedOutput("before-suite"))) + }) + + It("does not run any of the Its", func() { + Ω(rt).ShouldNot(HaveRun("A")) + Ω(rt).ShouldNot(HaveRun("B")) + }) + + It("does run the AfterSuite", func() { + Ω(rt).Should(HaveTracked("before-suite", "after-suite")) + }) + }) + + Describe("when AfterSuite fails/panics", func() { + BeforeEach(func() { + success, _ := RunFixture("failed aftersuite", func() { + BeforeSuite(rt.T("before-suite")) + Describe("top-level", func() { + It("A", rt.T("A")) + It("B", rt.T("B")) + }) + AfterSuite(rt.T("after-suite", func() { + writer.Write([]byte("after-suite")) + F("fail", cl) + })) + }) + Ω(success).Should(BeFalse()) + }) + + It("reports a suite failure", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(2), NPassed(2))) + }) + + It("runs and reports on all the tests and reports a failure for the AfterSuite", func() { + Ω(rt).Should(HaveTracked("before-suite", "A", "B", "after-suite")) + Ω(reporter.Did.FindByLeafNodeType(types.NodeTypeBeforeSuite)).Should(HavePassed()) + Ω(reporter.Did.Find("A")).Should(HavePassed()) + Ω(reporter.Did.Find("B")).Should(HavePassed()) + Ω(reporter.Did.FindByLeafNodeType(types.NodeTypeAfterSuite)).Should(HaveFailed("fail", cl, CapturedOutput("after-suite"))) + }) + }) + + Describe("individual test falures", func() { + Describe("when an It fails", func() { + BeforeEach(func() { + success, _ := RunFixture("failed it", func() { + BeforeSuite(rt.T("before-suite")) + Describe("top-level", func() { + It("A", rt.T("A", func() { + writer.Write([]byte("running A")) + })) + It("B", rt.T("B", func() { + writer.Write([]byte("running B")) + F("fail", cl) + })) + It("C", rt.T("C")) + }) + AfterEach(rt.T("after-each")) + AfterSuite(rt.T("after-suite")) + }) + Ω(success).Should(BeFalse()) + }) + + It("reports a suite failure", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(3), NPassed(2), NFailed(1))) + }) + + It("runs other Its, the AfterEach, and the AfterSuite", func() { + Ω(rt).Should(HaveTracked("before-suite", "A", "after-each", "B", "after-each", "C", "after-each", "after-suite")) + }) + + It("reports the It's failure", func() { + Ω(reporter.Did.Find("A")).Should(HavePassed(CapturedOutput("running A"))) + Ω(reporter.Did.Find("B")).Should(HaveFailed("fail", cl, CapturedOutput("running B"))) + Ω(reporter.Did.Find("C")).Should(HavePassed()) + }) + + It("matches up the nesting level correctly", func() { + report := reporter.Did.Find("B") + Ω(report.NodeTexts[report.Failure.NodeIndex]).Should(Equal("B")) + }) + }) + + Describe("when an It panics", func() { + BeforeEach(func() { + success, _ := RunFixture("panicked it", func() { + BeforeSuite(rt.T("before-suite")) + Describe("top-level", func() { + It("A", rt.T("A", func() { + writer.Write([]byte("running A")) + })) + It("B", rt.T("B", func() { + writer.Write([]byte("running B")) + panic("boom") + })) + It("C", rt.T("C")) + }) + AfterSuite(rt.T("after-suite")) + }) + Ω(success).Should(BeFalse()) + }) + + It("reports a suite failure", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(3), NPassed(2), NFailed(1))) + }) + + It("runs other Its and the AfterSuite", func() { + Ω(rt).Should(HaveTracked("before-suite", "A", "B", "C", "after-suite")) + }) + + It("reports the It's failure", func() { + Ω(reporter.Did.Find("A")).Should(HavePassed(CapturedOutput("running A"))) + Ω(reporter.Did.Find("B")).Should(HavePanicked("boom", CapturedOutput("running B"))) + Ω(reporter.Did.Find("C")).Should(HavePassed()) + }) + + It("matches up the nesting level correctly", func() { + report := reporter.Did.Find("B") + Ω(report.NodeTexts[report.Failure.NodeIndex]).Should(Equal("B")) + }) + }) + + Describe("when a BeforeEach fails/panics", func() { + BeforeEach(func() { + success, _ := RunFixture("failed before each", func() { + BeforeEach(rt.T("bef-1")) + JustBeforeEach(rt.T("jus-bef-1")) + Describe("top-level", func() { + BeforeEach(rt.T("bef-2", func() { + writer.Write([]byte("bef-2 is running")) + F("fail", cl) + })) + JustBeforeEach(rt.T("jus-bef-2")) + Describe("nested", func() { + BeforeEach(rt.T("bef-3")) + JustBeforeEach(rt.T("jus-bef-3")) + It("the test", rt.T("it")) + JustAfterEach(rt.T("jus-aft-3")) + AfterEach(rt.T("aft-3")) + }) + JustAfterEach(rt.T("jus-aft-2")) + AfterEach(rt.T("aft-2", func() { + writer.Write([]byte("aft-2 is running")) + })) + }) + JustAfterEach(rt.T("jus-aft-1")) + AfterEach(rt.T("aft-1")) + }) + Ω(success).Should(BeFalse()) + }) + + It("reports a suite failure and a spec failure", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(1), NPassed(0), NFailed(1))) + specReport := reporter.Did.Find("the test") + Ω(specReport).Should(HaveFailed("fail", cl), CapturedOutput("bef-2 is runningaft-2 is running")) + Ω(specReport.Failure.NodeType).Should(Equal(types.NodeTypeBeforeEach)) + }) + + It("matches up the nesting level correctly", func() { + report := reporter.Did.Find("the test") + Ω(report.NodeTexts[report.Failure.NodeIndex]).Should(Equal("top-level")) + }) + + It("runs the JustAfterEaches and AfterEaches at the same or lesser nesting level", func() { + Ω(rt).Should(HaveTracked("bef-1", "bef-2", "jus-aft-2", "jus-aft-1", "aft-2", "aft-1")) + }) + }) + + Describe("when a top-level BeforeEach fails/panics", func() { + BeforeEach(func() { + success, _ := RunFixture("failed before each", func() { + BeforeEach(rt.T("bef-1", func() { + F("fail", cl) + })) + It("the test", rt.T("it")) + }) + Ω(success).Should(BeFalse()) + }) + + It("matches up the nesting level correctly", func() { + report := reporter.Did.Find("the test") + Ω(report.Failure.NodeIndex).Should(Equal(-1)) + }) + }) + + Describe("when an AfterEach fails/panics", func() { + BeforeEach(func() { + success, _ := RunFixture("failed after each", func() { + BeforeEach(rt.T("bef-1")) + JustBeforeEach(rt.T("jus-bef-1")) + Describe("top-level", func() { + BeforeEach(rt.T("bef-2")) + Describe("nested", func() { + BeforeEach(rt.T("bef-3")) + It("the test", rt.T("it")) + JustAfterEach(rt.T("jus-aft-3")) + AfterEach(rt.T("aft-3", func() { + F("fail", cl) + })) + }) + JustAfterEach(rt.T("jus-aft-2")) + AfterEach(rt.T("aft-2", func() { + writer.Write([]byte("aft-2 is running")) + })) + }) + JustAfterEach(rt.T("jus-aft-1")) + AfterEach(rt.T("aft-1")) + }) + Ω(success).Should(BeFalse()) + }) + + It("reports a suite failure and a spec failure", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(1), NPassed(0), NFailed(1))) + specReport := reporter.Did.Find("the test") + Ω(specReport).Should(HaveFailed("fail", cl), CapturedOutput("aft-2 is running")) + Ω(specReport.Failure.NodeType).Should(Equal(types.NodeTypeAfterEach)) + }) + + It("matches up the nesting level correctly", func() { + report := reporter.Did.Find("the test") + Ω(report.NodeTexts[report.Failure.NodeIndex]).Should(Equal("nested")) + }) + + It("runs the subsequent after eaches", func() { + Ω(rt).Should(HaveTracked("bef-1", "bef-2", "bef-3", "jus-bef-1", "it", "jus-aft-3", "jus-aft-2", "jus-aft-1", "aft-3", "aft-2", "aft-1")) + }) + }) + + Describe("when multiple nodes within a given test run and fail", func() { + var clA, clB types.CodeLocation + BeforeEach(func() { + clA = types.CodeLocation{FileName: "A"} + clB = types.CodeLocation{FileName: "B"} + success, _ := RunFixture("failed after each", func() { + BeforeEach(rt.T("bef-1", func() { + writer.Write([]byte("run A")) + F("fail-A", clA) + })) + It("the test", rt.T("it")) + AfterEach(rt.T("aft-1", func() { + writer.Write([]byte("run B")) + F("fail-B", clB) + })) + }) + Ω(success).Should(BeFalse()) + }) + + It("reports a suite failure and a spec failure and only tracks the first failure", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(1), NPassed(0), NFailed(1))) + specReport := reporter.Did.Find("the test") + Ω(specReport).Should(HaveFailed("fail-A", clA), CapturedOutput("run Arun B")) + Ω(specReport.Failure.NodeType).Should(Equal(types.NodeTypeBeforeEach)) + Ω(rt).Should(HaveTracked("bef-1", "aft-1")) + }) + }) + }) + + Describe("when there are multiple tests that fail", func() { + BeforeEach(func() { + success, _ := RunFixture("failed after each", func() { + It("A", func() { F() }) + It("B", func() { F() }) + It("C", func() {}) + It("D", func() { F() }) + It("E", func() {}) + It("F", func() { panic("boom") }) + }) + Ω(success).Should(BeFalse()) + }) + + It("reports the correct number of failures", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(6), NPassed(2), NFailed(4))) + Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(ConsistOf("C", "E")) + Ω(reporter.Did.WithState(types.SpecStateFailed).Names()).Should(ConsistOf("A", "B", "D")) + Ω(reporter.Did.WithState(types.SpecStatePanicked).Names()).Should(ConsistOf("F")) + }) + }) +}) diff --git a/internal/internal_integration/focus_test.go b/internal/internal_integration/focus_test.go new file mode 100644 index 0000000000..3dfc702a84 --- /dev/null +++ b/internal/internal_integration/focus_test.go @@ -0,0 +1,220 @@ +package internal_integration_test + +import ( + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + + . "github.com/onsi/ginkgo/internal/test_helpers" +) + +var _ = Describe("Focus", func() { + Describe("when a suite has pending tests", func() { + fixture := func() { + It("A", rt.T("A")) + It("B", rt.T("B")) + PIt("C", rt.T("C")) + Describe("container", func() { + It("D", rt.T("D")) + }) + PDescribe("pending container", func() { + It("E", rt.T("E")) + It("F", rt.T("F")) + }) + } + Context("without config.FailOnPending", func() { + BeforeEach(func() { + success, hPF := RunFixture("pending tests", fixture) + Ω(success).Should(BeTrue()) + Ω(hPF).Should(BeFalse()) + }) + + It("does not run the pending tests", func() { + Ω(rt.TrackedRuns()).Should(ConsistOf("A", "B", "D")) + }) + + It("reports on the pending tests", func() { + Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(ConsistOf("A", "B", "D")) + Ω(reporter.Did.WithState(types.SpecStatePending).Names()).Should(ConsistOf("C", "E", "F")) + }) + + It("reports on the suite with accurate numbers", func() { + Ω(reporter.End).Should(BeASuiteSummary(true, NSpecs(6), NPassed(3), NPending(3), NWillRun(3), NSkipped(3))) + }) + }) + + Context("with config.FailOnPending", func() { + BeforeEach(func() { + conf.FailOnPending = true + success, hPF := RunFixture("pending tests", fixture) + Ω(success).Should(BeFalse()) + Ω(hPF).Should(BeFalse()) + }) + + It("reports on the suite with accurate numbers", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NPassed(3), NSpecs(6), NPending(3), NWillRun(3), NSkipped(3))) + }) + }) + }) + + Describe("with programmatic focus", func() { + var success bool + var hasProgrammaticFocus bool + BeforeEach(func() { + success, hasProgrammaticFocus = RunFixture("focused tests", func() { + It("A", rt.T("A")) + It("B", rt.T("B")) + FDescribe("focused container", func() { + It("C", rt.T("C")) + It("D", rt.T("D")) + PIt("E", rt.T("E")) + }) + FDescribe("focused container with focused child", func() { + It("F", rt.T("F")) + FIt("G", rt.T("G")) + }) + Describe("container", func() { + It("H", rt.T("H")) + }) + FIt("I", rt.T("I")) + }) + Ω(success).Should(BeTrue()) + }) + + It("should return true for hasProgrammaticFocus", func() { + Ω(hasProgrammaticFocus).Should(BeTrue()) + }) + + It("should run the focused tests, honoring the nested focus policy", func() { + Ω(rt.TrackedRuns()).Should(ConsistOf("C", "D", "G", "I")) + }) + + It("should report on the tests correctly", func() { + Ω(reporter.Did.WithState(types.SpecStateSkipped).Names()).Should(ConsistOf("A", "B", "F", "H")) + Ω(reporter.Did.WithState(types.SpecStatePending).Names()).Should(ConsistOf("E")) + Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(ConsistOf("C", "D", "G", "I")) + }) + + It("report on the suite with accurate numbers", func() { + Ω(reporter.End).Should(BeASuiteSummary(true, NPassed(4), NSkipped(5), NPending(1), NSpecs(9), NWillRun(4))) + }) + }) + + Describe("with config.FocusStrings and config.SkipStrings", func() { + BeforeEach(func() { + conf.FocusStrings = []string{"blue", "green"} + conf.SkipStrings = []string{"red"} + success, _ := RunFixture("cli focus tests", func() { + It("blue.1", rt.T("blue.1")) + It("blue.2", rt.T("blue.2")) + Describe("blue.container", func() { + It("yellow.1", rt.T("yellow.1")) + It("red.1", rt.T("red.1")) + PIt("blue.3", rt.T("blue.3")) + }) + Describe("green.container", func() { + It("yellow.2", rt.T("yellow.2")) + It("green.1", rt.T("green.1")) + }) + Describe("red.2", func() { + It("green.2", rt.T("green.2")) + }) + FIt("red.3", rt.T("red.3")) + }) + Ω(success).Should(BeTrue()) + }) + + It("should run tests that match", func() { + Ω(rt.TrackedRuns()).Should(ConsistOf("blue.1", "blue.2", "yellow.1", "yellow.2", "green.1")) + }) + + It("should report on the tests correctly", func() { + Ω(reporter.Did.WithState(types.SpecStateSkipped).Names()).Should(ConsistOf("red.1", "green.2", "red.3")) + Ω(reporter.Did.WithState(types.SpecStatePending).Names()).Should(ConsistOf("blue.3")) + Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(ConsistOf("blue.1", "blue.2", "yellow.1", "yellow.2", "green.1")) + }) + + It("report on the suite with accurate numbers", func() { + Ω(reporter.End).Should(BeASuiteSummary(true, NPassed(5), NSkipped(4), NPending(1), NSpecs(9), NWillRun(5))) + }) + }) + + Describe("when no tests will end up running", func() { + BeforeEach(func() { + conf.FocusStrings = []string{"red"} + success, _ := RunFixture("cli focus tests", func() { + BeforeSuite(rt.T("bef-suite")) + AfterSuite(rt.T("aft-suite")) + It("blue.1", rt.T("blue.1")) + It("blue.2", rt.T("blue.2")) + }) + Ω(success).Should(BeTrue()) + }) + + It("does not run the BeforeSuite or the AfterSuite", func() { + Ω(rt).Should(HaveTrackedNothing()) + }) + }) + + Describe("with config.RegexScansFilePath", func() { + BeforeEach(func() { + l := types.NewCodeLocation(0) + conf.FocusStrings = []string{l.FileName} + conf.RegexScansFilePath = true + success, _ := RunFixture("regex scans file path tests", func() { + It("A", rt.T("A")) + FIt("B", rt.T("B")) + PIt("C", rt.T("C")) + }) + Ω(success).Should(BeTrue()) + }) + + It("includes the filename when filtering", func() { + Ω(rt.TrackedRuns()).Should(ConsistOf("A", "B")) + Ω(reporter.Did.WithState(types.SpecStatePending).Names()).Should(ConsistOf("C")) + Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(ConsistOf("A", "B")) + Ω(reporter.End).Should(BeASuiteSummary(true, NPassed(2), NSkipped(1), NPending(1), NSpecs(3), NWillRun(2))) + }) + }) + + Describe("Skip()", func() { + BeforeEach(func() { + success, _ := RunFixture("Skip() tests", func() { + Describe("container to ensure order", func() { + It("A", rt.T("A")) + Describe("container", func() { + BeforeEach(rt.T("bef", func() { + failer.Skip("skip in Bef", cl) + panic("boom") //simulates what Ginkgo DSL does + })) + It("B", rt.T("B")) + It("C", rt.T("C")) + AfterEach(rt.T("aft")) + }) + It("D", rt.T("D", func() { + failer.Skip("skip D", cl) + panic("boom") //simulates what Ginkgo DSL does + })) + }) + }) + + Ω(success).Should(BeTrue()) + }) + + It("skips the tests that are Skipped()", func() { + Ω(rt).Should(HaveTracked("A", "bef", "aft", "bef", "aft", "D")) + Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(ConsistOf("A")) + Ω(reporter.Did.WithState(types.SpecStateSkipped).Names()).Should(ConsistOf("B", "C", "D")) + + Ω(reporter.Did.Find("B").Failure.Message).Should(Equal("skip in Bef")) + Ω(reporter.Did.Find("B").Failure.Location).Should(Equal(cl)) + + Ω(reporter.Did.Find("D").Failure.Message).Should(Equal("skip D")) + Ω(reporter.Did.Find("D").Failure.Location).Should(Equal(cl)) + }) + + It("report on the suite with accurate numbers", func() { + Ω(reporter.End).Should(BeASuiteSummary(true, NPassed(1), NSkipped(3), NPending(0), NSpecs(4), NWillRun(4))) + }) + }) +}) diff --git a/internal/internal_integration/internal_integration_suite_test.go b/internal/internal_integration/internal_integration_suite_test.go new file mode 100644 index 0000000000..318c8a3f92 --- /dev/null +++ b/internal/internal_integration/internal_integration_suite_test.go @@ -0,0 +1,128 @@ +package internal_integration_test + +import ( + "reflect" + "sync" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/test_helpers" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal" + "github.com/onsi/ginkgo/internal/global" +) + +func TestSuiteTests(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Suite Integration Tests") +} + +var conf config.GinkgoConfigType +var failer *internal.Failer +var writer *internal.Writer +var writerBuffer *gbytes.Buffer +var reporter *FakeReporter +var rt *RunTracker +var cl types.CodeLocation +var interruptHandler *FakeInterruptHandler + +var _ = BeforeEach(func() { + conf = config.GinkgoConfigType{} + failer = internal.NewFailer() + writerBuffer = gbytes.NewBuffer() + writer = internal.NewWriter(writerBuffer) + writer.SetStream(false) + reporter = &FakeReporter{} + rt = NewRunTracker() + cl = types.NewCodeLocation(0) + interruptHandler = NewFakeInterruptHandler() + + conf.ParallelTotal = 1 + conf.ParallelNode = 1 +}) + +/* Helpers to set up and run test fixtures using the Ginkgo DSL */ +func WithSuite(suite *internal.Suite, callback func()) { + originalSuite := global.Suite + global.Suite = suite + callback() + global.Suite = originalSuite +} + +func RunFixture(description string, callback func()) (bool, bool) { + suite := internal.NewSuite() + var success, hasProgrammaticFocus bool + WithSuite(suite, func() { + callback() + Ω(suite.BuildTree()).Should(Succeed()) + success, hasProgrammaticFocus = suite.Run(description, failer, reporter, writer, interruptHandler, conf) + }) + return success, hasProgrammaticFocus +} + +func F(options ...interface{}) { + location := cl + message := "fail" + for _, option := range options { + if reflect.TypeOf(option).Kind() == reflect.String { + message = option.(string) + } else if reflect.TypeOf(option) == reflect.TypeOf(cl) { + location = option.(types.CodeLocation) + } + } + + failer.Fail(message, location) + panic("panic to simulate how ginkgo's Fail works") +} + +/* InterruptHandler */ + +type FakeInterruptHandler struct { + triggerInterrupt chan bool + + c chan interface{} + lock *sync.Mutex + interrupted bool +} + +func NewFakeInterruptHandler() *FakeInterruptHandler { + handler := &FakeInterruptHandler{ + triggerInterrupt: make(chan bool), + c: make(chan interface{}), + lock: &sync.Mutex{}, + interrupted: false, + } + handler.registerForInterrupts() + return handler +} + +func (handler *FakeInterruptHandler) registerForInterrupts() { + go func() { + for { + <-handler.triggerInterrupt + handler.lock.Lock() + handler.interrupted = true + close(handler.c) + handler.c = make(chan interface{}) + handler.lock.Unlock() + } + }() +} + +func (handler *FakeInterruptHandler) Interrupt() { + handler.triggerInterrupt <- true +} + +func (handler *FakeInterruptHandler) Status() internal.InterruptStatus { + handler.lock.Lock() + defer handler.lock.Unlock() + + return internal.InterruptStatus{ + Interrupted: handler.interrupted, + Channel: handler.c, + } +} diff --git a/internal/internal_integration/interrupt_test.go b/internal/internal_integration/interrupt_test.go new file mode 100644 index 0000000000..79c4fee250 --- /dev/null +++ b/internal/internal_integration/interrupt_test.go @@ -0,0 +1,99 @@ +package internal_integration_test + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/test_helpers" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("When a test suite is interrupted", func() { + Describe("when it is interrupted in a BeforeSuite", func() { + BeforeEach(func() { + success, _ := RunFixture("interrupted test", func() { + BeforeSuite(rt.T("before-suite", func() { + interruptHandler.Interrupt() + time.Sleep(time.Hour) + })) + AfterSuite(rt.T("after-suite")) + It("A", rt.T("A")) + It("B", rt.T("B")) + }) + Ω(success).Should(Equal(false)) + }) + + It("runs the AfterSuite and skips all the tests", func() { + Ω(rt).Should(HaveTracked("before-suite", "after-suite")) + Ω(reporter.Did.FindByLeafNodeType(types.NodeTypeIt)).Should(BeZero()) + }) + + It("reports the correct failure", func() { + summary := reporter.Did.FindByLeafNodeType(types.NodeTypeBeforeSuite) + Ω(summary.State).Should(Equal(types.SpecStateInterrupted)) + Ω(summary.Failure.Message).Should(Equal("interrupted by user")) + }) + + It("reports the correct statistics", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(2), NWillRun(2), NPassed(0), NSkipped(2), NFailed(0))) + }) + }) + + Describe("when it is interrupted in a test", func() { + BeforeEach(func() { + conf.FlakeAttempts = 3 + success, _ := RunFixture("interrupted test", func() { + BeforeSuite(rt.T("before-suite")) + AfterSuite(rt.T("after-suite")) + BeforeEach(rt.T("bef.1")) + AfterEach(rt.T("aft.1")) + Describe("container", func() { + BeforeEach(rt.T("bef.2")) + AfterEach(rt.T("aft.2")) + It("runs", rt.T("runs")) + Describe("nested-container", func() { + BeforeEach(rt.T("bef.3-interrupt!", func() { + interruptHandler.Interrupt() + time.Sleep(time.Hour) + })) + AfterEach(rt.T("aft.3a")) + AfterEach(rt.T("aft.3b", func() { + interruptHandler.Interrupt() + time.Sleep(time.Hour) + })) + Describe("deeply-nested-container", func() { + BeforeEach(rt.T("bef.4")) + AfterEach(rt.T("aft.4")) + It("the interrupted test", rt.T("the interrupted test")) + It("skipped.1", rt.T("skipped.1")) + }) + }) + It("skipped.2", rt.T("skipped.2")) + }) + }) + Ω(success).Should(Equal(false)) + }) + + It("unwinds the after eaches at the appropriate nesting level, allowing additional interrupts of after eaches as it goes", func() { + Ω(rt).Should(HaveTracked("before-suite", + "bef.1", "bef.2", "runs", "aft.2", "aft.1", + "bef.1", "bef.2", "bef.3-interrupt!", "aft.3a", "aft.3b", "aft.2", "aft.1", + "after-suite")) + }) + + It("skips subsequent tests", func() { + Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(ConsistOf("runs")) + Ω(reporter.Did.WithState(types.SpecStateInterrupted).Names()).Should(ConsistOf("the interrupted test")) + Ω(reporter.Did.WithState(types.SpecStateSkipped).Names()).Should(ConsistOf("skipped.1", "skipped.2")) + }) + + It("reports the interrupted test as interrupted", func() { + Ω(reporter.Did.Find("the interrupted test").Failure.Message).Should(Equal("interrupted by user")) + }) + + It("reports the correct statistics", func() { + Ω(reporter.End).Should(BeASuiteSummary(false, NSpecs(4), NWillRun(4), NPassed(1), NSkipped(2), NFailed(1))) + }) + }) +}) diff --git a/internal/internal_integration/parallel_test.go b/internal/internal_integration/parallel_test.go new file mode 100644 index 0000000000..a323ec8f63 --- /dev/null +++ b/internal/internal_integration/parallel_test.go @@ -0,0 +1,157 @@ +package internal_integration_test + +import ( + "net/http" + "sync" + "time" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal" + "github.com/onsi/ginkgo/internal/parallel_support" + . "github.com/onsi/ginkgo/internal/test_helpers" + . "github.com/onsi/gomega" +) + +var _ = Describe("Running tests in parallel", func() { + var conf2 config.GinkgoConfigType + var reporter2 *FakeReporter + var rt2 *RunTracker + + var fixture = func(rt *RunTracker) { + SynchronizedBeforeSuite(func() []byte { + rt.Run("before-suite-1") + return []byte("floop") + }, func(node1Data []byte) { + rt.Run("before-suite-2 " + string(node1Data)) + }) + + It("A", rt.T("A", func() { + time.Sleep(10 * time.Millisecond) + })) + It("B", rt.T("B", func() { + time.Sleep(10 * time.Millisecond) + })) + It("C", rt.T("C", func() { + time.Sleep(10 * time.Millisecond) + })) + It("D", rt.T("D", func() { + time.Sleep(10 * time.Millisecond) + })) + It("E", rt.T("E", func() { + time.Sleep(10 * time.Millisecond) + })) + It("F", rt.T("F", func() { + time.Sleep(10 * time.Millisecond) + })) + + SynchronizedAfterSuite(rt.T("after-suite-1"), rt.T("after-suite-2")) + } + + BeforeEach(func() { + //set up configuration for node 1 and node 2 + conf.ParallelTotal = 2 + conf.ParallelNode = 1 + conf2 = config.GinkgoConfigType{ + ParallelTotal: 2, + ParallelNode: 2, + } + + // start up a remote server - we're using the real thing here, not a fake + server, err := parallel_support.NewServer(2, &FakeReporter{}) + Ω(err).ShouldNot(HaveOccurred()) + server.Start() + + // we're using a SynchronizedAfterSuite and making sure it runs on the correct nodes + // so we need to pass the server these "alive" callbacks. + // in real life the ginkgo cli sets these up and monitors the running processes + // here we do the same but are simply monitoring the running goroutines + aliveState := &sync.Map{} + for i := 1; i <= 2; i += 1 { + node := i + aliveState.Store(node, true) + server.RegisterAlive(node, func() bool { + alive, _ := aliveState.Load(node) + return alive.(bool) + }) + } + + // wait for the server to come up + Eventually(StatusCodePoller(server.Address() + "/up")).Should(Equal(http.StatusOK)) + conf.ParallelHost = server.Address() + conf2.ParallelHost = server.Address() + + // construct suite 1... + suite1 := internal.NewSuite() + WithSuite(suite1, func() { + fixture(rt) + Ω(suite1.BuildTree()).Should(Succeed()) + }) + + //now construct suite 2... + suite2 := internal.NewSuite() + rt2 = NewRunTracker() + WithSuite(suite2, func() { + fixture(rt2) + Ω(suite2.BuildTree()).Should(Succeed()) + }) + + finished := make(chan bool) + + //now launch suite 1... + go func() { + success, _ := suite1.Run("node 1", failer, reporter, writer, interruptHandler, conf) + finished <- success + aliveState.Store(1, false) + }() + + //and launch suite 2... + reporter2 = &FakeReporter{} + go func() { + success, _ := suite2.Run("node 2", internal.NewFailer(), reporter2, writer, interruptHandler, conf2) + finished <- success + aliveState.Store(2, false) + }() + + // eventually both suites should finish (and succeed)... + Eventually(finished).Should(Receive(Equal(true))) + Eventually(finished).Should(Receive(Equal(true))) + + // ...so we can safely shut down the server + server.Close() + + // and now we're ready to make asserts on the various run trackers and reporters + }) + + It("distributes tests across the parallel nodes and runs them", func() { + Ω(rt).Should(HaveRun("before-suite-1")) + Ω(rt).Should(HaveRun("before-suite-2 floop")) + Ω(rt).Should(HaveRun("after-suite-1")) + Ω(rt).Should(HaveRun("after-suite-2")) + + Ω(rt2).ShouldNot(HaveRun("before-suite-1")) + Ω(rt2).Should(HaveRun("before-suite-2 floop")) + Ω(rt2).Should(HaveRun("after-suite-1")) + Ω(rt2).ShouldNot(HaveRun("after-suite-2")) + + allRuns := append(rt.TrackedRuns(), rt2.TrackedRuns()...) + Ω(allRuns).Should(ConsistOf( + "before-suite-1", "before-suite-2 floop", "after-suite-1", "after-suite-2", "before-suite-2 floop", "after-suite-1", + "A", "B", "C", "D", "E", "F", //all ran + )) + + Ω(reporter.Did.Names()).ShouldNot(BeEmpty()) + Ω(reporter2.Did.Names()).ShouldNot(BeEmpty()) + names := append(reporter.Did.Names(), reporter2.Did.Names()...) + Ω(names).Should(ConsistOf("A", "B", "C", "D", "E", "F")) + }) + + It("reports the correct statistics", func() { + Ω(reporter.End.NumberOfTotalSpecs).Should(Equal(6)) + Ω(reporter2.End.NumberOfTotalSpecs).Should(Equal(6)) + Ω(reporter.End.NumberOfSpecsThatWillBeRun).Should(Equal(6)) + Ω(reporter2.End.NumberOfSpecsThatWillBeRun).Should(Equal(6)) + + Ω(reporter.End.NumberOfPassedSpecs + reporter2.End.NumberOfPassedSpecs).Should(Equal(6)) + }) +}) diff --git a/internal/internal_integration/run_test.go b/internal/internal_integration/run_test.go new file mode 100644 index 0000000000..d7ecaaf236 --- /dev/null +++ b/internal/internal_integration/run_test.go @@ -0,0 +1,133 @@ +package internal_integration_test + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/internal/test_helpers" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("Running Tests in Series - the happy path", func() { + BeforeEach(func() { + success, hPF := RunFixture("happy-path run suite", func() { + BeforeSuite(rt.T("before-suite", func() { + time.Sleep(10 * time.Millisecond) + writer.Write([]byte("before-suite\n")) + })) + AfterSuite(rt.T("after-suite", func() { + time.Sleep(20 * time.Millisecond) + })) + Describe("top-level-container", func() { + JustBeforeEach(rt.T("just-before-each")) + BeforeEach(rt.T("before-each", func() { + writer.Write([]byte("before-each\n")) + })) + AfterEach(rt.T("after-each")) + AfterEach(rt.T("after-each-2")) + JustAfterEach(rt.T("just-after-each")) + It("A", rt.T("A", func() { + time.Sleep(10 * time.Millisecond) + })) + It("B", rt.T("B", func() { + time.Sleep(20 * time.Millisecond) + })) + Describe("nested-container", func() { + JustBeforeEach(rt.T("nested-just-before-each")) + BeforeEach(rt.T("nested-before-each")) + AfterEach(rt.T("nested-after-each")) + JustAfterEach(rt.T("nested-just-after-each")) + JustAfterEach(rt.T("nested-just-after-each-2")) + It("C", rt.T("C", func() { + writer.Write([]byte("C\n")) + })) + It("D", rt.T("D")) + }) + }) + }) + Ω(success).Should(BeTrue()) + Ω(hPF).Should(BeFalse()) + }) + + It("runs all the test nodes in the expected order", func() { + Ω(rt).Should(HaveTracked( + "before-suite", + "before-each", "just-before-each", "A", "just-after-each", "after-each", "after-each-2", + "before-each", "just-before-each", "B", "just-after-each", "after-each", "after-each-2", + "before-each", "nested-before-each", "just-before-each", "nested-just-before-each", "C", "nested-just-after-each", "nested-just-after-each-2", "just-after-each", "nested-after-each", "after-each", "after-each-2", + "before-each", "nested-before-each", "just-before-each", "nested-just-before-each", "D", "nested-just-after-each", "nested-just-after-each-2", "just-after-each", "nested-after-each", "after-each", "after-each-2", + "after-suite", + )) + }) + + Describe("reporting", func() { + It("reports the suite summary correctly when starting", func() { + Ω(reporter.Begin).Should(MatchFields(IgnoreExtras, Fields{ + "SuiteDescription": Equal("happy-path run suite"), + "SuiteSucceeded": BeFalse(), + "NumberOfTotalSpecs": Equal(4), + "NumberOfSpecsThatWillBeRun": Equal(4), + })) + }) + + It("reports the suite summary correctly when complete", func() { + Ω(reporter.End).Should(MatchFields(IgnoreExtras, Fields{ + "SuiteDescription": Equal("happy-path run suite"), + "SuiteSucceeded": BeTrue(), + "NumberOfTotalSpecs": Equal(4), + "NumberOfSpecsThatWillBeRun": Equal(4), + "NumberOfSkippedSpecs": Equal(0), + "NumberOfPassedSpecs": Equal(4), + "NumberOfFailedSpecs": Equal(0), + "NumberOfPendingSpecs": Equal(0), + "NumberOfFlakedSpecs": Equal(0), + "RunTime": BeNumerically(">=", time.Millisecond*(10+20+10+20)), + })) + }) + + It("reports the correct suite node summaries", func() { + Ω(reporter.Did.FindByLeafNodeType(types.NodeTypeBeforeSuite)).Should(MatchFields(IgnoreExtras, Fields{ + "LeafNodeType": Equal(types.NodeTypeBeforeSuite), + "State": Equal(types.SpecStatePassed), + "RunTime": BeNumerically(">=", 10*time.Millisecond), + "Failure": BeZero(), + "CapturedGinkgoWriterOutput": Equal("before-suite\n"), + })) + + Ω(reporter.Did.FindByLeafNodeType(types.NodeTypeAfterSuite)).Should(MatchFields(IgnoreExtras, Fields{ + "LeafNodeType": Equal(types.NodeTypeAfterSuite), + "State": Equal(types.SpecStatePassed), + "RunTime": BeNumerically(">=", 20*time.Millisecond), + "Failure": BeZero(), + "CapturedGinkgoWriterOutput": BeZero(), + })) + }) + + It("reports about each just before it runs", func() { + Ω(reporter.Will.Names()).Should(Equal([]string{"A", "B", "C", "D"})) + }) + + It("reports about each test after it completes", func() { + Ω(reporter.Did.Names()).Should(Equal([]string{"A", "B", "C", "D"})) + Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(Equal([]string{"A", "B", "C", "D"})) + + //spot-check + Ω(reporter.Did.Find("C")).Should(MatchFields(IgnoreExtras, Fields{ + "LeafNodeType": Equal(types.NodeTypeIt), + "NodeTexts": Equal([]string{"top-level-container", "nested-container", "C"}), + "State": Equal(types.SpecStatePassed), + "Failure": BeZero(), + "NumAttempts": Equal(1), + "CapturedGinkgoWriterOutput": Equal("before-each\nC\n"), + })) + }) + + It("computes run times", func() { + Ω(reporter.Did.Find("A").RunTime).Should(BeNumerically(">=", 10*time.Millisecond)) + Ω(reporter.Did.Find("B").RunTime).Should(BeNumerically(">=", 20*time.Millisecond)) + }) + }) +}) diff --git a/internal/internal_integration/shuffle_test.go b/internal/internal_integration/shuffle_test.go new file mode 100644 index 0000000000..6ce917e39f --- /dev/null +++ b/internal/internal_integration/shuffle_test.go @@ -0,0 +1,89 @@ +package internal_integration_test + +import ( + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Shuffling Tests", func() { + var fixture = func() { + Describe("container-a", func() { + It("a.1", rt.T("a.1")) + It("a.2", rt.T("a.2")) + It("a.3", rt.T("a.3")) + It("a.4", rt.T("a.4")) + }) + + Describe("container-b", func() { + It("b.1", rt.T("b.1")) + It("b.2", rt.T("b.2")) + It("b.3", rt.T("b.3")) + It("b.4", rt.T("b.4")) + }) + + It("top.1", rt.T("top.1")) + It("top.2", rt.T("top.2")) + It("top.3", rt.T("top.3")) + It("top.4", rt.T("top.4")) + } + + Describe("the default behavior", func() { + It("shuffles top-level containers and its only, preserving the order of tests within containers", func() { + orderings := []string{} + uniqueOrderings := map[string]bool{} + for i := 0; i < 10; i += 1 { + conf.RandomSeed = int64(i) + RunFixture("run", fixture) + order := strings.Join(rt.TrackedRuns(), "") + rt.Reset() + + Ω(order).Should(ContainSubstring("a.1a.2a.3a.4"), "order in containers should be preserved") + Ω(order).Should(ContainSubstring("b.1b.2b.3b.4"), "order in containers should be preserved") + orderings = append(orderings, order) + uniqueOrderings[order] = true + } + Ω(orderings).Should(ContainElement(Not(ContainSubstring("top.1top.2top.3top.4"))), "top-level its should be randomized") + Ω(uniqueOrderings).ShouldNot(HaveLen(1), "after 10 runs at least a few should be different!") + }) + }) + + Describe("when told to randomize all specs", func() { + It("shuffles all its", func() { + conf.RandomizeAllSpecs = true + orderings := []string{} + uniqueOrderings := map[string]bool{} + for i := 0; i < 10; i += 1 { + conf.RandomSeed = int64(i) + RunFixture("run", fixture) + order := strings.Join(rt.TrackedRuns(), "") + rt.Reset() + + orderings = append(orderings, order) + uniqueOrderings[order] = true + } + Ω(orderings).Should(ContainElement(Not(ContainSubstring("top.1top.2top.3top.4"))), "top-level its should be randomized") + Ω(orderings).Should(ContainElement(Not(ContainSubstring("a.1a.2a.3a.4"))), "its in containers should be randomized") + Ω(orderings).Should(ContainElement(Not(ContainSubstring("b.1b.2b.3b.4"))), "its in containers should be randomized") + Ω(uniqueOrderings).ShouldNot(HaveLen(1), "after 10 runs at least a few should be different!") + }) + }) + + Describe("when given the same seed", func() { + It("yields the same order", func() { + for _, conf.RandomizeAllSpecs = range []bool{true, false} { + uniqueOrderings := map[string]bool{} + for i := 0; i < 10; i += 1 { + conf.RandomSeed = 1138 + RunFixture("run", fixture) + order := strings.Join(rt.TrackedRuns(), "") + rt.Reset() + uniqueOrderings[order] = true + } + + Ω(uniqueOrderings).Should(HaveLen(1), "all orders are the same") + } + }) + }) +}) diff --git a/internal/internal_suite_test.go b/internal/internal_suite_test.go new file mode 100644 index 0000000000..0ed240b9d5 --- /dev/null +++ b/internal/internal_suite_test.go @@ -0,0 +1,109 @@ +package internal_test + +import ( + "reflect" + "testing" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/internal" +) + +func TestInternal(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Internal Suite") +} + +type Node = internal.Node +type Nodes = internal.Nodes +type NodeType = types.NodeType +type TreeNode = internal.TreeNode +type TreeNodes = internal.TreeNodes +type Spec = internal.Spec +type Specs = internal.Specs + +var ntIt = types.NodeTypeIt +var ntCon = types.NodeTypeContainer +var ntAf = types.NodeTypeAfterEach +var ntBef = types.NodeTypeBeforeEach +var ntJusAf = types.NodeTypeJustAfterEach +var ntJusBef = types.NodeTypeJustBeforeEach + +type NestingLevel int +type MarkedPending bool +type MarkedFocus bool + +// convenience helper to quickly make nodes +func N(options ...interface{}) Node { + node := internal.NewNode(types.NodeTypeIt, "", nil, cl, false, false) + for _, option := range options { + if reflect.TypeOf(option).Kind() == reflect.String { + node.Text = option.(string) + } else if reflect.TypeOf(option) == reflect.TypeOf(types.NodeTypeInvalid) { + node.NodeType = option.(NodeType) + } else if reflect.TypeOf(option) == reflect.TypeOf(NestingLevel(1)) { + node.NestingLevel = int(option.(NestingLevel)) + } else if reflect.TypeOf(option) == reflect.TypeOf(cl) { + node.CodeLocation = option.(types.CodeLocation) + } else if reflect.TypeOf(option) == reflect.TypeOf(MarkedFocus(true)) { + node.MarkedFocus = bool(option.(MarkedFocus)) + } else if reflect.TypeOf(option) == reflect.TypeOf(MarkedPending(true)) { + node.MarkedPending = bool(option.(MarkedPending)) + } else if reflect.TypeOf(option).Kind() == reflect.Func { + node.Body = option.(func()) + } + } + return node +} + +// convenience helper to quickly make tree nodes +func TN(node Node, children ...TreeNode) TreeNode { + return TreeNode{ + Node: node, + Children: TreeNodes(children), + } +} + +// convenience helper to quickly make specs +func S(nodes ...Node) Spec { + return Spec{Nodes: nodes} +} + +// convenience helper to quickly make code locations +func CL(options ...interface{}) types.CodeLocation { + cl = types.NewCodeLocation(0) + for _, option := range options { + if reflect.TypeOf(option).Kind() == reflect.String { + cl.FileName = option.(string) + } else if reflect.TypeOf(option).Kind() == reflect.Int { + cl.LineNumber = option.(int) + } + } + return cl +} + +func mustFindNodeWithText(tree TreeNode, text string) Node { + node := findNodeWithText(tree, text) + ExpectWithOffset(1, node).ShouldNot(BeZero(), "Failed to find node in tree with text '%s'", text) + return node +} + +func findNodeWithText(tree TreeNode, text string) Node { + if tree.Node.Text == text { + return tree.Node + } + for _, tn := range tree.Children { + n := findNodeWithText(tn, text) + if !n.IsZero() { + return n + } + } + return Node{} +} + +var cl types.CodeLocation +var _ = BeforeEach(func() { + cl = types.NewCodeLocation(0) +}) diff --git a/internal/interrupt_handler.go b/internal/interrupt_handler.go new file mode 100644 index 0000000000..3c5fbe0997 --- /dev/null +++ b/internal/interrupt_handler.go @@ -0,0 +1,58 @@ +package internal + +import ( + "os" + "os/signal" + "sync" + "syscall" +) + +type InterruptStatus struct { + Interrupted bool + Channel chan interface{} +} + +type InterruptHandlerInterface interface { + Status() InterruptStatus +} + +type InterruptHandler struct { + c chan interface{} + lock *sync.Mutex + interrupted bool +} + +func NewInterruptHandler() *InterruptHandler { + handler := &InterruptHandler{ + c: make(chan interface{}), + lock: &sync.Mutex{}, + interrupted: false, + } + handler.registerForInterrupts() + return handler +} + +func (handler *InterruptHandler) registerForInterrupts() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + for { + <-c + handler.lock.Lock() + handler.interrupted = true + close(handler.c) + handler.c = make(chan interface{}) + handler.lock.Unlock() + } + }() +} + +func (handler *InterruptHandler) Status() InterruptStatus { + handler.lock.Lock() + defer handler.lock.Unlock() + + return InterruptStatus{ + Interrupted: handler.interrupted, + Channel: handler.c, + } +} diff --git a/internal/leafnodes/benchmarker.go b/internal/leafnodes/benchmarker.go deleted file mode 100644 index 393901e11c..0000000000 --- a/internal/leafnodes/benchmarker.go +++ /dev/null @@ -1,103 +0,0 @@ -package leafnodes - -import ( - "math" - "time" - - "sync" - - "github.com/onsi/ginkgo/types" -) - -type benchmarker struct { - mu sync.Mutex - measurements map[string]*types.SpecMeasurement - orderCounter int -} - -func newBenchmarker() *benchmarker { - return &benchmarker{ - measurements: make(map[string]*types.SpecMeasurement), - } -} - -func (b *benchmarker) Time(name string, body func(), info ...interface{}) (elapsedTime time.Duration) { - t := time.Now() - body() - elapsedTime = time.Since(t) - - b.mu.Lock() - defer b.mu.Unlock() - measurement := b.getMeasurement(name, "Fastest Time", "Slowest Time", "Average Time", "s", 3, info...) - measurement.Results = append(measurement.Results, elapsedTime.Seconds()) - - return -} - -func (b *benchmarker) RecordValue(name string, value float64, info ...interface{}) { - b.mu.Lock() - measurement := b.getMeasurement(name, "Smallest", " Largest", " Average", "", 3, info...) - defer b.mu.Unlock() - measurement.Results = append(measurement.Results, value) -} - -func (b *benchmarker) RecordValueWithPrecision(name string, value float64, units string, precision int, info ...interface{}) { - b.mu.Lock() - measurement := b.getMeasurement(name, "Smallest", " Largest", " Average", units, precision, info...) - defer b.mu.Unlock() - measurement.Results = append(measurement.Results, value) -} - -func (b *benchmarker) getMeasurement(name string, smallestLabel string, largestLabel string, averageLabel string, units string, precision int, info ...interface{}) *types.SpecMeasurement { - measurement, ok := b.measurements[name] - if !ok { - var computedInfo interface{} - computedInfo = nil - if len(info) > 0 { - computedInfo = info[0] - } - measurement = &types.SpecMeasurement{ - Name: name, - Info: computedInfo, - Order: b.orderCounter, - SmallestLabel: smallestLabel, - LargestLabel: largestLabel, - AverageLabel: averageLabel, - Units: units, - Precision: precision, - Results: make([]float64, 0), - } - b.measurements[name] = measurement - b.orderCounter++ - } - - return measurement -} - -func (b *benchmarker) measurementsReport() map[string]*types.SpecMeasurement { - b.mu.Lock() - defer b.mu.Unlock() - for _, measurement := range b.measurements { - measurement.Smallest = math.MaxFloat64 - measurement.Largest = -math.MaxFloat64 - sum := float64(0) - sumOfSquares := float64(0) - - for _, result := range measurement.Results { - if result > measurement.Largest { - measurement.Largest = result - } - if result < measurement.Smallest { - measurement.Smallest = result - } - sum += result - sumOfSquares += result * result - } - - n := float64(len(measurement.Results)) - measurement.Average = sum / n - measurement.StdDeviation = math.Sqrt(sumOfSquares/n - (sum/n)*(sum/n)) - } - - return b.measurements -} diff --git a/internal/leafnodes/interfaces.go b/internal/leafnodes/interfaces.go deleted file mode 100644 index 8c3902d601..0000000000 --- a/internal/leafnodes/interfaces.go +++ /dev/null @@ -1,19 +0,0 @@ -package leafnodes - -import ( - "github.com/onsi/ginkgo/types" -) - -type BasicNode interface { - Type() types.SpecComponentType - Run() (types.SpecState, types.SpecFailure) - CodeLocation() types.CodeLocation -} - -type SubjectNode interface { - BasicNode - - Text() string - Flag() types.FlagType - Samples() int -} diff --git a/internal/leafnodes/it_node.go b/internal/leafnodes/it_node.go deleted file mode 100644 index 6eded7b763..0000000000 --- a/internal/leafnodes/it_node.go +++ /dev/null @@ -1,47 +0,0 @@ -package leafnodes - -import ( - "time" - - "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -type ItNode struct { - runner *runner - - flag types.FlagType - text string -} - -func NewItNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *ItNode { - return &ItNode{ - runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeIt, componentIndex), - flag: flag, - text: text, - } -} - -func (node *ItNode) Run() (outcome types.SpecState, failure types.SpecFailure) { - return node.runner.run() -} - -func (node *ItNode) Type() types.SpecComponentType { - return types.SpecComponentTypeIt -} - -func (node *ItNode) Text() string { - return node.text -} - -func (node *ItNode) Flag() types.FlagType { - return node.flag -} - -func (node *ItNode) CodeLocation() types.CodeLocation { - return node.runner.codeLocation -} - -func (node *ItNode) Samples() int { - return 1 -} diff --git a/internal/leafnodes/it_node_test.go b/internal/leafnodes/it_node_test.go deleted file mode 100644 index 29fa0c6e2a..0000000000 --- a/internal/leafnodes/it_node_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package leafnodes_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/leafnodes" - . "github.com/onsi/gomega" - - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/types" -) - -var _ = Describe("It Nodes", func() { - It("should report the correct type, text, flag, and code location", func() { - codeLocation := codelocation.New(0) - it := NewItNode("my it node", func() {}, types.FlagTypeFocused, codeLocation, 0, nil, 3) - Ω(it.Type()).Should(Equal(types.SpecComponentTypeIt)) - Ω(it.Flag()).Should(Equal(types.FlagTypeFocused)) - Ω(it.Text()).Should(Equal("my it node")) - Ω(it.CodeLocation()).Should(Equal(codeLocation)) - Ω(it.Samples()).Should(Equal(1)) - }) -}) diff --git a/internal/leafnodes/leaf_node_suite_test.go b/internal/leafnodes/leaf_node_suite_test.go deleted file mode 100644 index a7ba9e006e..0000000000 --- a/internal/leafnodes/leaf_node_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package leafnodes_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestLeafNode(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "LeafNode Suite") -} diff --git a/internal/leafnodes/measure_node.go b/internal/leafnodes/measure_node.go deleted file mode 100644 index 3ab9a6d552..0000000000 --- a/internal/leafnodes/measure_node.go +++ /dev/null @@ -1,62 +0,0 @@ -package leafnodes - -import ( - "reflect" - - "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -type MeasureNode struct { - runner *runner - - text string - flag types.FlagType - samples int - benchmarker *benchmarker -} - -func NewMeasureNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, samples int, failer *failer.Failer, componentIndex int) *MeasureNode { - benchmarker := newBenchmarker() - - wrappedBody := func() { - reflect.ValueOf(body).Call([]reflect.Value{reflect.ValueOf(benchmarker)}) - } - - return &MeasureNode{ - runner: newRunner(wrappedBody, codeLocation, 0, failer, types.SpecComponentTypeMeasure, componentIndex), - - text: text, - flag: flag, - samples: samples, - benchmarker: benchmarker, - } -} - -func (node *MeasureNode) Run() (outcome types.SpecState, failure types.SpecFailure) { - return node.runner.run() -} - -func (node *MeasureNode) MeasurementsReport() map[string]*types.SpecMeasurement { - return node.benchmarker.measurementsReport() -} - -func (node *MeasureNode) Type() types.SpecComponentType { - return types.SpecComponentTypeMeasure -} - -func (node *MeasureNode) Text() string { - return node.text -} - -func (node *MeasureNode) Flag() types.FlagType { - return node.flag -} - -func (node *MeasureNode) CodeLocation() types.CodeLocation { - return node.runner.codeLocation -} - -func (node *MeasureNode) Samples() int { - return node.samples -} diff --git a/internal/leafnodes/measure_node_test.go b/internal/leafnodes/measure_node_test.go deleted file mode 100644 index 1cd13336af..0000000000 --- a/internal/leafnodes/measure_node_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package leafnodes_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/leafnodes" - . "github.com/onsi/gomega" - - "time" - - "github.com/onsi/ginkgo/internal/codelocation" - Failer "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -var _ = Describe("Measure Nodes", func() { - It("should report the correct type, text, flag, and code location", func() { - codeLocation := codelocation.New(0) - measure := NewMeasureNode("my measure node", func(b Benchmarker) {}, types.FlagTypeFocused, codeLocation, 10, nil, 3) - Ω(measure.Type()).Should(Equal(types.SpecComponentTypeMeasure)) - Ω(measure.Flag()).Should(Equal(types.FlagTypeFocused)) - Ω(measure.Text()).Should(Equal("my measure node")) - Ω(measure.CodeLocation()).Should(Equal(codeLocation)) - Ω(measure.Samples()).Should(Equal(10)) - }) - - Describe("benchmarking", func() { - var measure *MeasureNode - - Describe("Value", func() { - BeforeEach(func() { - measure = NewMeasureNode("the measurement", func(b Benchmarker) { - b.RecordValue("foo", 7, "info!") - b.RecordValue("foo", 2) - b.RecordValue("foo", 3) - b.RecordValue("bar", 0.3) - b.RecordValue("bar", 0.1) - b.RecordValue("bar", 0.5) - b.RecordValue("bar", 0.7) - }, types.FlagTypeFocused, codelocation.New(0), 1, Failer.New(), 3) - Ω(measure.Run()).Should(Equal(types.SpecStatePassed)) - }) - - It("records passed in values and reports on them", func() { - report := measure.MeasurementsReport() - Ω(report).Should(HaveLen(2)) - Ω(report["foo"].Name).Should(Equal("foo")) - Ω(report["foo"].Info).Should(Equal("info!")) - Ω(report["foo"].Order).Should(Equal(0)) - Ω(report["foo"].SmallestLabel).Should(Equal("Smallest")) - Ω(report["foo"].LargestLabel).Should(Equal(" Largest")) - Ω(report["foo"].AverageLabel).Should(Equal(" Average")) - Ω(report["foo"].Units).Should(Equal("")) - Ω(report["foo"].Results).Should(Equal([]float64{7, 2, 3})) - Ω(report["foo"].Smallest).Should(BeNumerically("==", 2)) - Ω(report["foo"].Largest).Should(BeNumerically("==", 7)) - Ω(report["foo"].Average).Should(BeNumerically("==", 4)) - Ω(report["foo"].StdDeviation).Should(BeNumerically("~", 2.16, 0.01)) - - Ω(report["bar"].Name).Should(Equal("bar")) - Ω(report["bar"].Info).Should(BeNil()) - Ω(report["bar"].SmallestLabel).Should(Equal("Smallest")) - Ω(report["bar"].Order).Should(Equal(1)) - Ω(report["bar"].LargestLabel).Should(Equal(" Largest")) - Ω(report["bar"].AverageLabel).Should(Equal(" Average")) - Ω(report["bar"].Units).Should(Equal("")) - Ω(report["bar"].Results).Should(Equal([]float64{0.3, 0.1, 0.5, 0.7})) - Ω(report["bar"].Smallest).Should(BeNumerically("==", 0.1)) - Ω(report["bar"].Largest).Should(BeNumerically("==", 0.7)) - Ω(report["bar"].Average).Should(BeNumerically("==", 0.4)) - Ω(report["bar"].StdDeviation).Should(BeNumerically("~", 0.22, 0.01)) - }) - }) - - Describe("Value with precision", func() { - BeforeEach(func() { - measure = NewMeasureNode("the measurement", func(b Benchmarker) { - b.RecordValueWithPrecision("foo", 7, "ms", 7, "info!") - b.RecordValueWithPrecision("foo", 2, "ms", 6) - b.RecordValueWithPrecision("foo", 3, "ms", 5) - b.RecordValueWithPrecision("bar", 0.3, "ns", 4) - b.RecordValueWithPrecision("bar", 0.1, "ns", 3) - b.RecordValueWithPrecision("bar", 0.5, "ns", 2) - b.RecordValueWithPrecision("bar", 0.7, "ns", 1) - }, types.FlagTypeFocused, codelocation.New(0), 1, Failer.New(), 3) - Ω(measure.Run()).Should(Equal(types.SpecStatePassed)) - }) - - It("records passed in values and reports on them", func() { - report := measure.MeasurementsReport() - Ω(report).Should(HaveLen(2)) - Ω(report["foo"].Name).Should(Equal("foo")) - Ω(report["foo"].Info).Should(Equal("info!")) - Ω(report["foo"].Order).Should(Equal(0)) - Ω(report["foo"].SmallestLabel).Should(Equal("Smallest")) - Ω(report["foo"].LargestLabel).Should(Equal(" Largest")) - Ω(report["foo"].AverageLabel).Should(Equal(" Average")) - Ω(report["foo"].Units).Should(Equal("ms")) - Ω(report["foo"].Results).Should(Equal([]float64{7, 2, 3})) - Ω(report["foo"].Smallest).Should(BeNumerically("==", 2)) - Ω(report["foo"].Largest).Should(BeNumerically("==", 7)) - Ω(report["foo"].Average).Should(BeNumerically("==", 4)) - Ω(report["foo"].StdDeviation).Should(BeNumerically("~", 2.16, 0.01)) - - Ω(report["bar"].Name).Should(Equal("bar")) - Ω(report["bar"].Info).Should(BeNil()) - Ω(report["bar"].SmallestLabel).Should(Equal("Smallest")) - Ω(report["bar"].Order).Should(Equal(1)) - Ω(report["bar"].LargestLabel).Should(Equal(" Largest")) - Ω(report["bar"].AverageLabel).Should(Equal(" Average")) - Ω(report["bar"].Units).Should(Equal("ns")) - Ω(report["bar"].Results).Should(Equal([]float64{0.3, 0.1, 0.5, 0.7})) - Ω(report["bar"].Smallest).Should(BeNumerically("==", 0.1)) - Ω(report["bar"].Largest).Should(BeNumerically("==", 0.7)) - Ω(report["bar"].Average).Should(BeNumerically("==", 0.4)) - Ω(report["bar"].StdDeviation).Should(BeNumerically("~", 0.22, 0.01)) - }) - }) - - Describe("Time", func() { - BeforeEach(func() { - measure = NewMeasureNode("the measurement", func(b Benchmarker) { - b.Time("foo", func() { - time.Sleep(200 * time.Millisecond) - }, "info!") - b.Time("foo", func() { - time.Sleep(300 * time.Millisecond) - }) - b.Time("foo", func() { - time.Sleep(250 * time.Millisecond) - }) - }, types.FlagTypeFocused, codelocation.New(0), 1, Failer.New(), 3) - Ω(measure.Run()).Should(Equal(types.SpecStatePassed)) - }) - - It("records passed in values and reports on them", func() { - report := measure.MeasurementsReport() - Ω(report).Should(HaveLen(1)) - Ω(report["foo"].Name).Should(Equal("foo")) - Ω(report["foo"].Info).Should(Equal("info!")) - Ω(report["foo"].SmallestLabel).Should(Equal("Fastest Time")) - Ω(report["foo"].LargestLabel).Should(Equal("Slowest Time")) - Ω(report["foo"].AverageLabel).Should(Equal("Average Time")) - Ω(report["foo"].Units).Should(Equal("s")) - Ω(report["foo"].Results).Should(HaveLen(3)) - Ω(report["foo"].Results[0]).Should(BeNumerically("~", 0.2, 0.06)) - Ω(report["foo"].Results[1]).Should(BeNumerically("~", 0.3, 0.06)) - Ω(report["foo"].Results[2]).Should(BeNumerically("~", 0.25, 0.06)) - Ω(report["foo"].Smallest).Should(BeNumerically("~", 0.2, 0.06)) - Ω(report["foo"].Largest).Should(BeNumerically("~", 0.3, 0.06)) - Ω(report["foo"].Average).Should(BeNumerically("~", 0.25, 0.06)) - Ω(report["foo"].StdDeviation).Should(BeNumerically("~", 0.07, 0.04)) - }) - }) - }) -}) diff --git a/internal/leafnodes/runner.go b/internal/leafnodes/runner.go deleted file mode 100644 index 16cb66c3e4..0000000000 --- a/internal/leafnodes/runner.go +++ /dev/null @@ -1,117 +0,0 @@ -package leafnodes - -import ( - "fmt" - "reflect" - "time" - - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -type runner struct { - isAsync bool - asyncFunc func(chan<- interface{}) - syncFunc func() - codeLocation types.CodeLocation - timeoutThreshold time.Duration - nodeType types.SpecComponentType - componentIndex int - failer *failer.Failer -} - -func newRunner(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, nodeType types.SpecComponentType, componentIndex int) *runner { - bodyType := reflect.TypeOf(body) - if bodyType.Kind() != reflect.Func { - panic(fmt.Sprintf("Expected a function but got something else at %v", codeLocation)) - } - - runner := &runner{ - codeLocation: codeLocation, - timeoutThreshold: timeout, - failer: failer, - nodeType: nodeType, - componentIndex: componentIndex, - } - - switch bodyType.NumIn() { - case 0: - runner.syncFunc = body.(func()) - return runner - case 1: - if !(bodyType.In(0).Kind() == reflect.Chan && bodyType.In(0).Elem().Kind() == reflect.Interface) { - panic(fmt.Sprintf("Must pass a Done channel to function at %v", codeLocation)) - } - - wrappedBody := func(done chan<- interface{}) { - bodyValue := reflect.ValueOf(body) - bodyValue.Call([]reflect.Value{reflect.ValueOf(done)}) - } - - runner.isAsync = true - runner.asyncFunc = wrappedBody - return runner - } - - panic(fmt.Sprintf("Too many arguments to function at %v", codeLocation)) -} - -func (r *runner) run() (outcome types.SpecState, failure types.SpecFailure) { - if r.isAsync { - return r.runAsync() - } else { - return r.runSync() - } -} - -func (r *runner) runAsync() (outcome types.SpecState, failure types.SpecFailure) { - done := make(chan interface{}, 1) - - go func() { - finished := false - - defer func() { - if e := recover(); e != nil || !finished { - r.failer.Panic(codelocation.New(2), e) - select { - case <-done: - break - default: - close(done) - } - } - }() - - r.asyncFunc(done) - finished = true - }() - - // If this goroutine gets no CPU time before the select block, - // the <-done case may complete even if the test took longer than the timeoutThreshold. - // This can cause flaky behaviour, but we haven't seen it in the wild. - select { - case <-done: - case <-time.After(r.timeoutThreshold): - r.failer.Timeout(r.codeLocation) - } - - failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation) - return -} -func (r *runner) runSync() (outcome types.SpecState, failure types.SpecFailure) { - finished := false - - defer func() { - if e := recover(); e != nil || !finished { - r.failer.Panic(codelocation.New(2), e) - } - - failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation) - }() - - r.syncFunc() - finished = true - - return -} diff --git a/internal/leafnodes/setup_nodes.go b/internal/leafnodes/setup_nodes.go deleted file mode 100644 index e3e9cb7c58..0000000000 --- a/internal/leafnodes/setup_nodes.go +++ /dev/null @@ -1,48 +0,0 @@ -package leafnodes - -import ( - "time" - - "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -type SetupNode struct { - runner *runner -} - -func (node *SetupNode) Run() (outcome types.SpecState, failure types.SpecFailure) { - return node.runner.run() -} - -func (node *SetupNode) Type() types.SpecComponentType { - return node.runner.nodeType -} - -func (node *SetupNode) CodeLocation() types.CodeLocation { - return node.runner.codeLocation -} - -func NewBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { - return &SetupNode{ - runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeBeforeEach, componentIndex), - } -} - -func NewAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { - return &SetupNode{ - runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeAfterEach, componentIndex), - } -} - -func NewJustBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { - return &SetupNode{ - runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeJustBeforeEach, componentIndex), - } -} - -func NewJustAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode { - return &SetupNode{ - runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeJustAfterEach, componentIndex), - } -} diff --git a/internal/leafnodes/setup_nodes_test.go b/internal/leafnodes/setup_nodes_test.go deleted file mode 100644 index 9810688cb4..0000000000 --- a/internal/leafnodes/setup_nodes_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package leafnodes_test - -import ( - . "github.com/onsi/ginkgo" - "github.com/onsi/ginkgo/types" - . "github.com/onsi/gomega" - - . "github.com/onsi/ginkgo/internal/leafnodes" - - "github.com/onsi/ginkgo/internal/codelocation" -) - -var _ = Describe("Setup Nodes", func() { - Describe("BeforeEachNodes", func() { - It("should report the correct type and code location", func() { - codeLocation := codelocation.New(0) - beforeEach := NewBeforeEachNode(func() {}, codeLocation, 0, nil, 3) - Ω(beforeEach.Type()).Should(Equal(types.SpecComponentTypeBeforeEach)) - Ω(beforeEach.CodeLocation()).Should(Equal(codeLocation)) - }) - }) - - Describe("AfterEachNodes", func() { - It("should report the correct type and code location", func() { - codeLocation := codelocation.New(0) - afterEach := NewAfterEachNode(func() {}, codeLocation, 0, nil, 3) - Ω(afterEach.Type()).Should(Equal(types.SpecComponentTypeAfterEach)) - Ω(afterEach.CodeLocation()).Should(Equal(codeLocation)) - }) - }) - - Describe("JustBeforeEachNodes", func() { - It("should report the correct type and code location", func() { - codeLocation := codelocation.New(0) - justBeforeEach := NewJustBeforeEachNode(func() {}, codeLocation, 0, nil, 3) - Ω(justBeforeEach.Type()).Should(Equal(types.SpecComponentTypeJustBeforeEach)) - Ω(justBeforeEach.CodeLocation()).Should(Equal(codeLocation)) - }) - }) - Describe("JustAfterEachNodes", func() { - It("should report the correct type and code location", func() { - codeLocation := codelocation.New(0) - justAfterEach := NewJustAfterEachNode(func() {}, codeLocation, 0, nil, 3) - Ω(justAfterEach.Type()).Should(Equal(types.SpecComponentTypeJustAfterEach)) - Ω(justAfterEach.CodeLocation()).Should(Equal(codeLocation)) - }) - }) -}) diff --git a/internal/leafnodes/shared_runner_test.go b/internal/leafnodes/shared_runner_test.go deleted file mode 100644 index 09c69a6bd1..0000000000 --- a/internal/leafnodes/shared_runner_test.go +++ /dev/null @@ -1,353 +0,0 @@ -package leafnodes_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/leafnodes" - . "github.com/onsi/gomega" - - "reflect" - "time" - - "github.com/onsi/ginkgo/internal/codelocation" - Failer "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -type runnable interface { - Run() (outcome types.SpecState, failure types.SpecFailure) - CodeLocation() types.CodeLocation -} - -func SynchronousSharedRunnerBehaviors(build func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable, componentType types.SpecComponentType, componentIndex int) { - var ( - outcome types.SpecState - failure types.SpecFailure - - failer *Failer.Failer - - componentCodeLocation types.CodeLocation - innerCodeLocation types.CodeLocation - - didRun bool - ) - - BeforeEach(func() { - failer = Failer.New() - componentCodeLocation = codelocation.New(0) - innerCodeLocation = codelocation.New(0) - - didRun = false - }) - - Describe("synchronous functions", func() { - Context("when the function passes", func() { - BeforeEach(func() { - outcome, failure = build(func() { - didRun = true - }, 0, failer, componentCodeLocation).Run() - }) - - It("should have a successful outcome", func() { - Ω(didRun).Should(BeTrue()) - - Ω(outcome).Should(Equal(types.SpecStatePassed)) - Ω(failure).Should(BeZero()) - }) - }) - - Context("when a failure occurs", func() { - BeforeEach(func() { - outcome, failure = build(func() { - didRun = true - failer.Fail("bam", innerCodeLocation) - panic("should not matter") - }, 0, failer, componentCodeLocation).Run() - }) - - It("should return the failure", func() { - Ω(didRun).Should(BeTrue()) - - Ω(outcome).Should(Equal(types.SpecStateFailed)) - Ω(failure).Should(Equal(types.SpecFailure{ - Message: "bam", - Location: innerCodeLocation, - ForwardedPanic: "", - ComponentIndex: componentIndex, - ComponentType: componentType, - ComponentCodeLocation: componentCodeLocation, - })) - }) - }) - - Context("when a panic occurs", func() { - BeforeEach(func() { - outcome, failure = build(func() { - didRun = true - innerCodeLocation = codelocation.New(0) - panic("ack!") - }, 0, failer, componentCodeLocation).Run() - }) - - It("should return the panic", func() { - Ω(didRun).Should(BeTrue()) - - Ω(outcome).Should(Equal(types.SpecStatePanicked)) - Ω(failure.ForwardedPanic).Should(Equal("ack!")) - }) - }) - - Context("when a panic occurs with a nil value", func() { - BeforeEach(func() { - outcome, failure = build(func() { - didRun = true - innerCodeLocation = codelocation.New(0) - panic(nil) - }, 0, failer, componentCodeLocation).Run() - }) - - It("should return the nil-valued panic", func() { - Ω(didRun).Should(BeTrue()) - - Ω(outcome).Should(Equal(types.SpecStatePanicked)) - Ω(failure.ForwardedPanic).Should(Equal("")) - }) - }) - - }) -} - -func AsynchronousSharedRunnerBehaviors(build func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable, componentType types.SpecComponentType, componentIndex int) { - var ( - outcome types.SpecState - failure types.SpecFailure - - failer *Failer.Failer - - componentCodeLocation types.CodeLocation - innerCodeLocation types.CodeLocation - - didRun bool - ) - - BeforeEach(func() { - failer = Failer.New() - componentCodeLocation = codelocation.New(0) - innerCodeLocation = codelocation.New(0) - - didRun = false - }) - - Describe("asynchronous functions", func() { - var timeoutDuration time.Duration - - BeforeEach(func() { - timeoutDuration = time.Duration(1 * float64(time.Second)) - }) - - Context("when running", func() { - It("should run the function as a goroutine, and block until it's done", func() { - proveAsync := make(chan bool) - - build(func(done Done) { - didRun = true - proveAsync <- true - close(done) - }, timeoutDuration, failer, componentCodeLocation).Run() - - Eventually(proveAsync).Should(Receive(Equal(true))) - }) - }) - - Context("when the function passes", func() { - BeforeEach(func() { - outcome, failure = build(func(done Done) { - didRun = true - close(done) - }, timeoutDuration, failer, componentCodeLocation).Run() - }) - - It("should have a successful outcome", func() { - Ω(didRun).Should(BeTrue()) - Ω(outcome).Should(Equal(types.SpecStatePassed)) - Ω(failure).Should(BeZero()) - }) - }) - - Context("when the function fails", func() { - BeforeEach(func() { - outcome, failure = build(func(done Done) { - didRun = true - failer.Fail("bam", innerCodeLocation) - time.Sleep(20 * time.Millisecond) - defer close(done) - panic("doesn't matter") - }, 10*time.Millisecond, failer, componentCodeLocation).Run() - }) - - It("should return the failure", func() { - Ω(didRun).Should(BeTrue()) - - Ω(outcome).Should(Equal(types.SpecStateFailed)) - Ω(failure).Should(Equal(types.SpecFailure{ - Message: "bam", - Location: innerCodeLocation, - ForwardedPanic: "", - ComponentIndex: componentIndex, - ComponentType: componentType, - ComponentCodeLocation: componentCodeLocation, - })) - }) - }) - - Context("when the function doesn't close the done channel in time", func() { - var guard chan struct{} - - BeforeEach(func() { - guard = make(chan struct{}) - outcome, failure = build(func(done Done) { - didRun = true - close(guard) - }, 10*time.Millisecond, failer, componentCodeLocation).Run() - }) - - It("should return a timeout", func() { - <-guard - Ω(didRun).Should(BeTrue()) - - Ω(outcome).Should(Equal(types.SpecStateTimedOut)) - Ω(failure).Should(Equal(types.SpecFailure{ - Message: "Timed out", - Location: componentCodeLocation, - ForwardedPanic: "", - ComponentIndex: componentIndex, - ComponentType: componentType, - ComponentCodeLocation: componentCodeLocation, - })) - }) - }) - - Context("when the function panics", func() { - BeforeEach(func() { - outcome, failure = build(func(done Done) { - didRun = true - innerCodeLocation = codelocation.New(0) - panic("ack!") - }, 100*time.Millisecond, failer, componentCodeLocation).Run() - }) - - It("should return the panic", func() { - Ω(didRun).Should(BeTrue()) - - Ω(outcome).Should(Equal(types.SpecStatePanicked)) - Ω(failure.ForwardedPanic).Should(Equal("ack!")) - }) - }) - - Context("when the function panics with a nil value", func() { - BeforeEach(func() { - outcome, failure = build(func(done Done) { - didRun = true - innerCodeLocation = codelocation.New(0) - panic(nil) - }, 100*time.Millisecond, failer, componentCodeLocation).Run() - }) - - It("should return the nil-valued panic", func() { - Ω(didRun).Should(BeTrue()) - - Ω(outcome).Should(Equal(types.SpecStatePanicked)) - Ω(failure.ForwardedPanic).Should(Equal("")) - }) - }) - }) -} - -func InvalidSharedRunnerBehaviors(build func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable, componentType types.SpecComponentType) { - var ( - failer *Failer.Failer - componentCodeLocation types.CodeLocation - ) - - BeforeEach(func() { - failer = Failer.New() - componentCodeLocation = codelocation.New(0) - }) - - Describe("invalid functions", func() { - Context("when passed something that's not a function", func() { - It("should panic", func() { - Ω(func() { - build("not a function", 0, failer, componentCodeLocation) - }).Should(Panic()) - }) - }) - - Context("when the function takes the wrong kind of argument", func() { - It("should panic", func() { - Ω(func() { - build(func(oops string) {}, 0, failer, componentCodeLocation) - }).Should(Panic()) - }) - }) - - Context("when the function takes more than one argument", func() { - It("should panic", func() { - Ω(func() { - build(func(done Done, oops string) {}, 0, failer, componentCodeLocation) - }).Should(Panic()) - }) - }) - }) -} - -var _ = Describe("Shared RunnableNode behavior", func() { - Describe("It Nodes", func() { - build := func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { - return NewItNode("", body, types.FlagTypeFocused, componentCodeLocation, timeout, failer, 3) - } - - SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeIt, 3) - AsynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeIt, 3) - InvalidSharedRunnerBehaviors(build, types.SpecComponentTypeIt) - }) - - Describe("Measure Nodes", func() { - build := func(body interface{}, _ time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { - return NewMeasureNode("", func(Benchmarker) { - reflect.ValueOf(body).Call([]reflect.Value{}) - }, types.FlagTypeFocused, componentCodeLocation, 10, failer, 3) - } - - SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeMeasure, 3) - }) - - Describe("BeforeEach Nodes", func() { - build := func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { - return NewBeforeEachNode(body, componentCodeLocation, timeout, failer, 3) - } - - SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeBeforeEach, 3) - AsynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeBeforeEach, 3) - InvalidSharedRunnerBehaviors(build, types.SpecComponentTypeBeforeEach) - }) - - Describe("AfterEach Nodes", func() { - build := func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { - return NewAfterEachNode(body, componentCodeLocation, timeout, failer, 3) - } - - SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeAfterEach, 3) - AsynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeAfterEach, 3) - InvalidSharedRunnerBehaviors(build, types.SpecComponentTypeAfterEach) - }) - - Describe("JustBeforeEach Nodes", func() { - build := func(body interface{}, timeout time.Duration, failer *Failer.Failer, componentCodeLocation types.CodeLocation) runnable { - return NewJustBeforeEachNode(body, componentCodeLocation, timeout, failer, 3) - } - - SynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeJustBeforeEach, 3) - AsynchronousSharedRunnerBehaviors(build, types.SpecComponentTypeJustBeforeEach, 3) - InvalidSharedRunnerBehaviors(build, types.SpecComponentTypeJustBeforeEach) - }) -}) diff --git a/internal/leafnodes/suite_nodes.go b/internal/leafnodes/suite_nodes.go deleted file mode 100644 index 80f16ed786..0000000000 --- a/internal/leafnodes/suite_nodes.go +++ /dev/null @@ -1,55 +0,0 @@ -package leafnodes - -import ( - "time" - - "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -type SuiteNode interface { - Run(parallelNode int, parallelTotal int, syncHost string) bool - Passed() bool - Summary() *types.SetupSummary -} - -type simpleSuiteNode struct { - runner *runner - outcome types.SpecState - failure types.SpecFailure - runTime time.Duration -} - -func (node *simpleSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool { - t := time.Now() - node.outcome, node.failure = node.runner.run() - node.runTime = time.Since(t) - - return node.outcome == types.SpecStatePassed -} - -func (node *simpleSuiteNode) Passed() bool { - return node.outcome == types.SpecStatePassed -} - -func (node *simpleSuiteNode) Summary() *types.SetupSummary { - return &types.SetupSummary{ - ComponentType: node.runner.nodeType, - CodeLocation: node.runner.codeLocation, - State: node.outcome, - RunTime: node.runTime, - Failure: node.failure, - } -} - -func NewBeforeSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { - return &simpleSuiteNode{ - runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0), - } -} - -func NewAfterSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { - return &simpleSuiteNode{ - runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0), - } -} diff --git a/internal/leafnodes/suite_nodes_test.go b/internal/leafnodes/suite_nodes_test.go deleted file mode 100644 index 246b329fe2..0000000000 --- a/internal/leafnodes/suite_nodes_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package leafnodes_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - . "github.com/onsi/ginkgo/internal/leafnodes" - - "time" - - "github.com/onsi/ginkgo/internal/codelocation" - Failer "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -var _ = Describe("SuiteNodes", func() { - Describe("BeforeSuite nodes", func() { - var befSuite SuiteNode - var failer *Failer.Failer - var codeLocation types.CodeLocation - var innerCodeLocation types.CodeLocation - var outcome bool - - BeforeEach(func() { - failer = Failer.New() - codeLocation = codelocation.New(0) - innerCodeLocation = codelocation.New(0) - }) - - Context("when the body passes", func() { - BeforeEach(func() { - befSuite = NewBeforeSuiteNode(func() { - time.Sleep(10 * time.Millisecond) - }, codeLocation, 0, failer) - outcome = befSuite.Run(0, 0, "") - }) - - It("should return true when run and report as passed", func() { - Ω(outcome).Should(BeTrue()) - Ω(befSuite.Passed()).Should(BeTrue()) - }) - - It("should have the correct summary", func() { - summary := befSuite.Summary() - Ω(summary.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) - Ω(summary.CodeLocation).Should(Equal(codeLocation)) - Ω(summary.State).Should(Equal(types.SpecStatePassed)) - Ω(summary.RunTime).Should(BeNumerically(">=", 10*time.Millisecond)) - Ω(summary.Failure).Should(BeZero()) - }) - }) - - Context("when the body fails", func() { - BeforeEach(func() { - befSuite = NewBeforeSuiteNode(func() { - failer.Fail("oops", innerCodeLocation) - }, codeLocation, 0, failer) - outcome = befSuite.Run(0, 0, "") - }) - - It("should return false when run and report as failed", func() { - Ω(outcome).Should(BeFalse()) - Ω(befSuite.Passed()).Should(BeFalse()) - }) - - It("should have the correct summary", func() { - summary := befSuite.Summary() - Ω(summary.State).Should(Equal(types.SpecStateFailed)) - Ω(summary.Failure.Message).Should(Equal("oops")) - Ω(summary.Failure.Location).Should(Equal(innerCodeLocation)) - Ω(summary.Failure.ForwardedPanic).Should(BeEmpty()) - Ω(summary.Failure.ComponentIndex).Should(Equal(0)) - Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) - Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) - }) - }) - - Context("when the body times out", func() { - BeforeEach(func() { - befSuite = NewBeforeSuiteNode(func(done Done) { - }, codeLocation, time.Millisecond, failer) - outcome = befSuite.Run(0, 0, "") - }) - - It("should return false when run and report as failed", func() { - Ω(outcome).Should(BeFalse()) - Ω(befSuite.Passed()).Should(BeFalse()) - }) - - It("should have the correct summary", func() { - summary := befSuite.Summary() - Ω(summary.State).Should(Equal(types.SpecStateTimedOut)) - Ω(summary.Failure.ForwardedPanic).Should(BeEmpty()) - Ω(summary.Failure.ComponentIndex).Should(Equal(0)) - Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) - Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) - }) - }) - - Context("when the body panics", func() { - BeforeEach(func() { - befSuite = NewBeforeSuiteNode(func() { - panic("bam") - }, codeLocation, 0, failer) - outcome = befSuite.Run(0, 0, "") - }) - - It("should return false when run and report as failed", func() { - Ω(outcome).Should(BeFalse()) - Ω(befSuite.Passed()).Should(BeFalse()) - }) - - It("should have the correct summary", func() { - summary := befSuite.Summary() - Ω(summary.State).Should(Equal(types.SpecStatePanicked)) - Ω(summary.Failure.ForwardedPanic).Should(Equal("bam")) - Ω(summary.Failure.ComponentIndex).Should(Equal(0)) - Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) - Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) - }) - }) - }) - - Describe("AfterSuite nodes", func() { - var aftSuite SuiteNode - var failer *Failer.Failer - var codeLocation types.CodeLocation - var innerCodeLocation types.CodeLocation - var outcome bool - - BeforeEach(func() { - failer = Failer.New() - codeLocation = codelocation.New(0) - innerCodeLocation = codelocation.New(0) - }) - - Context("when the body passes", func() { - BeforeEach(func() { - aftSuite = NewAfterSuiteNode(func() { - time.Sleep(10 * time.Millisecond) - }, codeLocation, 0, failer) - outcome = aftSuite.Run(0, 0, "") - }) - - It("should return true when run and report as passed", func() { - Ω(outcome).Should(BeTrue()) - Ω(aftSuite.Passed()).Should(BeTrue()) - }) - - It("should have the correct summary", func() { - summary := aftSuite.Summary() - Ω(summary.ComponentType).Should(Equal(types.SpecComponentTypeAfterSuite)) - Ω(summary.CodeLocation).Should(Equal(codeLocation)) - Ω(summary.State).Should(Equal(types.SpecStatePassed)) - Ω(summary.RunTime).Should(BeNumerically(">=", 10*time.Millisecond)) - Ω(summary.Failure).Should(BeZero()) - }) - }) - - Context("when the body fails", func() { - BeforeEach(func() { - aftSuite = NewAfterSuiteNode(func() { - failer.Fail("oops", innerCodeLocation) - }, codeLocation, 0, failer) - outcome = aftSuite.Run(0, 0, "") - }) - - It("should return false when run and report as failed", func() { - Ω(outcome).Should(BeFalse()) - Ω(aftSuite.Passed()).Should(BeFalse()) - }) - - It("should have the correct summary", func() { - summary := aftSuite.Summary() - Ω(summary.State).Should(Equal(types.SpecStateFailed)) - Ω(summary.Failure.Message).Should(Equal("oops")) - Ω(summary.Failure.Location).Should(Equal(innerCodeLocation)) - Ω(summary.Failure.ForwardedPanic).Should(BeEmpty()) - Ω(summary.Failure.ComponentIndex).Should(Equal(0)) - Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeAfterSuite)) - Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) - }) - }) - - Context("when the body times out", func() { - BeforeEach(func() { - aftSuite = NewAfterSuiteNode(func(done Done) { - }, codeLocation, time.Millisecond, failer) - outcome = aftSuite.Run(0, 0, "") - }) - - It("should return false when run and report as failed", func() { - Ω(outcome).Should(BeFalse()) - Ω(aftSuite.Passed()).Should(BeFalse()) - }) - - It("should have the correct summary", func() { - summary := aftSuite.Summary() - Ω(summary.State).Should(Equal(types.SpecStateTimedOut)) - Ω(summary.Failure.ForwardedPanic).Should(BeEmpty()) - Ω(summary.Failure.ComponentIndex).Should(Equal(0)) - Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeAfterSuite)) - Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) - }) - }) - - Context("when the body panics", func() { - BeforeEach(func() { - aftSuite = NewAfterSuiteNode(func() { - panic("bam") - }, codeLocation, 0, failer) - outcome = aftSuite.Run(0, 0, "") - }) - - It("should return false when run and report as failed", func() { - Ω(outcome).Should(BeFalse()) - Ω(aftSuite.Passed()).Should(BeFalse()) - }) - - It("should have the correct summary", func() { - summary := aftSuite.Summary() - Ω(summary.State).Should(Equal(types.SpecStatePanicked)) - Ω(summary.Failure.ForwardedPanic).Should(Equal("bam")) - Ω(summary.Failure.ComponentIndex).Should(Equal(0)) - Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeAfterSuite)) - Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) - }) - }) - }) -}) diff --git a/internal/leafnodes/synchronized_after_suite_node.go b/internal/leafnodes/synchronized_after_suite_node.go deleted file mode 100644 index a721d0cf7f..0000000000 --- a/internal/leafnodes/synchronized_after_suite_node.go +++ /dev/null @@ -1,90 +0,0 @@ -package leafnodes - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "time" - - "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -type synchronizedAfterSuiteNode struct { - runnerA *runner - runnerB *runner - - outcome types.SpecState - failure types.SpecFailure - runTime time.Duration -} - -func NewSynchronizedAfterSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { - return &synchronizedAfterSuiteNode{ - runnerA: newRunner(bodyA, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0), - runnerB: newRunner(bodyB, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0), - } -} - -func (node *synchronizedAfterSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool { - node.outcome, node.failure = node.runnerA.run() - - if parallelNode == 1 { - if parallelTotal > 1 { - node.waitUntilOtherNodesAreDone(syncHost) - } - - outcome, failure := node.runnerB.run() - - if node.outcome == types.SpecStatePassed { - node.outcome, node.failure = outcome, failure - } - } - - return node.outcome == types.SpecStatePassed -} - -func (node *synchronizedAfterSuiteNode) Passed() bool { - return node.outcome == types.SpecStatePassed -} - -func (node *synchronizedAfterSuiteNode) Summary() *types.SetupSummary { - return &types.SetupSummary{ - ComponentType: node.runnerA.nodeType, - CodeLocation: node.runnerA.codeLocation, - State: node.outcome, - RunTime: node.runTime, - Failure: node.failure, - } -} - -func (node *synchronizedAfterSuiteNode) waitUntilOtherNodesAreDone(syncHost string) { - for { - if node.canRun(syncHost) { - return - } - - time.Sleep(50 * time.Millisecond) - } -} - -func (node *synchronizedAfterSuiteNode) canRun(syncHost string) bool { - resp, err := http.Get(syncHost + "/RemoteAfterSuiteData") - if err != nil || resp.StatusCode != http.StatusOK { - return false - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return false - } - resp.Body.Close() - - afterSuiteData := types.RemoteAfterSuiteData{} - err = json.Unmarshal(body, &afterSuiteData) - if err != nil { - return false - } - - return afterSuiteData.CanRun -} diff --git a/internal/leafnodes/synchronized_after_suite_node_test.go b/internal/leafnodes/synchronized_after_suite_node_test.go deleted file mode 100644 index edbdf6ae59..0000000000 --- a/internal/leafnodes/synchronized_after_suite_node_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package leafnodes_test - -import ( - "sync" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/types" - . "github.com/onsi/gomega" - - "net/http" - - "github.com/onsi/gomega/ghttp" - - "time" - - "github.com/onsi/ginkgo/internal/codelocation" - Failer "github.com/onsi/ginkgo/internal/failer" -) - -var _ = Describe("SynchronizedAfterSuiteNode", func() { - var failer *Failer.Failer - var node SuiteNode - var codeLocation types.CodeLocation - var innerCodeLocation types.CodeLocation - var outcome bool - var server *ghttp.Server - var things []string - var lock *sync.Mutex - - BeforeEach(func() { - things = []string{} - server = ghttp.NewServer() - codeLocation = codelocation.New(0) - innerCodeLocation = codelocation.New(0) - failer = Failer.New() - lock = &sync.Mutex{} - }) - - AfterEach(func() { - server.Close() - }) - - newNode := func(bodyA interface{}, bodyB interface{}) SuiteNode { - return NewSynchronizedAfterSuiteNode(bodyA, bodyB, codeLocation, time.Millisecond, failer) - } - - ranThing := func(thing string) { - lock.Lock() - defer lock.Unlock() - things = append(things, thing) - } - - thingsThatRan := func() []string { - lock.Lock() - defer lock.Unlock() - return things - } - - Context("when not running in parallel", func() { - Context("when all is well", func() { - BeforeEach(func() { - node = newNode(func() { - ranThing("A") - }, func() { - ranThing("B") - }) - - outcome = node.Run(1, 1, server.URL()) - }) - - It("should run A, then B", func() { - Ω(thingsThatRan()).Should(Equal([]string{"A", "B"})) - }) - - It("should report success", func() { - Ω(outcome).Should(BeTrue()) - Ω(node.Passed()).Should(BeTrue()) - Ω(node.Summary().State).Should(Equal(types.SpecStatePassed)) - }) - }) - - Context("when A fails", func() { - BeforeEach(func() { - node = newNode(func() { - ranThing("A") - failer.Fail("bam", innerCodeLocation) - }, func() { - ranThing("B") - }) - - outcome = node.Run(1, 1, server.URL()) - }) - - It("should still run B", func() { - Ω(thingsThatRan()).Should(Equal([]string{"A", "B"})) - }) - - It("should report failure", func() { - Ω(outcome).Should(BeFalse()) - Ω(node.Passed()).Should(BeFalse()) - Ω(node.Summary().State).Should(Equal(types.SpecStateFailed)) - }) - }) - - Context("when B fails", func() { - BeforeEach(func() { - node = newNode(func() { - ranThing("A") - }, func() { - ranThing("B") - failer.Fail("bam", innerCodeLocation) - }) - - outcome = node.Run(1, 1, server.URL()) - }) - - It("should run all the things", func() { - Ω(thingsThatRan()).Should(Equal([]string{"A", "B"})) - }) - - It("should report failure", func() { - Ω(outcome).Should(BeFalse()) - Ω(node.Passed()).Should(BeFalse()) - Ω(node.Summary().State).Should(Equal(types.SpecStateFailed)) - }) - }) - }) - - Context("when running in parallel", func() { - Context("as the first node", func() { - BeforeEach(func() { - server.AppendHandlers(ghttp.CombineHandlers( - ghttp.VerifyRequest("GET", "/RemoteAfterSuiteData"), - func(writer http.ResponseWriter, request *http.Request) { - ranThing("Request1") - }, - ghttp.RespondWithJSONEncoded(200, types.RemoteAfterSuiteData{CanRun: false}), - ), ghttp.CombineHandlers( - ghttp.VerifyRequest("GET", "/RemoteAfterSuiteData"), - func(writer http.ResponseWriter, request *http.Request) { - ranThing("Request2") - }, - ghttp.RespondWithJSONEncoded(200, types.RemoteAfterSuiteData{CanRun: false}), - ), ghttp.CombineHandlers( - ghttp.VerifyRequest("GET", "/RemoteAfterSuiteData"), - func(writer http.ResponseWriter, request *http.Request) { - ranThing("Request3") - }, - ghttp.RespondWithJSONEncoded(200, types.RemoteAfterSuiteData{CanRun: true}), - )) - - node = newNode(func() { - ranThing("A") - }, func() { - ranThing("B") - }) - - outcome = node.Run(1, 3, server.URL()) - }) - - It("should run A and, when the server says its time, run B", func() { - Ω(thingsThatRan()).Should(Equal([]string{"A", "Request1", "Request2", "Request3", "B"})) - }) - - It("should report success", func() { - Ω(outcome).Should(BeTrue()) - Ω(node.Passed()).Should(BeTrue()) - Ω(node.Summary().State).Should(Equal(types.SpecStatePassed)) - }) - }) - - Context("as any other node", func() { - BeforeEach(func() { - node = newNode(func() { - ranThing("A") - }, func() { - ranThing("B") - }) - - outcome = node.Run(2, 3, server.URL()) - }) - - It("should run A, and not run B", func() { - Ω(thingsThatRan()).Should(Equal([]string{"A"})) - }) - - It("should not talk to the server", func() { - Ω(server.ReceivedRequests()).Should(BeEmpty()) - }) - - It("should report success", func() { - Ω(outcome).Should(BeTrue()) - Ω(node.Passed()).Should(BeTrue()) - Ω(node.Summary().State).Should(Equal(types.SpecStatePassed)) - }) - }) - }) -}) diff --git a/internal/leafnodes/synchronized_before_suite_node.go b/internal/leafnodes/synchronized_before_suite_node.go deleted file mode 100644 index d5c8893194..0000000000 --- a/internal/leafnodes/synchronized_before_suite_node.go +++ /dev/null @@ -1,181 +0,0 @@ -package leafnodes - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "net/http" - "reflect" - "time" - - "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -type synchronizedBeforeSuiteNode struct { - runnerA *runner - runnerB *runner - - data []byte - - outcome types.SpecState - failure types.SpecFailure - runTime time.Duration -} - -func NewSynchronizedBeforeSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode { - node := &synchronizedBeforeSuiteNode{} - - node.runnerA = newRunner(node.wrapA(bodyA), codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0) - node.runnerB = newRunner(node.wrapB(bodyB), codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0) - - return node -} - -func (node *synchronizedBeforeSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool { - t := time.Now() - defer func() { - node.runTime = time.Since(t) - }() - - if parallelNode == 1 { - node.outcome, node.failure = node.runA(parallelTotal, syncHost) - } else { - node.outcome, node.failure = node.waitForA(syncHost) - } - - if node.outcome != types.SpecStatePassed { - return false - } - node.outcome, node.failure = node.runnerB.run() - - return node.outcome == types.SpecStatePassed -} - -func (node *synchronizedBeforeSuiteNode) runA(parallelTotal int, syncHost string) (types.SpecState, types.SpecFailure) { - outcome, failure := node.runnerA.run() - - if parallelTotal > 1 { - state := types.RemoteBeforeSuiteStatePassed - if outcome != types.SpecStatePassed { - state = types.RemoteBeforeSuiteStateFailed - } - json := (types.RemoteBeforeSuiteData{ - Data: node.data, - State: state, - }).ToJSON() - http.Post(syncHost+"/BeforeSuiteState", "application/json", bytes.NewBuffer(json)) - } - - return outcome, failure -} - -func (node *synchronizedBeforeSuiteNode) waitForA(syncHost string) (types.SpecState, types.SpecFailure) { - failure := func(message string) types.SpecFailure { - return types.SpecFailure{ - Message: message, - Location: node.runnerA.codeLocation, - ComponentType: node.runnerA.nodeType, - ComponentIndex: node.runnerA.componentIndex, - ComponentCodeLocation: node.runnerA.codeLocation, - } - } - for { - resp, err := http.Get(syncHost + "/BeforeSuiteState") - if err != nil || resp.StatusCode != http.StatusOK { - return types.SpecStateFailed, failure("Failed to fetch BeforeSuite state") - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return types.SpecStateFailed, failure("Failed to read BeforeSuite state") - } - resp.Body.Close() - - beforeSuiteData := types.RemoteBeforeSuiteData{} - err = json.Unmarshal(body, &beforeSuiteData) - if err != nil { - return types.SpecStateFailed, failure("Failed to decode BeforeSuite state") - } - - switch beforeSuiteData.State { - case types.RemoteBeforeSuiteStatePassed: - node.data = beforeSuiteData.Data - return types.SpecStatePassed, types.SpecFailure{} - case types.RemoteBeforeSuiteStateFailed: - return types.SpecStateFailed, failure("BeforeSuite on Node 1 failed") - case types.RemoteBeforeSuiteStateDisappeared: - return types.SpecStateFailed, failure("Node 1 disappeared before completing BeforeSuite") - } - - time.Sleep(50 * time.Millisecond) - } -} - -func (node *synchronizedBeforeSuiteNode) Passed() bool { - return node.outcome == types.SpecStatePassed -} - -func (node *synchronizedBeforeSuiteNode) Summary() *types.SetupSummary { - return &types.SetupSummary{ - ComponentType: node.runnerA.nodeType, - CodeLocation: node.runnerA.codeLocation, - State: node.outcome, - RunTime: node.runTime, - Failure: node.failure, - } -} - -func (node *synchronizedBeforeSuiteNode) wrapA(bodyA interface{}) interface{} { - typeA := reflect.TypeOf(bodyA) - if typeA.Kind() != reflect.Func { - panic("SynchronizedBeforeSuite expects a function as its first argument") - } - - takesNothing := typeA.NumIn() == 0 - takesADoneChannel := typeA.NumIn() == 1 && typeA.In(0).Kind() == reflect.Chan && typeA.In(0).Elem().Kind() == reflect.Interface - returnsBytes := typeA.NumOut() == 1 && typeA.Out(0).Kind() == reflect.Slice && typeA.Out(0).Elem().Kind() == reflect.Uint8 - - if !((takesNothing || takesADoneChannel) && returnsBytes) { - panic("SynchronizedBeforeSuite's first argument should be a function that returns []byte and either takes no arguments or takes a Done channel.") - } - - if takesADoneChannel { - return func(done chan<- interface{}) { - out := reflect.ValueOf(bodyA).Call([]reflect.Value{reflect.ValueOf(done)}) - node.data = out[0].Interface().([]byte) - } - } - - return func() { - out := reflect.ValueOf(bodyA).Call([]reflect.Value{}) - node.data = out[0].Interface().([]byte) - } -} - -func (node *synchronizedBeforeSuiteNode) wrapB(bodyB interface{}) interface{} { - typeB := reflect.TypeOf(bodyB) - if typeB.Kind() != reflect.Func { - panic("SynchronizedBeforeSuite expects a function as its second argument") - } - - returnsNothing := typeB.NumOut() == 0 - takesBytesOnly := typeB.NumIn() == 1 && typeB.In(0).Kind() == reflect.Slice && typeB.In(0).Elem().Kind() == reflect.Uint8 - takesBytesAndDone := typeB.NumIn() == 2 && - typeB.In(0).Kind() == reflect.Slice && typeB.In(0).Elem().Kind() == reflect.Uint8 && - typeB.In(1).Kind() == reflect.Chan && typeB.In(1).Elem().Kind() == reflect.Interface - - if !((takesBytesOnly || takesBytesAndDone) && returnsNothing) { - panic("SynchronizedBeforeSuite's second argument should be a function that returns nothing and either takes []byte or ([]byte, Done)") - } - - if takesBytesAndDone { - return func(done chan<- interface{}) { - reflect.ValueOf(bodyB).Call([]reflect.Value{reflect.ValueOf(node.data), reflect.ValueOf(done)}) - } - } - - return func() { - reflect.ValueOf(bodyB).Call([]reflect.Value{reflect.ValueOf(node.data)}) - } -} diff --git a/internal/leafnodes/synchronized_before_suite_node_test.go b/internal/leafnodes/synchronized_before_suite_node_test.go deleted file mode 100644 index 46c3e276b6..0000000000 --- a/internal/leafnodes/synchronized_before_suite_node_test.go +++ /dev/null @@ -1,446 +0,0 @@ -package leafnodes_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/leafnodes" - . "github.com/onsi/gomega" - - "net/http" - - "github.com/onsi/gomega/ghttp" - - "time" - - "github.com/onsi/ginkgo/internal/codelocation" - Failer "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/types" -) - -var _ = Describe("SynchronizedBeforeSuiteNode", func() { - var failer *Failer.Failer - var node SuiteNode - var codeLocation types.CodeLocation - var innerCodeLocation types.CodeLocation - var outcome bool - var server *ghttp.Server - - BeforeEach(func() { - server = ghttp.NewServer() - codeLocation = codelocation.New(0) - innerCodeLocation = codelocation.New(0) - failer = Failer.New() - }) - - AfterEach(func() { - server.Close() - }) - - newNode := func(bodyA interface{}, bodyB interface{}) SuiteNode { - return NewSynchronizedBeforeSuiteNode(bodyA, bodyB, codeLocation, time.Millisecond, failer) - } - - Describe("when not running in parallel", func() { - Context("when all is well", func() { - var data []byte - BeforeEach(func() { - data = nil - - node = newNode(func() []byte { - return []byte("my data") - }, func(d []byte) { - data = d - }) - - outcome = node.Run(1, 1, server.URL()) - }) - - It("should run A, then B passing the output from A to B", func() { - Ω(data).Should(Equal([]byte("my data"))) - }) - - It("should report success", func() { - Ω(outcome).Should(BeTrue()) - Ω(node.Passed()).Should(BeTrue()) - Ω(node.Summary().State).Should(Equal(types.SpecStatePassed)) - }) - }) - - Context("when A fails", func() { - var ranB bool - BeforeEach(func() { - ranB = false - node = newNode(func() []byte { - failer.Fail("boom", innerCodeLocation) - return nil - }, func([]byte) { - ranB = true - }) - - outcome = node.Run(1, 1, server.URL()) - }) - - It("should not run B", func() { - Ω(ranB).Should(BeFalse()) - }) - - It("should report failure", func() { - Ω(outcome).Should(BeFalse()) - Ω(node.Passed()).Should(BeFalse()) - Ω(node.Summary().State).Should(Equal(types.SpecStateFailed)) - }) - }) - - Context("when B fails", func() { - BeforeEach(func() { - node = newNode(func() []byte { - return nil - }, func([]byte) { - failer.Fail("boom", innerCodeLocation) - }) - - outcome = node.Run(1, 1, server.URL()) - }) - - It("should report failure", func() { - Ω(outcome).Should(BeFalse()) - Ω(node.Passed()).Should(BeFalse()) - Ω(node.Summary().State).Should(Equal(types.SpecStateFailed)) - }) - }) - - Context("when A times out", func() { - var ranB bool - BeforeEach(func() { - ranB = false - node = newNode(func(Done) []byte { - time.Sleep(time.Second) - return nil - }, func([]byte) { - ranB = true - }) - - outcome = node.Run(1, 1, server.URL()) - }) - - It("should not run B", func() { - Ω(ranB).Should(BeFalse()) - }) - - It("should report failure", func() { - Ω(outcome).Should(BeFalse()) - Ω(node.Passed()).Should(BeFalse()) - Ω(node.Summary().State).Should(Equal(types.SpecStateTimedOut)) - }) - }) - - Context("when B times out", func() { - BeforeEach(func() { - node = newNode(func() []byte { - return nil - }, func([]byte, Done) { - time.Sleep(time.Second) - }) - - outcome = node.Run(1, 1, server.URL()) - }) - - It("should report failure", func() { - Ω(outcome).Should(BeFalse()) - Ω(node.Passed()).Should(BeFalse()) - Ω(node.Summary().State).Should(Equal(types.SpecStateTimedOut)) - }) - }) - }) - - Describe("when running in parallel", func() { - var ranB bool - var parallelNode, parallelTotal int - BeforeEach(func() { - ranB = false - parallelNode, parallelTotal = 1, 3 - }) - - Context("as the first node, it runs A", func() { - var expectedState types.RemoteBeforeSuiteData - - BeforeEach(func() { - parallelNode, parallelTotal = 1, 3 - }) - - JustBeforeEach(func() { - server.AppendHandlers(ghttp.CombineHandlers( - ghttp.VerifyRequest("POST", "/BeforeSuiteState"), - ghttp.VerifyJSONRepresenting(expectedState), - )) - - outcome = node.Run(parallelNode, parallelTotal, server.URL()) - }) - - Context("when A succeeds", func() { - BeforeEach(func() { - expectedState = types.RemoteBeforeSuiteData{Data: []byte("my data"), State: types.RemoteBeforeSuiteStatePassed} - - node = newNode(func() []byte { - return []byte("my data") - }, func([]byte) { - ranB = true - }) - }) - - It("should post about A succeeding", func() { - Ω(server.ReceivedRequests()).Should(HaveLen(1)) - }) - - It("should run B", func() { - Ω(ranB).Should(BeTrue()) - }) - - It("should report success", func() { - Ω(outcome).Should(BeTrue()) - }) - }) - - Context("when A fails", func() { - BeforeEach(func() { - expectedState = types.RemoteBeforeSuiteData{Data: nil, State: types.RemoteBeforeSuiteStateFailed} - - node = newNode(func() []byte { - panic("BAM") - }, func([]byte) { - ranB = true - }) - }) - - It("should post about A failing", func() { - Ω(server.ReceivedRequests()).Should(HaveLen(1)) - }) - - It("should not run B", func() { - Ω(ranB).Should(BeFalse()) - }) - - It("should report failure", func() { - Ω(outcome).Should(BeFalse()) - }) - }) - }) - - Context("as the Nth node", func() { - var statusCode int - var response interface{} - var ranA bool - var bData []byte - - BeforeEach(func() { - ranA = false - bData = nil - - statusCode = http.StatusOK - - server.AppendHandlers(ghttp.CombineHandlers( - ghttp.VerifyRequest("GET", "/BeforeSuiteState"), - ghttp.RespondWith(http.StatusOK, string((types.RemoteBeforeSuiteData{Data: nil, State: types.RemoteBeforeSuiteStatePending}).ToJSON())), - ), ghttp.CombineHandlers( - ghttp.VerifyRequest("GET", "/BeforeSuiteState"), - ghttp.RespondWith(http.StatusOK, string((types.RemoteBeforeSuiteData{Data: nil, State: types.RemoteBeforeSuiteStatePending}).ToJSON())), - ), ghttp.CombineHandlers( - ghttp.VerifyRequest("GET", "/BeforeSuiteState"), - ghttp.RespondWithJSONEncodedPtr(&statusCode, &response), - )) - - node = newNode(func() []byte { - ranA = true - return nil - }, func(data []byte) { - bData = data - }) - - parallelNode, parallelTotal = 2, 3 - }) - - Context("when A on node1 succeeds", func() { - BeforeEach(func() { - response = types.RemoteBeforeSuiteData{Data: []byte("my data"), State: types.RemoteBeforeSuiteStatePassed} - outcome = node.Run(parallelNode, parallelTotal, server.URL()) - }) - - It("should not run A", func() { - Ω(ranA).Should(BeFalse()) - }) - - It("should poll for A", func() { - Ω(server.ReceivedRequests()).Should(HaveLen(3)) - }) - - It("should run B when the polling succeeds", func() { - Ω(bData).Should(Equal([]byte("my data"))) - }) - - It("should succeed", func() { - Ω(outcome).Should(BeTrue()) - Ω(node.Passed()).Should(BeTrue()) - }) - }) - - Context("when A on node1 fails", func() { - BeforeEach(func() { - response = types.RemoteBeforeSuiteData{Data: []byte("my data"), State: types.RemoteBeforeSuiteStateFailed} - outcome = node.Run(parallelNode, parallelTotal, server.URL()) - }) - - It("should not run A", func() { - Ω(ranA).Should(BeFalse()) - }) - - It("should poll for A", func() { - Ω(server.ReceivedRequests()).Should(HaveLen(3)) - }) - - It("should not run B", func() { - Ω(bData).Should(BeNil()) - }) - - It("should fail", func() { - Ω(outcome).Should(BeFalse()) - Ω(node.Passed()).Should(BeFalse()) - - summary := node.Summary() - Ω(summary.State).Should(Equal(types.SpecStateFailed)) - Ω(summary.Failure.Message).Should(Equal("BeforeSuite on Node 1 failed")) - Ω(summary.Failure.Location).Should(Equal(codeLocation)) - Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) - Ω(summary.Failure.ComponentIndex).Should(Equal(0)) - Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) - }) - }) - - Context("when node1 disappears", func() { - BeforeEach(func() { - response = types.RemoteBeforeSuiteData{Data: []byte("my data"), State: types.RemoteBeforeSuiteStateDisappeared} - outcome = node.Run(parallelNode, parallelTotal, server.URL()) - }) - - It("should not run A", func() { - Ω(ranA).Should(BeFalse()) - }) - - It("should poll for A", func() { - Ω(server.ReceivedRequests()).Should(HaveLen(3)) - }) - - It("should not run B", func() { - Ω(bData).Should(BeNil()) - }) - - It("should fail", func() { - Ω(outcome).Should(BeFalse()) - Ω(node.Passed()).Should(BeFalse()) - - summary := node.Summary() - Ω(summary.State).Should(Equal(types.SpecStateFailed)) - Ω(summary.Failure.Message).Should(Equal("Node 1 disappeared before completing BeforeSuite")) - Ω(summary.Failure.Location).Should(Equal(codeLocation)) - Ω(summary.Failure.ComponentType).Should(Equal(types.SpecComponentTypeBeforeSuite)) - Ω(summary.Failure.ComponentIndex).Should(Equal(0)) - Ω(summary.Failure.ComponentCodeLocation).Should(Equal(codeLocation)) - }) - }) - }) - }) - - Describe("construction", func() { - Describe("the first function", func() { - Context("when the first function returns a byte array", func() { - Context("and takes nothing", func() { - It("should be fine", func() { - Ω(func() { - newNode(func() []byte { return nil }, func([]byte) {}) - }).ShouldNot(Panic()) - }) - }) - - Context("and takes a done function", func() { - It("should be fine", func() { - Ω(func() { - newNode(func(Done) []byte { return nil }, func([]byte) {}) - }).ShouldNot(Panic()) - }) - }) - - Context("and takes more than one thing", func() { - It("should panic", func() { - Ω(func() { - newNode(func(Done, Done) []byte { return nil }, func([]byte) {}) - }).Should(Panic()) - }) - }) - - Context("and takes something else", func() { - It("should panic", func() { - Ω(func() { - newNode(func(bool) []byte { return nil }, func([]byte) {}) - }).Should(Panic()) - }) - }) - }) - - Context("when the first function does not return a byte array", func() { - It("should panic", func() { - Ω(func() { - newNode(func() {}, func([]byte) {}) - }).Should(Panic()) - - Ω(func() { - newNode(func() []int { return nil }, func([]byte) {}) - }).Should(Panic()) - }) - }) - }) - - Describe("the second function", func() { - Context("when the second function takes a byte array", func() { - It("should be fine", func() { - Ω(func() { - newNode(func() []byte { return nil }, func([]byte) {}) - }).ShouldNot(Panic()) - }) - }) - - Context("when it also takes a done channel", func() { - It("should be fine", func() { - Ω(func() { - newNode(func() []byte { return nil }, func([]byte, Done) {}) - }).ShouldNot(Panic()) - }) - }) - - Context("if it takes anything else", func() { - It("should panic", func() { - Ω(func() { - newNode(func() []byte { return nil }, func([]byte, chan bool) {}) - }).Should(Panic()) - - Ω(func() { - newNode(func() []byte { return nil }, func(string) {}) - }).Should(Panic()) - }) - }) - - Context("if it takes nothing at all", func() { - It("should panic", func() { - Ω(func() { - newNode(func() []byte { return nil }, func() {}) - }).Should(Panic()) - }) - }) - - Context("if it returns something", func() { - It("should panic", func() { - Ω(func() { - newNode(func() []byte { return nil }, func([]byte) []byte { return nil }) - }).Should(Panic()) - }) - }) - }) - }) -}) diff --git a/internal/node.go b/internal/node.go new file mode 100644 index 0000000000..84f3a4aec3 --- /dev/null +++ b/internal/node.go @@ -0,0 +1,183 @@ +package internal + +import ( + "sort" + + "github.com/onsi/ginkgo/types" + "sync" +) + +var _global_node_id_counter = uint(0) +var _global_id_mutex = &sync.Mutex{} + +func UniqueNodeID() uint { + //There's a reace in the internal integration tests if we don't make + //accessing _global_node_id_counter safe across goroutines. + _global_id_mutex.Lock() + defer _global_id_mutex.Unlock() + _global_node_id_counter += 1 + return _global_node_id_counter +} + +/* Node */ +type Node struct { + ID uint + NodeType types.NodeType + + Text string + Body func() + CodeLocation types.CodeLocation + NestingLevel int + + MarkedFocus bool + MarkedPending bool +} + +func NewNode(nodeType types.NodeType, text string, body func(), codeLocation types.CodeLocation, markedFocus bool, markedPending bool) Node { + return Node{ + ID: UniqueNodeID(), + NodeType: nodeType, + Text: text, + Body: body, + CodeLocation: codeLocation, + MarkedFocus: markedFocus, + MarkedPending: markedPending, + NestingLevel: -1, + } +} + +func (n Node) IsZero() bool { + return n.ID == 0 +} + +/* Nodes */ +type Nodes []Node + +func (n Nodes) CopyAppend(nodes ...Node) Nodes { + out := Nodes{} + for _, node := range n { + out = append(out, node) + } + for _, node := range nodes { + out = append(out, node) + } + return out +} + +func (n Nodes) SplitAround(pivot Node) (Nodes, Nodes) { + left := Nodes{} + right := Nodes{} + found := false + for _, node := range n { + if node.ID == pivot.ID { + found = true + continue + } + if found { + right = append(right, node) + } else { + left = append(left, node) + } + } + + return left, right +} + +func (n Nodes) FirstNodeWithType(nodeTypes ...types.NodeType) Node { + for _, node := range n { + if node.NodeType.Is(nodeTypes...) { + return node + } + } + return Node{} +} + +func (n Nodes) WithType(nodeTypes ...types.NodeType) Nodes { + out := Nodes{} + for _, node := range n { + if node.NodeType.Is(nodeTypes...) { + out = append(out, node) + } + } + return out +} + +func (n Nodes) WithoutType(nodeTypes ...types.NodeType) Nodes { + out := Nodes{} + for _, node := range n { + if !node.NodeType.Is(nodeTypes...) { + out = append(out, node) + } + } + return out +} + +func (n Nodes) WithinNestingLevel(deepestNestingLevel int) Nodes { + out := Nodes{} + for _, node := range n { + if node.NestingLevel <= deepestNestingLevel { + out = append(out, node) + } + } + return out +} + +func (n Nodes) SortedByDescendingNestingLevel() Nodes { + out := Nodes{} + for _, node := range n { + out = append(out, node) + } + sort.SliceStable(out, func(i int, j int) bool { + return out[i].NestingLevel > out[j].NestingLevel + }) + + return out +} + +func (n Nodes) Texts() []string { + out := []string{} + for _, node := range n { + out = append(out, node.Text) + } + return out +} + +func (n Nodes) CodeLocations() []types.CodeLocation { + out := []types.CodeLocation{} + for _, node := range n { + out = append(out, node.CodeLocation) + } + return out +} + +func (n Nodes) BestTextFor(node Node) string { + if node.Text != "" { + return node.Text + } + parentNestingLevel := node.NestingLevel - 1 + for _, node := range n { + if node.Text != "" && node.NestingLevel == parentNestingLevel { + return node.Text + } + } + + return "" +} + +func (n Nodes) HasNodeMarkedPending() bool { + for _, node := range n { + if node.MarkedPending { + return true + } + } + return false +} + +func (n Nodes) HasNodeMarkedFocus() bool { + for _, node := range n { + if node.MarkedFocus { + return true + } + } + return false +} diff --git a/internal/node_test.go b/internal/node_test.go new file mode 100644 index 0000000000..a9d9dbb549 --- /dev/null +++ b/internal/node_test.go @@ -0,0 +1,304 @@ +package internal_test + +import ( + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/internal" +) + +var _ = Describe("UniqueNodeID", func() { + It("returns a unique id every time it's called", func() { + Ω(internal.UniqueNodeID()).ShouldNot(Equal(internal.UniqueNodeID())) + }) +}) + +var _ = Describe("Node", func() { + Describe("The NewNode constructor", func() { + It("creates a node with a non-zero id", func() { + var didRun bool + node := internal.NewNode(ntIt, "hummus", func() { didRun = true }, cl, true, false) + Ω(node.ID).ShouldNot(BeZero()) + Ω(node.NodeType).Should(Equal(ntIt)) + Ω(node.Text).Should(Equal("hummus")) + node.Body() + Ω(didRun).Should(BeTrue()) + Ω(node.CodeLocation).Should(Equal(cl)) + Ω(node.MarkedFocus).Should(BeTrue()) + Ω(node.MarkedPending).Should(BeFalse()) + Ω(node.NestingLevel).Should(Equal(-1)) + }) + }) + + Describe("IsZero()", func() { + It("returns true if the node is zero", func() { + Ω(Node{}.IsZero()).Should(BeTrue()) + }) + + It("returns false if the node is non-zero", func() { + node := internal.NewNode(ntIt, "hummus", func() {}, cl, false, false) + Ω(node.IsZero()).Should(BeFalse()) + }) + }) +}) + +var _ = Describe("Nodes", func() { + Describe("CopyAppend", func() { + var n1, n2, n3, n4 Node + + BeforeEach(func() { + n1, n2, n3, n4 = N(), N(), N(), N() + }) + + It("appends the passed in nodes and returns the result", func() { + result := Nodes{n1, n2}.CopyAppend(n3, n4) + Ω(result).Should(Equal(Nodes{n1, n2, n3, n4})) + }) + + It("makes a copy, leaving the original untouched", func() { + original := Nodes{n1, n2} + original.CopyAppend(n3, n4) + Ω(original).Should(Equal(Nodes{n1, n2})) + }) + }) + + Describe("SplitAround", func() { + var nodes Nodes + + BeforeEach(func() { + nodes = Nodes{N(), N(), N(), N(), N()} + }) + + Context("when the pivot is a member of nodes", func() { + Context("when the pivot is not at one of the ends", func() { + It("returns the correct left and right nodes", func() { + left, right := nodes.SplitAround(nodes[2]) + Ω(left).Should(Equal(Nodes{nodes[0], nodes[1]})) + Ω(right).Should(Equal(Nodes{nodes[3], nodes[4]})) + }) + }) + + Context("when the pivot is the first member", func() { + It("returns an empty left nodes and the complete right nodes", func() { + left, right := nodes.SplitAround(nodes[0]) + Ω(left).Should(BeEmpty()) + Ω(right).Should(Equal(Nodes{nodes[1], nodes[2], nodes[3], nodes[4]})) + + }) + }) + + Context("when the pivot is the last member", func() { + It("returns an empty right nodes and the complete left nodes", func() { + left, right := nodes.SplitAround(nodes[4]) + Ω(left).Should(Equal(Nodes{nodes[0], nodes[1], nodes[2], nodes[3]})) + Ω(right).Should(BeEmpty()) + }) + }) + }) + + Context("when the pivot is not in nodes", func() { + It("returns an empty right nodes and the complete left nodes", func() { + left, right := nodes.SplitAround(N()) + Ω(left).Should(Equal(nodes)) + Ω(right).Should(BeEmpty()) + }) + }) + }) + + Describe("FirstNodeWithType", func() { + var nodes Nodes + + BeforeEach(func() { + nodes = Nodes{N(ntCon), N("bef1", ntBef), N("bef2", ntBef), N(ntIt), N(ntAf)} + }) + + Context("when there is a matching node", func() { + It("returns the first node that matches one of the requested node types", func() { + Ω(nodes.FirstNodeWithType(ntAf, ntIt, ntBef).Text).Should(Equal("bef1")) + }) + }) + Context("when there is no matching node", func() { + It("returns an empty node", func() { + Ω(nodes.FirstNodeWithType(ntJusAf)).Should(BeZero()) + }) + }) + }) + + Describe("Filtering By NodeType", func() { + var nCon, nBef1, nBef2, nIt, nAf Node + var nodes Nodes + + BeforeEach(func() { + nCon = N(ntCon) + nBef1 = N(ntBef) + nBef2 = N(ntBef) + nIt = N(ntIt) + nAf = N(ntAf) + nodes = Nodes{nCon, nBef1, nBef2, nIt, nAf} + }) + + Describe("WithType", func() { + Context("when there are matching nodes", func() { + It("returns them while preserving order", func() { + Ω(nodes.WithType(ntIt, ntBef)).Should(Equal(Nodes{nBef1, nBef2, nIt})) + }) + }) + + Context("when there are no matching nodes", func() { + It("returns an empty Nodes{}", func() { + Ω(nodes.WithType(ntJusAf)).Should(BeEmpty()) + }) + }) + }) + + Describe("WithoutType", func() { + Context("when there are matching nodes", func() { + It("does not include them in the result", func() { + Ω(nodes.WithoutType(ntIt, ntBef)).Should(Equal(Nodes{nCon, nAf})) + }) + }) + + Context("when no nodes match", func() { + It("doesn't elide any nodes", func() { + Ω(nodes.WithoutType(ntJusAf)).Should(Equal(nodes)) + }) + }) + }) + }) + + Describe("SortedByDescendingNestingLevel", func() { + var n0A, n0B, n1A, n1B, n1C, n2A, n2B Node + var nodes Nodes + BeforeEach(func() { + n0A = N(NestingLevel(0)) + n0B = N(NestingLevel(0)) + n1A = N(NestingLevel(1)) + n1B = N(NestingLevel(1)) + n1C = N(NestingLevel(1)) + n2A = N(NestingLevel(2)) + n2B = N(NestingLevel(2)) + nodes = Nodes{n0A, n0B, n1A, n1B, n1C, n2A, n2B} + }) + + It("returns copy sorted by descending nesting level, preserving order within nesting level", func() { + Ω(nodes.SortedByDescendingNestingLevel()).Should(Equal(Nodes{n2A, n2B, n1A, n1B, n1C, n0A, n0B})) + Ω(nodes).Should(Equal(Nodes{n0A, n0B, n1A, n1B, n1C, n2A, n2B}), "original nodes should not have been modified") + }) + }) + + Describe("WithinNestingLevel", func() { + var n0, n1, n2a, n2b, n3 Node + var nodes Nodes + BeforeEach(func() { + n0 = N(NestingLevel(0)) + n1 = N(NestingLevel(1)) + n2a = N(NestingLevel(2)) + n3 = N(NestingLevel(3)) + n2b = N(NestingLevel(2)) + nodes = Nodes{n0, n1, n2a, n3, n2b} + }) + + It("returns nodes, in order, with nesting level equal to or less than the requested level", func() { + Ω(nodes.WithinNestingLevel(-1)).Should(BeEmpty()) + Ω(nodes.WithinNestingLevel(0)).Should(Equal(Nodes{n0})) + Ω(nodes.WithinNestingLevel(1)).Should(Equal(Nodes{n0, n1})) + Ω(nodes.WithinNestingLevel(2)).Should(Equal(Nodes{n0, n1, n2a, n2b})) + Ω(nodes.WithinNestingLevel(3)).Should(Equal(Nodes{n0, n1, n2a, n3, n2b})) + }) + }) + + Describe("Texts", func() { + var nodes Nodes + BeforeEach(func() { + nodes = Nodes{N("the first node"), N(""), N("2"), N("c"), N("")} + }) + + It("returns a string slice containing the individual node text strings in order", func() { + Ω(nodes.Texts()).Should(Equal([]string{"the first node", "", "2", "c", ""})) + }) + }) + + Describe("CodeLocation", func() { + var nodes Nodes + var cl1, cl2 types.CodeLocation + BeforeEach(func() { + cl1 = types.NewCodeLocation(0) + cl2 = types.NewCodeLocation(0) + nodes = Nodes{N(cl1), N(cl2), N()} + }) + + It("returns a types.CodeLocation sice containing the individual node code locations in order", func() { + Ω(nodes.CodeLocations()).Should(Equal([]types.CodeLocation{cl1, cl2, cl})) + }) + }) + + Describe("BestTextFor", func() { + var nIt, nBef1, nBef2 Node + var nodes Nodes + BeforeEach(func() { + nIt = N("an it", ntIt, NestingLevel(2)) + nBef1 = N(ntBef, NestingLevel(2)) + nBef2 = N(ntBef, NestingLevel(4)) + nodes = Nodes{ + N("the root container", ntCon, NestingLevel(0)), + N("the inner container", ntCon, NestingLevel(1)), + nBef1, + nIt, + nBef2, + } + }) + + Context("when the passed in node has text", func() { + It("returns that text", func() { + Ω(nodes.BestTextFor(nIt)).Should(Equal("an it")) + }) + }) + + Context("when the node has no text", func() { + Context("and there is a node one-nesting-level-up with text", func() { + It("returns that node's text", func() { + Ω(nodes.BestTextFor(nBef1)).Should(Equal("the inner container")) + }) + }) + + Context("and there is no node one-nesting-level up with text", func() { + It("returns empty string", func() { + Ω(nodes.BestTextFor(nBef2)).Should(Equal("")) + }) + }) + }) + }) + + Describe("HasNodeMarkedPending", func() { + Context("when there is a node marked pending", func() { + It("returns true", func() { + nodes := Nodes{N(), N(), N(MarkedPending(true)), N()} + Ω(nodes.HasNodeMarkedPending()).Should(BeTrue()) + }) + }) + + Context("when there is no node marked pending", func() { + It("returns false", func() { + nodes := Nodes{N(), N(), N()} + Ω(nodes.HasNodeMarkedPending()).Should(BeFalse()) + }) + }) + }) + + Describe("HasNodeMarkedFocus", func() { + Context("when there is a node marked focus", func() { + It("returns true", func() { + nodes := Nodes{N(), N(), N(MarkedFocus(true)), N()} + Ω(nodes.HasNodeMarkedFocus()).Should(BeTrue()) + }) + }) + + Context("when there is no node marked focus", func() { + It("returns false", func() { + nodes := Nodes{N(), N(), N()} + Ω(nodes.HasNodeMarkedFocus()).Should(BeFalse()) + }) + }) + }) +}) diff --git a/internal/parallel_support/forwarding_reporter.go b/internal/parallel_support/forwarding_reporter.go new file mode 100644 index 0000000000..91bf894b48 --- /dev/null +++ b/internal/parallel_support/forwarding_reporter.go @@ -0,0 +1,114 @@ +package parallel_support + +import ( + "bytes" + "encoding/json" + "net/http" + "os" + + "github.com/onsi/ginkgo/internal" + "github.com/onsi/ginkgo/reporters" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +/* +The OutputInterceptor is used by the ForwardingReporter to +intercept and capture all stdin and stderr output during a test run. +*/ +type OutputInterceptor interface { + StartInterceptingOutput() error + StopInterceptingAndReturnOutput() (string, error) +} + +type ConfigAndSummary struct { + Config config.GinkgoConfigType `json:"config"` + Summary types.SuiteSummary `json:"suite-summary"` +} + +/* +The ForwardingReporter is a Ginkgo reporter that forwards information to +a Ginkgo remote server. + +When streaming parallel test output, this repoter is automatically installed by Ginkgo. + +This is accomplished by passing in the GINKGO_REMOTE_REPORTING_SERVER environment variable to `go test`, the Ginkgo test runner +detects this environment variable (which should contain the host of the server) and automatically installs a ForwardingReporter +in place of Ginkgo's DefaultReporter. +*/ + +type ForwardingReporter struct { + serverHost string + outputInterceptor OutputInterceptor + debugFile *os.File + nestedReporter *reporters.DefaultReporter +} + +func NewForwardingReporter(config config.DefaultReporterConfigType, serverHost string, outputInterceptor OutputInterceptor, ginkgoWriter *internal.Writer, debugFile string) *ForwardingReporter { + reporter := &ForwardingReporter{ + serverHost: serverHost, + outputInterceptor: outputInterceptor, + debugFile: nil, + } + + if debugFile != "" { + var err error + reporter.debugFile, err = os.Create(debugFile) + if err != nil { + panic(err.Error()) + } + + config.Succinct = false + config.NoColor = true + config.Verbose = true + config.FullTrace = true + reporter.nestedReporter = reporters.NewDefaultReporter(config, reporter.debugFile) + } + + return reporter +} + +func (reporter *ForwardingReporter) post(path string, data interface{}) { + encoded, _ := json.Marshal(data) + buffer := bytes.NewBuffer(encoded) + http.Post(reporter.serverHost+path, "application/json", buffer) +} + +func (reporter *ForwardingReporter) SpecSuiteWillBegin(conf config.GinkgoConfigType, summary types.SuiteSummary) { + data := ConfigAndSummary{Config: conf, Summary: summary} + + reporter.outputInterceptor.StartInterceptingOutput() + if reporter.debugFile != nil { + reporter.nestedReporter.SpecSuiteWillBegin(conf, summary) + reporter.debugFile.Sync() + } + reporter.post("/SpecSuiteWillBegin", data) +} + +func (reporter *ForwardingReporter) WillRun(summary types.Summary) { + if reporter.debugFile != nil { + reporter.nestedReporter.WillRun(summary) + reporter.debugFile.Sync() + } +} + +func (reporter *ForwardingReporter) DidRun(summary types.Summary) { + output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() + reporter.outputInterceptor.StartInterceptingOutput() + summary.CapturedStdOutErr = output + if reporter.debugFile != nil { + reporter.nestedReporter.DidRun(summary) + reporter.debugFile.Sync() + } + reporter.post("/DidRun", summary) +} + +func (reporter *ForwardingReporter) SpecSuiteDidEnd(summary types.SuiteSummary) { + reporter.outputInterceptor.StopInterceptingAndReturnOutput() + if reporter.debugFile != nil { + reporter.nestedReporter.SpecSuiteDidEnd(summary) + reporter.debugFile.Sync() + } + reporter.post("/SpecSuiteDidEnd", summary) +} diff --git a/internal/parallel_support/forwarding_reporter_test.go b/internal/parallel_support/forwarding_reporter_test.go new file mode 100644 index 0000000000..20c1038620 --- /dev/null +++ b/internal/parallel_support/forwarding_reporter_test.go @@ -0,0 +1,162 @@ +package parallel_support_test + +import ( + "fmt" + "io/ioutil" + "os" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/internal/parallel_support" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/ghttp" +) + +var _ = Describe("ForwardingReporter", func() { + var ( + server *ghttp.Server + reporter *ForwardingReporter + interceptor *fakeOutputInterceptor + debugFile string + ) + + DebugFileContents := func() *gbytes.Buffer { + content, err := ioutil.ReadFile(debugFile) + Ω(err).ShouldNot(HaveOccurred()) + return gbytes.BufferWithBytes(content) + } + + BeforeEach(func() { + debugFile = fmt.Sprintf("ginkgo-debug-%d", GinkgoParallelNode()) + server = ghttp.NewServer() + + interceptor = &fakeOutputInterceptor{ + InterceptedOutput: "The intercepted output!", + } + + reporter = NewForwardingReporter(config.DefaultReporterConfigType{}, server.URL(), interceptor, nil, debugFile) + }) + + AfterEach(func() { + os.Remove(debugFile) + server.Close() + }) + + Context("When a suite begins", func() { + BeforeEach(func() { + suiteSummary := types.SuiteSummary{ + SuiteDescription: "My Test Suite", + } + + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/SpecSuiteWillBegin"), + ghttp.VerifyJSONRepresenting(ConfigAndSummary{ + Config: config.GinkgoConfig, + Summary: suiteSummary, + }), + )) + + reporter.SpecSuiteWillBegin(config.GinkgoConfig, suiteSummary) + }) + + It("should start intercepting output", func() { + Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) + }) + + It("should POST the SuiteSummary and Ginkgo Config to the Ginkgo server", func() { + Ω(server.ReceivedRequests()).Should(HaveLen(1)) + }) + + It("should report to the debugFile", func() { + debug := DebugFileContents() + Ω(debug).Should(gbytes.Say("Running Suite: My Test Suite")) + }) + }) + + Context("When a spec will run", func() { + BeforeEach(func() { + reporter.WillRun(types.Summary{ + State: types.SpecStatePassed, + NodeTexts: []string{"My test"}, + NodeLocations: []types.CodeLocation{types.NewCodeLocation(0)}, + }) + }) + + It("should not send anything to the server", func() { + Ω(server.ReceivedRequests()).Should(BeEmpty()) + }) + + It("should report to the debugFile", func() { + debug := DebugFileContents() + Ω(debug).Should(gbytes.Say("My test")) + Ω(debug).Should(gbytes.Say(`forwarding_reporter_test.go:\d+`)) + }) + }) + + Context("When a spec completes", func() { + BeforeEach(func() { + cls := []types.CodeLocation{types.NewCodeLocation(0)} + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/DidRun"), + ghttp.VerifyJSONRepresenting(types.Summary{ + State: types.SpecStatePassed, + NodeTexts: []string{"My test"}, + NodeLocations: cls, + CapturedStdOutErr: interceptor.InterceptedOutput, + }), + )) + + reporter.DidRun(types.Summary{ + State: types.SpecStatePassed, + NodeTexts: []string{"My test"}, + NodeLocations: cls, + }) + }) + + It("should POST the SpecSummary to the Ginkgo server and include any intercepted output", func() { + Ω(server.ReceivedRequests()).Should(HaveLen(1)) + }) + + It("should stop, then start intercepting output", func() { + Ω(interceptor.DidStopInterceptingOutput).Should(BeTrue()) + Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) + }) + + It("should report to the debugFile", func() { + debug := DebugFileContents() + Ω(debug).Should(gbytes.Say("My test")) + Ω(debug).Should(gbytes.Say(`forwarding_reporter_test.go:\d+`)) + Ω(debug).Should(gbytes.Say("Begin Captured StdOut/StdErr Output >>")) + Ω(debug).Should(gbytes.Say("The intercepted output!")) + }) + }) + + Context("When a suite ends", func() { + BeforeEach(func() { + suiteSummary := types.SuiteSummary{ + SuiteDescription: "My Test Suite", + SuiteSucceeded: true, + NumberOfPassedSpecs: 10, + } + + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/SpecSuiteDidEnd"), + ghttp.VerifyJSONRepresenting(suiteSummary), + )) + + reporter.SpecSuiteDidEnd(suiteSummary) + }) + + It("should POST the SuiteSummary to the Ginkgo server", func() { + Ω(server.ReceivedRequests()).Should(HaveLen(1)) + }) + + It("should report to the debugFile", func() { + debug := DebugFileContents() + Ω(debug).Should(gbytes.Say("SUCCESS!")) + Ω(debug).Should(gbytes.Say("10 Passed |")) + }) + }) +}) diff --git a/internal/remote/output_interceptor_unix.go b/internal/parallel_support/output_interceptor_unix.go similarity index 63% rename from internal/remote/output_interceptor_unix.go rename to internal/parallel_support/output_interceptor_unix.go index 774967db66..2ce94223b4 100644 --- a/internal/remote/output_interceptor_unix.go +++ b/internal/parallel_support/output_interceptor_unix.go @@ -1,13 +1,12 @@ // +build freebsd openbsd netbsd dragonfly darwin linux solaris -package remote +package parallel_support import ( "errors" "io/ioutil" "os" - "github.com/nxadm/tail" "golang.org/x/sys/unix" ) @@ -17,10 +16,11 @@ func NewOutputInterceptor() OutputInterceptor { type outputInterceptor struct { redirectFile *os.File - streamTarget *os.File intercepting bool - tailer *tail.Tail doneTailing chan bool + + stdoutClone int + stderrClone int } func (interceptor *outputInterceptor) StartInterceptingOutput() error { @@ -36,23 +36,14 @@ func (interceptor *outputInterceptor) StartInterceptingOutput() error { return err } + interceptor.stdoutClone, _ = unix.Dup(1) + interceptor.stderrClone, _ = unix.Dup(2) + // This might call Dup3 if the dup2 syscall is not available, e.g. on // linux/arm64 or linux/riscv64 unix.Dup2(int(interceptor.redirectFile.Fd()), 1) unix.Dup2(int(interceptor.redirectFile.Fd()), 2) - if interceptor.streamTarget != nil { - interceptor.tailer, _ = tail.TailFile(interceptor.redirectFile.Name(), tail.Config{Follow: true}) - interceptor.doneTailing = make(chan bool) - - go func() { - for line := range interceptor.tailer.Lines { - interceptor.streamTarget.Write([]byte(line.Text + "\n")) - } - close(interceptor.doneTailing) - }() - } - return nil } @@ -65,18 +56,10 @@ func (interceptor *outputInterceptor) StopInterceptingAndReturnOutput() (string, output, err := ioutil.ReadFile(interceptor.redirectFile.Name()) os.Remove(interceptor.redirectFile.Name()) - interceptor.intercepting = false + unix.Dup2(interceptor.stdoutClone, 1) + unix.Dup2(interceptor.stderrClone, 2) - if interceptor.streamTarget != nil { - interceptor.tailer.Stop() - interceptor.tailer.Cleanup() - <-interceptor.doneTailing - interceptor.streamTarget.Sync() - } + interceptor.intercepting = false return string(output), err } - -func (interceptor *outputInterceptor) StreamTo(out *os.File) { - interceptor.streamTarget = out -} diff --git a/internal/remote/output_interceptor_win.go b/internal/parallel_support/output_interceptor_win.go similarity index 87% rename from internal/remote/output_interceptor_win.go rename to internal/parallel_support/output_interceptor_win.go index 40c790336c..bebd32def1 100644 --- a/internal/remote/output_interceptor_win.go +++ b/internal/parallel_support/output_interceptor_win.go @@ -1,10 +1,9 @@ // +build windows -package remote +package parallel_support import ( "errors" - "os" ) func NewOutputInterceptor() OutputInterceptor { @@ -32,5 +31,3 @@ func (interceptor *outputInterceptor) StopInterceptingAndReturnOutput() (string, return "", nil } - -func (interceptor *outputInterceptor) StreamTo(*os.File) {} diff --git a/internal/parallel_support/parallel_support_suite_test.go b/internal/parallel_support/parallel_support_suite_test.go new file mode 100644 index 0000000000..c2c86c73e0 --- /dev/null +++ b/internal/parallel_support/parallel_support_suite_test.go @@ -0,0 +1,63 @@ +package parallel_support_test + +import ( + "io" + "io/ioutil" + "net/http" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestParallelSupport(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Parallel Support Suite") +} + +type post struct { + url string + bodyType string + bodyContent []byte +} + +type fakePoster struct { + posts []post +} + +func newFakePoster() *fakePoster { + return &fakePoster{ + posts: make([]post, 0), + } +} + +func (poster *fakePoster) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { + bodyContent, _ := ioutil.ReadAll(body) + poster.posts = append(poster.posts, post{ + url: url, + bodyType: bodyType, + bodyContent: bodyContent, + }) + return nil, nil +} + +type fakeOutputInterceptor struct { + DidStartInterceptingOutput bool + DidStopInterceptingOutput bool + InterceptedOutput string +} + +func (interceptor *fakeOutputInterceptor) StartInterceptingOutput() error { + interceptor.DidStartInterceptingOutput = true + return nil +} + +func (interceptor *fakeOutputInterceptor) StopInterceptingAndReturnOutput() (string, error) { + interceptor.DidStopInterceptingOutput = true + return interceptor.InterceptedOutput, nil +} + +func (interceptor *fakeOutputInterceptor) StreamTo(*os.File) { +} diff --git a/internal/remote/server.go b/internal/parallel_support/server.go similarity index 59% rename from internal/remote/server.go rename to internal/parallel_support/server.go index 93e9dac057..1ef890d4a1 100644 --- a/internal/remote/server.go +++ b/internal/parallel_support/server.go @@ -5,18 +5,16 @@ This is used, primarily, to enable streaming parallel test output but has, in pr */ -package remote +package parallel_support import ( "encoding/json" - "io/ioutil" "net" "net/http" "sync" - "github.com/onsi/ginkgo/internal/spec_iterator" + "github.com/onsi/ginkgo/internal" - "github.com/onsi/ginkgo/config" "github.com/onsi/ginkgo/reporters" "github.com/onsi/ginkgo/types" ) @@ -26,27 +24,36 @@ Server spins up on an automatically selected port and listens for communication It then forwards that communication to attached reporters. */ type Server struct { + Done chan interface{} + listener net.Listener - reporters []reporters.Reporter + reporter reporters.Reporter alives []func() bool lock *sync.Mutex beforeSuiteData types.RemoteBeforeSuiteData parallelTotal int counter int + + numSuiteDidBegins int + numSuiteDidEnds int + aggregatedSuiteEndSummary types.SuiteSummary + summaryHoldingArea []types.Summary } //Create a new server, automatically selecting a port -func NewServer(parallelTotal int) (*Server, error) { +func NewServer(parallelTotal int, reporter reporters.Reporter) (*Server, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, err } return &Server{ listener: listener, + reporter: reporter, lock: &sync.Mutex{}, alives: make([]func() bool, parallelTotal), beforeSuiteData: types.RemoteBeforeSuiteData{Data: nil, State: types.RemoteBeforeSuiteStatePending}, parallelTotal: parallelTotal, + Done: make(chan interface{}), }, nil } @@ -58,17 +65,14 @@ func (server *Server) Start() { //streaming endpoints mux.HandleFunc("/SpecSuiteWillBegin", server.specSuiteWillBegin) - mux.HandleFunc("/BeforeSuiteDidRun", server.beforeSuiteDidRun) - mux.HandleFunc("/AfterSuiteDidRun", server.afterSuiteDidRun) - mux.HandleFunc("/SpecWillRun", server.specWillRun) - mux.HandleFunc("/SpecDidComplete", server.specDidComplete) + mux.HandleFunc("/DidRun", server.didRun) mux.HandleFunc("/SpecSuiteDidEnd", server.specSuiteDidEnd) //synchronization endpoints mux.HandleFunc("/BeforeSuiteState", server.handleBeforeSuiteState) - mux.HandleFunc("/RemoteAfterSuiteData", server.handleRemoteAfterSuiteData) + mux.HandleFunc("/AfterSuiteState", server.handleRemoteAfterSuiteData) mux.HandleFunc("/counter", server.handleCounter) - mux.HandleFunc("/has-counter", server.handleHasCounter) //for backward compatibility + mux.HandleFunc("/up", server.handleUp) go httpServer.Serve(server.listener) } @@ -88,78 +92,78 @@ func (server *Server) Address() string { // //The server will forward all received messages to Ginkgo reporters registered with `RegisterReporters` -func (server *Server) readAll(request *http.Request) []byte { +func (server *Server) decode(request *http.Request, object interface{}) error { defer request.Body.Close() - body, _ := ioutil.ReadAll(request.Body) - return body -} - -func (server *Server) RegisterReporters(reporters ...reporters.Reporter) { - server.reporters = reporters + return json.NewDecoder(request.Body).Decode(object) } func (server *Server) specSuiteWillBegin(writer http.ResponseWriter, request *http.Request) { - body := server.readAll(request) - - var data struct { - Config config.GinkgoConfigType `json:"config"` - Summary *types.SuiteSummary `json:"suite-summary"` - } + server.lock.Lock() + defer server.lock.Unlock() - json.Unmarshal(body, &data) + server.numSuiteDidBegins += 1 - for _, reporter := range server.reporters { - reporter.SpecSuiteWillBegin(data.Config, data.Summary) + var data ConfigAndSummary + err := server.decode(request, &data) + if err != nil { + writer.WriteHeader(http.StatusBadRequest) + return } -} -func (server *Server) beforeSuiteDidRun(writer http.ResponseWriter, request *http.Request) { - body := server.readAll(request) - var setupSummary *types.SetupSummary - json.Unmarshal(body, &setupSummary) + // all summaries are identical, so it's fine to simply emit the last one of these + if server.numSuiteDidBegins == server.parallelTotal { + server.reporter.SpecSuiteWillBegin(data.Config, data.Summary) - for _, reporter := range server.reporters { - reporter.BeforeSuiteDidRun(setupSummary) + for _, summary := range server.summaryHoldingArea { + server.reporter.WillRun(summary) + server.reporter.DidRun(summary) + } + + server.summaryHoldingArea = nil } } -func (server *Server) afterSuiteDidRun(writer http.ResponseWriter, request *http.Request) { - body := server.readAll(request) - var setupSummary *types.SetupSummary - json.Unmarshal(body, &setupSummary) +func (server *Server) didRun(writer http.ResponseWriter, request *http.Request) { + server.lock.Lock() + defer server.lock.Unlock() - for _, reporter := range server.reporters { - reporter.AfterSuiteDidRun(setupSummary) + var summary types.Summary + err := server.decode(request, &summary) + if err != nil { + writer.WriteHeader(http.StatusBadRequest) + return } -} -func (server *Server) specWillRun(writer http.ResponseWriter, request *http.Request) { - body := server.readAll(request) - var specSummary *types.SpecSummary - json.Unmarshal(body, &specSummary) - - for _, reporter := range server.reporters { - reporter.SpecWillRun(specSummary) + if server.numSuiteDidBegins == server.parallelTotal { + server.reporter.WillRun(summary) + server.reporter.DidRun(summary) + } else { + server.summaryHoldingArea = append(server.summaryHoldingArea, summary) } } -func (server *Server) specDidComplete(writer http.ResponseWriter, request *http.Request) { - body := server.readAll(request) - var specSummary *types.SpecSummary - json.Unmarshal(body, &specSummary) +func (server *Server) specSuiteDidEnd(writer http.ResponseWriter, request *http.Request) { + server.lock.Lock() + defer server.lock.Unlock() - for _, reporter := range server.reporters { - reporter.SpecDidComplete(specSummary) + server.numSuiteDidEnds += 1 + + var summary types.SuiteSummary + err := server.decode(request, &summary) + if err != nil { + writer.WriteHeader(http.StatusBadRequest) + return } -} -func (server *Server) specSuiteDidEnd(writer http.ResponseWriter, request *http.Request) { - body := server.readAll(request) - var suiteSummary *types.SuiteSummary - json.Unmarshal(body, &suiteSummary) + if server.numSuiteDidEnds == 1 { + server.aggregatedSuiteEndSummary = summary + } else { + server.aggregatedSuiteEndSummary = server.aggregatedSuiteEndSummary.Add(summary) + } - for _, reporter := range server.reporters { - reporter.SpecSuiteDidEnd(suiteSummary) + if server.numSuiteDidEnds == server.parallelTotal { + server.reporter.SpecSuiteDidEnd(server.aggregatedSuiteEndSummary) + close(server.Done) } } @@ -210,7 +214,7 @@ func (server *Server) handleRemoteAfterSuiteData(writer http.ResponseWriter, req } func (server *Server) handleCounter(writer http.ResponseWriter, request *http.Request) { - c := spec_iterator.Counter{} + c := internal.Counter{} server.lock.Lock() c.Index = server.counter server.counter++ @@ -219,6 +223,6 @@ func (server *Server) handleCounter(writer http.ResponseWriter, request *http.Re json.NewEncoder(writer).Encode(c) } -func (server *Server) handleHasCounter(writer http.ResponseWriter, request *http.Request) { - writer.Write([]byte("")) +func (server *Server) handleUp(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) } diff --git a/internal/remote/server_test.go b/internal/parallel_support/server_test.go similarity index 54% rename from internal/remote/server_test.go rename to internal/parallel_support/server_test.go index 36bd00355b..3ed0d6b8dd 100644 --- a/internal/remote/server_test.go +++ b/internal/parallel_support/server_test.go @@ -1,30 +1,38 @@ -package remote_test +package parallel_support_test import ( + "time" + . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/remote" + . "github.com/onsi/ginkgo/internal/parallel_support" . "github.com/onsi/gomega" - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/reporters" - "github.com/onsi/ginkgo/types" - "bytes" "encoding/json" "net/http" + + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/internal/test_helpers" + "github.com/onsi/ginkgo/types" ) var _ = Describe("Server", func() { var ( - server *Server + server *Server + reporter *FakeReporter + forwardingReporter *ForwardingReporter ) BeforeEach(func() { var err error - server, err = NewServer(3) + reporter = &FakeReporter{} + server, err = NewServer(3, reporter) Ω(err).ShouldNot(HaveOccurred()) server.Start() + Eventually(StatusCodePoller(server.Address() + "/up")).Should(Equal(http.StatusOK)) + + forwardingReporter = NewForwardingReporter(config.DefaultReporterConfigType{}, server.Address(), &fakeOutputInterceptor{}, nil, "") }) AfterEach(func() { @@ -32,92 +40,99 @@ var _ = Describe("Server", func() { }) Describe("Streaming endpoints", func() { - var ( - reporterA, reporterB *reporters.FakeReporter - forwardingReporter *ForwardingReporter - - suiteSummary *types.SuiteSummary - setupSummary *types.SetupSummary - specSummary *types.SpecSummary - ) + var beginSummary, thirdBeginSummary types.SuiteSummary + var endSummary1, endSummary2, endSummary3 types.SuiteSummary + var summaryA, summaryB, summaryC types.Summary BeforeEach(func() { - reporterA = reporters.NewFakeReporter() - reporterB = reporters.NewFakeReporter() - - server.RegisterReporters(reporterA, reporterB) + beginSummary = types.SuiteSummary{SuiteDescription: "my sweet suite"} + thirdBeginSummary = types.SuiteSummary{SuiteDescription: "laste one in gets forwarded"} - forwardingReporter = NewForwardingReporter(config.DefaultReporterConfigType{}, server.Address(), &http.Client{}, &fakeOutputInterceptor{}, nil, "") + summaryA = types.Summary{NodeTexts: []string{"A"}} + summaryB = types.Summary{NodeTexts: []string{"B"}} + summaryC = types.Summary{NodeTexts: []string{"C"}} - suiteSummary = &types.SuiteSummary{ - SuiteDescription: "My Test Suite", - } - - setupSummary = &types.SetupSummary{ - State: types.SpecStatePassed, - } - - specSummary = &types.SpecSummary{ - ComponentTexts: []string{"My", "Spec"}, - State: types.SpecStatePassed, - } + endSummary1 = types.SuiteSummary{NumberOfPassedSpecs: 2, RunTime: time.Second, SuiteSucceeded: true} + endSummary2 = types.SuiteSummary{NumberOfPassedSpecs: 3, RunTime: time.Minute, NumberOfSkippedSpecs: 2, SuiteSucceeded: true} + endSummary3 = types.SuiteSummary{NumberOfPassedSpecs: 1, RunTime: time.Second, NumberOfFailedSpecs: 3, SuiteSucceeded: false} }) It("should make its address available", func() { Ω(server.Address()).Should(MatchRegexp(`http://127.0.0.1:\d{2,}`)) }) - Describe("/SpecSuiteWillBegin", func() { - It("should decode and forward the Ginkgo config and suite summary", func(done Done) { - forwardingReporter.SpecSuiteWillBegin(config.GinkgoConfig, suiteSummary) - Ω(reporterA.Config).Should(Equal(config.GinkgoConfig)) - Ω(reporterB.Config).Should(Equal(config.GinkgoConfig)) - Ω(reporterA.BeginSummary).Should(Equal(suiteSummary)) - Ω(reporterB.BeginSummary).Should(Equal(suiteSummary)) - close(done) + Context("before all nodes have reported SpecSuiteWillBegin", func() { + BeforeEach(func() { + forwardingReporter.SpecSuiteWillBegin(config.GinkgoConfig, beginSummary) + forwardingReporter.DidRun(summaryA) + forwardingReporter.SpecSuiteWillBegin(config.GinkgoConfig, beginSummary) + forwardingReporter.DidRun(summaryB) }) - }) - Describe("/BeforeSuiteDidRun", func() { - It("should decode and forward the setup summary", func() { - forwardingReporter.BeforeSuiteDidRun(setupSummary) - Ω(reporterA.BeforeSuiteSummary).Should(Equal(setupSummary)) - Ω(reporterB.BeforeSuiteSummary).Should(Equal(setupSummary)) + It("should not forward anything to the attached reporter", func() { + Ω(reporter.Config).Should(BeZero()) + Ω(reporter.Begin).Should(BeZero()) + Ω(reporter.Will).Should(BeEmpty()) + Ω(reporter.Did).Should(BeEmpty()) }) - }) - Describe("/AfterSuiteDidRun", func() { - It("should decode and forward the setup summary", func() { - forwardingReporter.AfterSuiteDidRun(setupSummary) - Ω(reporterA.AfterSuiteSummary).Should(Equal(setupSummary)) - Ω(reporterB.AfterSuiteSummary).Should(Equal(setupSummary)) - }) - }) + Context("when the final node reports SpecSuiteWillBegin", func() { + BeforeEach(func() { + forwardingReporter.SpecSuiteWillBegin(config.GinkgoConfig, thirdBeginSummary) + }) - Describe("/SpecWillRun", func() { - It("should decode and forward the spec summary", func(done Done) { - forwardingReporter.SpecWillRun(specSummary) - Ω(reporterA.SpecWillRunSummaries[0]).Should(Equal(specSummary)) - Ω(reporterB.SpecWillRunSummaries[0]).Should(Equal(specSummary)) - close(done) - }) - }) + It("forwards to SpecSuiteWillBegin and catches up on any received summareis", func() { + Ω(reporter.Config).Should(Equal(config.GinkgoConfig)) + Ω(reporter.Begin).Should(Equal(thirdBeginSummary)) + Ω(reporter.Will.Names()).Should(ConsistOf("A", "B")) + Ω(reporter.Did.Names()).Should(ConsistOf("A", "B")) + }) - Describe("/SpecDidComplete", func() { - It("should decode and forward the spec summary", func(done Done) { - forwardingReporter.SpecDidComplete(specSummary) - Ω(reporterA.SpecSummaries[0]).Should(Equal(specSummary)) - Ω(reporterB.SpecSummaries[0]).Should(Equal(specSummary)) - close(done) - }) - }) + Context("any subsequent summaries", func() { + BeforeEach(func() { + forwardingReporter.DidRun(summaryC) + }) - Describe("/SpecSuiteDidEnd", func() { - It("should decode and forward the suite summary", func(done Done) { - forwardingReporter.SpecSuiteDidEnd(suiteSummary) - Ω(reporterA.EndSummary).Should(Equal(suiteSummary)) - Ω(reporterB.EndSummary).Should(Equal(suiteSummary)) - close(done) + It("are forwarded immediately", func() { + Ω(reporter.Will.Names()).Should(ConsistOf("A", "B", "C")) + Ω(reporter.Did.Names()).Should(ConsistOf("A", "B", "C")) + }) + }) + + Context("when SpecSuiteDidEnd start arriving", func() { + BeforeEach(func() { + forwardingReporter.SpecSuiteDidEnd(endSummary1) + forwardingReporter.SpecSuiteDidEnd(endSummary2) + }) + + It("does not forward them yet...", func() { + Ω(reporter.End).Should(BeZero()) + }) + + It("doesn't signal it's done", func() { + Ω(server.Done).ShouldNot(BeClosed()) + }) + + Context("when the final SpecSuiteDidEnd arrive", func() { + BeforeEach(func() { + forwardingReporter.SpecSuiteDidEnd(endSummary3) + }) + + It("forwards the aggregation of all received end summaries", func() { + Ω(reporter.End).Should(Equal(types.SuiteSummary{ + SuiteSucceeded: false, + NumberOfPassedSpecs: 6, + NumberOfSkippedSpecs: 2, + NumberOfFailedSpecs: 3, + RunTime: time.Minute, + })) + }) + + It("should signal it's done", func() { + Ω(server.Done).Should(BeClosed()) + }) + }) + }) }) }) }) @@ -208,7 +223,7 @@ var _ = Describe("Server", func() { Describe("GETting RemoteAfterSuiteData", func() { getRemoteAfterSuiteData := func() bool { - resp, err := http.Get(server.Address() + "/RemoteAfterSuiteData") + resp, err := http.Get(server.Address() + "/AfterSuiteState") Ω(err).ShouldNot(HaveOccurred()) Ω(resp.StatusCode).Should(Equal(http.StatusOK)) diff --git a/internal/remote/aggregator.go b/internal/remote/aggregator.go deleted file mode 100644 index 992437d9e0..0000000000 --- a/internal/remote/aggregator.go +++ /dev/null @@ -1,249 +0,0 @@ -/* - -Aggregator is a reporter used by the Ginkgo CLI to aggregate and present parallel test output -coherently as tests complete. You shouldn't need to use this in your code. To run tests in parallel: - - ginkgo -nodes=N - -where N is the number of nodes you desire. -*/ -package remote - -import ( - "time" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/reporters/stenographer" - "github.com/onsi/ginkgo/types" -) - -type configAndSuite struct { - config config.GinkgoConfigType - summary *types.SuiteSummary -} - -type Aggregator struct { - nodeCount int - config config.DefaultReporterConfigType - stenographer stenographer.Stenographer - result chan bool - - suiteBeginnings chan configAndSuite - aggregatedSuiteBeginnings []configAndSuite - - beforeSuites chan *types.SetupSummary - aggregatedBeforeSuites []*types.SetupSummary - - afterSuites chan *types.SetupSummary - aggregatedAfterSuites []*types.SetupSummary - - specCompletions chan *types.SpecSummary - completedSpecs []*types.SpecSummary - - suiteEndings chan *types.SuiteSummary - aggregatedSuiteEndings []*types.SuiteSummary - specs []*types.SpecSummary - - startTime time.Time -} - -func NewAggregator(nodeCount int, result chan bool, config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *Aggregator { - aggregator := &Aggregator{ - nodeCount: nodeCount, - result: result, - config: config, - stenographer: stenographer, - - suiteBeginnings: make(chan configAndSuite), - beforeSuites: make(chan *types.SetupSummary), - afterSuites: make(chan *types.SetupSummary), - specCompletions: make(chan *types.SpecSummary), - suiteEndings: make(chan *types.SuiteSummary), - } - - go aggregator.mux() - - return aggregator -} - -func (aggregator *Aggregator) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { - aggregator.suiteBeginnings <- configAndSuite{config, summary} -} - -func (aggregator *Aggregator) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { - aggregator.beforeSuites <- setupSummary -} - -func (aggregator *Aggregator) AfterSuiteDidRun(setupSummary *types.SetupSummary) { - aggregator.afterSuites <- setupSummary -} - -func (aggregator *Aggregator) SpecWillRun(specSummary *types.SpecSummary) { - //noop -} - -func (aggregator *Aggregator) SpecDidComplete(specSummary *types.SpecSummary) { - aggregator.specCompletions <- specSummary -} - -func (aggregator *Aggregator) SpecSuiteDidEnd(summary *types.SuiteSummary) { - aggregator.suiteEndings <- summary -} - -func (aggregator *Aggregator) mux() { -loop: - for { - select { - case configAndSuite := <-aggregator.suiteBeginnings: - aggregator.registerSuiteBeginning(configAndSuite) - case setupSummary := <-aggregator.beforeSuites: - aggregator.registerBeforeSuite(setupSummary) - case setupSummary := <-aggregator.afterSuites: - aggregator.registerAfterSuite(setupSummary) - case specSummary := <-aggregator.specCompletions: - aggregator.registerSpecCompletion(specSummary) - case suite := <-aggregator.suiteEndings: - finished, passed := aggregator.registerSuiteEnding(suite) - if finished { - aggregator.result <- passed - break loop - } - } - } -} - -func (aggregator *Aggregator) registerSuiteBeginning(configAndSuite configAndSuite) { - aggregator.aggregatedSuiteBeginnings = append(aggregator.aggregatedSuiteBeginnings, configAndSuite) - - if len(aggregator.aggregatedSuiteBeginnings) == 1 { - aggregator.startTime = time.Now() - } - - if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount { - return - } - - aggregator.stenographer.AnnounceSuite(configAndSuite.summary.SuiteDescription, configAndSuite.config.RandomSeed, configAndSuite.config.RandomizeAllSpecs, aggregator.config.Succinct) - - totalNumberOfSpecs := 0 - if len(aggregator.aggregatedSuiteBeginnings) > 0 { - totalNumberOfSpecs = configAndSuite.summary.NumberOfSpecsBeforeParallelization - } - - aggregator.stenographer.AnnounceTotalNumberOfSpecs(totalNumberOfSpecs, aggregator.config.Succinct) - aggregator.stenographer.AnnounceAggregatedParallelRun(aggregator.nodeCount, aggregator.config.Succinct) - aggregator.flushCompletedSpecs() -} - -func (aggregator *Aggregator) registerBeforeSuite(setupSummary *types.SetupSummary) { - aggregator.aggregatedBeforeSuites = append(aggregator.aggregatedBeforeSuites, setupSummary) - aggregator.flushCompletedSpecs() -} - -func (aggregator *Aggregator) registerAfterSuite(setupSummary *types.SetupSummary) { - aggregator.aggregatedAfterSuites = append(aggregator.aggregatedAfterSuites, setupSummary) - aggregator.flushCompletedSpecs() -} - -func (aggregator *Aggregator) registerSpecCompletion(specSummary *types.SpecSummary) { - aggregator.completedSpecs = append(aggregator.completedSpecs, specSummary) - aggregator.specs = append(aggregator.specs, specSummary) - aggregator.flushCompletedSpecs() -} - -func (aggregator *Aggregator) flushCompletedSpecs() { - if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount { - return - } - - for _, setupSummary := range aggregator.aggregatedBeforeSuites { - aggregator.announceBeforeSuite(setupSummary) - } - - for _, specSummary := range aggregator.completedSpecs { - aggregator.announceSpec(specSummary) - } - - for _, setupSummary := range aggregator.aggregatedAfterSuites { - aggregator.announceAfterSuite(setupSummary) - } - - aggregator.aggregatedBeforeSuites = []*types.SetupSummary{} - aggregator.completedSpecs = []*types.SpecSummary{} - aggregator.aggregatedAfterSuites = []*types.SetupSummary{} -} - -func (aggregator *Aggregator) announceBeforeSuite(setupSummary *types.SetupSummary) { - aggregator.stenographer.AnnounceCapturedOutput(setupSummary.CapturedOutput) - if setupSummary.State != types.SpecStatePassed { - aggregator.stenographer.AnnounceBeforeSuiteFailure(setupSummary, aggregator.config.Succinct, aggregator.config.FullTrace) - } -} - -func (aggregator *Aggregator) announceAfterSuite(setupSummary *types.SetupSummary) { - aggregator.stenographer.AnnounceCapturedOutput(setupSummary.CapturedOutput) - if setupSummary.State != types.SpecStatePassed { - aggregator.stenographer.AnnounceAfterSuiteFailure(setupSummary, aggregator.config.Succinct, aggregator.config.FullTrace) - } -} - -func (aggregator *Aggregator) announceSpec(specSummary *types.SpecSummary) { - if aggregator.config.Verbose && specSummary.State != types.SpecStatePending && specSummary.State != types.SpecStateSkipped { - aggregator.stenographer.AnnounceSpecWillRun(specSummary) - } - - aggregator.stenographer.AnnounceCapturedOutput(specSummary.CapturedOutput) - - switch specSummary.State { - case types.SpecStatePassed: - if specSummary.IsMeasurement { - aggregator.stenographer.AnnounceSuccessfulMeasurement(specSummary, aggregator.config.Succinct) - } else if specSummary.RunTime.Seconds() >= aggregator.config.SlowSpecThreshold { - aggregator.stenographer.AnnounceSuccessfulSlowSpec(specSummary, aggregator.config.Succinct) - } else { - aggregator.stenographer.AnnounceSuccessfulSpec(specSummary) - } - - case types.SpecStatePending: - aggregator.stenographer.AnnouncePendingSpec(specSummary, aggregator.config.NoisyPendings && !aggregator.config.Succinct) - case types.SpecStateSkipped: - aggregator.stenographer.AnnounceSkippedSpec(specSummary, aggregator.config.Succinct || !aggregator.config.NoisySkippings, aggregator.config.FullTrace) - case types.SpecStateTimedOut: - aggregator.stenographer.AnnounceSpecTimedOut(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace) - case types.SpecStatePanicked: - aggregator.stenographer.AnnounceSpecPanicked(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace) - case types.SpecStateFailed: - aggregator.stenographer.AnnounceSpecFailed(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace) - } -} - -func (aggregator *Aggregator) registerSuiteEnding(suite *types.SuiteSummary) (finished bool, passed bool) { - aggregator.aggregatedSuiteEndings = append(aggregator.aggregatedSuiteEndings, suite) - if len(aggregator.aggregatedSuiteEndings) < aggregator.nodeCount { - return false, false - } - - aggregatedSuiteSummary := &types.SuiteSummary{} - aggregatedSuiteSummary.SuiteSucceeded = true - - for _, suiteSummary := range aggregator.aggregatedSuiteEndings { - if !suiteSummary.SuiteSucceeded { - aggregatedSuiteSummary.SuiteSucceeded = false - } - - aggregatedSuiteSummary.NumberOfSpecsThatWillBeRun += suiteSummary.NumberOfSpecsThatWillBeRun - aggregatedSuiteSummary.NumberOfTotalSpecs += suiteSummary.NumberOfTotalSpecs - aggregatedSuiteSummary.NumberOfPassedSpecs += suiteSummary.NumberOfPassedSpecs - aggregatedSuiteSummary.NumberOfFailedSpecs += suiteSummary.NumberOfFailedSpecs - aggregatedSuiteSummary.NumberOfPendingSpecs += suiteSummary.NumberOfPendingSpecs - aggregatedSuiteSummary.NumberOfSkippedSpecs += suiteSummary.NumberOfSkippedSpecs - aggregatedSuiteSummary.NumberOfFlakedSpecs += suiteSummary.NumberOfFlakedSpecs - } - - aggregatedSuiteSummary.RunTime = time.Since(aggregator.startTime) - - aggregator.stenographer.SummarizeFailures(aggregator.specs) - aggregator.stenographer.AnnounceSpecRunCompletion(aggregatedSuiteSummary, aggregator.config.Succinct) - - return true, aggregatedSuiteSummary.SuiteSucceeded -} diff --git a/internal/remote/aggregator_test.go b/internal/remote/aggregator_test.go deleted file mode 100644 index b37f18d8dd..0000000000 --- a/internal/remote/aggregator_test.go +++ /dev/null @@ -1,313 +0,0 @@ -package remote_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "time" - - "github.com/onsi/ginkgo/config" - . "github.com/onsi/ginkgo/internal/remote" - st "github.com/onsi/ginkgo/reporters/stenographer" - "github.com/onsi/ginkgo/types" -) - -var _ = Describe("Aggregator", func() { - var ( - aggregator *Aggregator - reporterConfig config.DefaultReporterConfigType - stenographer *st.FakeStenographer - result chan bool - - ginkgoConfig1 config.GinkgoConfigType - ginkgoConfig2 config.GinkgoConfigType - - suiteSummary1 *types.SuiteSummary - suiteSummary2 *types.SuiteSummary - - beforeSummary *types.SetupSummary - afterSummary *types.SetupSummary - specSummary *types.SpecSummary - - suiteDescription string - ) - - BeforeEach(func() { - reporterConfig = config.DefaultReporterConfigType{ - NoColor: false, - SlowSpecThreshold: 0.1, - NoisyPendings: true, - Succinct: false, - Verbose: true, - } - stenographer = st.NewFakeStenographer() - result = make(chan bool, 1) - aggregator = NewAggregator(2, result, reporterConfig, stenographer) - - // - // now set up some fixture data - // - - ginkgoConfig1 = config.GinkgoConfigType{ - RandomSeed: 1138, - RandomizeAllSpecs: true, - ParallelNode: 1, - ParallelTotal: 2, - } - - ginkgoConfig2 = config.GinkgoConfigType{ - RandomSeed: 1138, - RandomizeAllSpecs: true, - ParallelNode: 2, - ParallelTotal: 2, - } - - suiteDescription = "My Parallel Suite" - - suiteSummary1 = &types.SuiteSummary{ - SuiteDescription: suiteDescription, - - NumberOfSpecsBeforeParallelization: 30, - NumberOfTotalSpecs: 17, - NumberOfSpecsThatWillBeRun: 15, - NumberOfPendingSpecs: 1, - NumberOfSkippedSpecs: 1, - } - - suiteSummary2 = &types.SuiteSummary{ - SuiteDescription: suiteDescription, - - NumberOfSpecsBeforeParallelization: 30, - NumberOfTotalSpecs: 13, - NumberOfSpecsThatWillBeRun: 8, - NumberOfPendingSpecs: 2, - NumberOfSkippedSpecs: 3, - } - - beforeSummary = &types.SetupSummary{ - State: types.SpecStatePassed, - CapturedOutput: "BeforeSuiteOutput", - } - - afterSummary = &types.SetupSummary{ - State: types.SpecStatePassed, - CapturedOutput: "AfterSuiteOutput", - } - - specSummary = &types.SpecSummary{ - State: types.SpecStatePassed, - CapturedOutput: "SpecOutput", - } - }) - - call := st.NewFakeStenographerCall - - beginSuite := func() { - stenographer.Reset() - aggregator.SpecSuiteWillBegin(ginkgoConfig2, suiteSummary2) - aggregator.SpecSuiteWillBegin(ginkgoConfig1, suiteSummary1) - Eventually(func() interface{} { - return len(stenographer.Calls()) - }).Should(BeNumerically(">=", 3)) - } - - Describe("Announcing the beginning of the suite", func() { - Context("When one of the parallel-suites starts", func() { - BeforeEach(func() { - aggregator.SpecSuiteWillBegin(ginkgoConfig2, suiteSummary2) - }) - - It("should be silent", func() { - Consistently(func() interface{} { return stenographer.Calls() }).Should(BeEmpty()) - }) - }) - - Context("once all of the parallel-suites have started", func() { - BeforeEach(func() { - aggregator.SpecSuiteWillBegin(ginkgoConfig2, suiteSummary2) - aggregator.SpecSuiteWillBegin(ginkgoConfig1, suiteSummary1) - Eventually(func() interface{} { - return stenographer.Calls() - }).Should(HaveLen(3)) - }) - - It("should announce the beginning of the suite", func() { - Ω(stenographer.Calls()).Should(HaveLen(3)) - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuite", suiteDescription, ginkgoConfig1.RandomSeed, true, false))) - Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceTotalNumberOfSpecs", 30, false))) - Ω(stenographer.Calls()[2]).Should(Equal(call("AnnounceAggregatedParallelRun", 2, false))) - }) - }) - }) - - Describe("Announcing specs and before suites", func() { - Context("when the parallel-suites have not all started", func() { - BeforeEach(func() { - aggregator.BeforeSuiteDidRun(beforeSummary) - aggregator.AfterSuiteDidRun(afterSummary) - aggregator.SpecDidComplete(specSummary) - }) - - It("should not announce any specs", func() { - Consistently(func() interface{} { return stenographer.Calls() }).Should(BeEmpty()) - }) - - Context("when the parallel-suites subsequently start", func() { - BeforeEach(func() { - beginSuite() - }) - - It("should announce the specs, the before suites and the after suites", func() { - Eventually(func() interface{} { - return stenographer.Calls() - }).Should(ContainElement(call("AnnounceSuccessfulSpec", specSummary))) - - Ω(stenographer.Calls()).Should(ContainElement(call("AnnounceCapturedOutput", beforeSummary.CapturedOutput))) - Ω(stenographer.Calls()).Should(ContainElement(call("AnnounceCapturedOutput", afterSummary.CapturedOutput))) - }) - }) - }) - - Context("When the parallel-suites have all started", func() { - BeforeEach(func() { - beginSuite() - stenographer.Reset() - }) - - Context("When a spec completes", func() { - BeforeEach(func() { - aggregator.BeforeSuiteDidRun(beforeSummary) - aggregator.SpecDidComplete(specSummary) - aggregator.AfterSuiteDidRun(afterSummary) - Eventually(func() interface{} { - return stenographer.Calls() - }).Should(HaveLen(5)) - }) - - It("should announce the captured output of the BeforeSuite", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceCapturedOutput", beforeSummary.CapturedOutput))) - }) - - It("should announce that the spec will run (when in verbose mode)", func() { - Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceSpecWillRun", specSummary))) - }) - - It("should announce the captured stdout of the spec", func() { - Ω(stenographer.Calls()[2]).Should(Equal(call("AnnounceCapturedOutput", specSummary.CapturedOutput))) - }) - - It("should announce completion", func() { - Ω(stenographer.Calls()[3]).Should(Equal(call("AnnounceSuccessfulSpec", specSummary))) - }) - - It("should announce the captured output of the AfterSuite", func() { - Ω(stenographer.Calls()[4]).Should(Equal(call("AnnounceCapturedOutput", afterSummary.CapturedOutput))) - }) - }) - }) - }) - - Describe("Announcing the end of the suite", func() { - BeforeEach(func() { - beginSuite() - stenographer.Reset() - }) - - Context("When one of the parallel-suites ends", func() { - BeforeEach(func() { - aggregator.SpecSuiteDidEnd(suiteSummary2) - }) - - It("should be silent", func() { - Consistently(func() interface{} { return stenographer.Calls() }).Should(BeEmpty()) - }) - - It("should not notify the channel", func() { - Ω(result).Should(BeEmpty()) - }) - }) - - Context("once all of the parallel-suites end", func() { - BeforeEach(func() { - time.Sleep(200 * time.Millisecond) - - suiteSummary1.SuiteSucceeded = true - suiteSummary1.NumberOfPassedSpecs = 15 - suiteSummary1.NumberOfFailedSpecs = 0 - suiteSummary1.NumberOfFlakedSpecs = 3 - suiteSummary2.SuiteSucceeded = false - suiteSummary2.NumberOfPassedSpecs = 5 - suiteSummary2.NumberOfFailedSpecs = 3 - suiteSummary2.NumberOfFlakedSpecs = 4 - - aggregator.SpecSuiteDidEnd(suiteSummary2) - aggregator.SpecSuiteDidEnd(suiteSummary1) - Eventually(func() interface{} { - return stenographer.Calls() - }).Should(HaveLen(2)) - }) - - It("should announce the end of the suite", func() { - compositeSummary := stenographer.Calls()[1].Args[0].(*types.SuiteSummary) - - Ω(compositeSummary.SuiteSucceeded).Should(BeFalse()) - Ω(compositeSummary.NumberOfSpecsThatWillBeRun).Should(Equal(23)) - Ω(compositeSummary.NumberOfTotalSpecs).Should(Equal(30)) - Ω(compositeSummary.NumberOfPassedSpecs).Should(Equal(20)) - Ω(compositeSummary.NumberOfFailedSpecs).Should(Equal(3)) - Ω(compositeSummary.NumberOfPendingSpecs).Should(Equal(3)) - Ω(compositeSummary.NumberOfSkippedSpecs).Should(Equal(4)) - Ω(compositeSummary.NumberOfFlakedSpecs).Should(Equal(7)) - Ω(compositeSummary.RunTime.Seconds()).Should(BeNumerically(">", 0.2)) - }) - }) - - Context("when all the parallel-suites pass", func() { - BeforeEach(func() { - suiteSummary1.SuiteSucceeded = true - suiteSummary2.SuiteSucceeded = true - - aggregator.SpecSuiteDidEnd(suiteSummary2) - aggregator.SpecSuiteDidEnd(suiteSummary1) - Eventually(func() interface{} { - return stenographer.Calls() - }).Should(HaveLen(2)) - }) - - It("should report success", func() { - compositeSummary := stenographer.Calls()[1].Args[0].(*types.SuiteSummary) - - Ω(compositeSummary.SuiteSucceeded).Should(BeTrue()) - }) - - It("should notify the channel that it succeded", func(done Done) { - Ω(<-result).Should(BeTrue()) - close(done) - }) - }) - - Context("when one of the parallel-suites fails", func() { - BeforeEach(func() { - suiteSummary1.SuiteSucceeded = true - suiteSummary2.SuiteSucceeded = false - - aggregator.SpecSuiteDidEnd(suiteSummary2) - aggregator.SpecSuiteDidEnd(suiteSummary1) - Eventually(func() interface{} { - return stenographer.Calls() - }).Should(HaveLen(2)) - }) - - It("should report failure", func() { - compositeSummary := stenographer.Calls()[1].Args[0].(*types.SuiteSummary) - - Ω(compositeSummary.SuiteSucceeded).Should(BeFalse()) - }) - - It("should notify the channel that it failed", func(done Done) { - Ω(<-result).Should(BeFalse()) - close(done) - }) - }) - }) -}) diff --git a/internal/remote/fake_output_interceptor_test.go b/internal/remote/fake_output_interceptor_test.go deleted file mode 100644 index ef54862ead..0000000000 --- a/internal/remote/fake_output_interceptor_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package remote_test - -import "os" - -type fakeOutputInterceptor struct { - DidStartInterceptingOutput bool - DidStopInterceptingOutput bool - InterceptedOutput string -} - -func (interceptor *fakeOutputInterceptor) StartInterceptingOutput() error { - interceptor.DidStartInterceptingOutput = true - return nil -} - -func (interceptor *fakeOutputInterceptor) StopInterceptingAndReturnOutput() (string, error) { - interceptor.DidStopInterceptingOutput = true - return interceptor.InterceptedOutput, nil -} - -func (interceptor *fakeOutputInterceptor) StreamTo(*os.File) { -} diff --git a/internal/remote/fake_poster_test.go b/internal/remote/fake_poster_test.go deleted file mode 100644 index 3543c59c64..0000000000 --- a/internal/remote/fake_poster_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package remote_test - -import ( - "io" - "io/ioutil" - "net/http" -) - -type post struct { - url string - bodyType string - bodyContent []byte -} - -type fakePoster struct { - posts []post -} - -func newFakePoster() *fakePoster { - return &fakePoster{ - posts: make([]post, 0), - } -} - -func (poster *fakePoster) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { - bodyContent, _ := ioutil.ReadAll(body) - poster.posts = append(poster.posts, post{ - url: url, - bodyType: bodyType, - bodyContent: bodyContent, - }) - return nil, nil -} diff --git a/internal/remote/forwarding_reporter.go b/internal/remote/forwarding_reporter.go deleted file mode 100644 index 284bc62e5e..0000000000 --- a/internal/remote/forwarding_reporter.go +++ /dev/null @@ -1,147 +0,0 @@ -package remote - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - - "github.com/onsi/ginkgo/internal/writer" - "github.com/onsi/ginkgo/reporters" - "github.com/onsi/ginkgo/reporters/stenographer" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/types" -) - -//An interface to net/http's client to allow the injection of fakes under test -type Poster interface { - Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) -} - -/* -The ForwardingReporter is a Ginkgo reporter that forwards information to -a Ginkgo remote server. - -When streaming parallel test output, this repoter is automatically installed by Ginkgo. - -This is accomplished by passing in the GINKGO_REMOTE_REPORTING_SERVER environment variable to `go test`, the Ginkgo test runner -detects this environment variable (which should contain the host of the server) and automatically installs a ForwardingReporter -in place of Ginkgo's DefaultReporter. -*/ - -type ForwardingReporter struct { - serverHost string - poster Poster - outputInterceptor OutputInterceptor - debugMode bool - debugFile *os.File - nestedReporter *reporters.DefaultReporter -} - -func NewForwardingReporter(config config.DefaultReporterConfigType, serverHost string, poster Poster, outputInterceptor OutputInterceptor, ginkgoWriter *writer.Writer, debugFile string) *ForwardingReporter { - reporter := &ForwardingReporter{ - serverHost: serverHost, - poster: poster, - outputInterceptor: outputInterceptor, - } - - if debugFile != "" { - var err error - reporter.debugMode = true - reporter.debugFile, err = os.Create(debugFile) - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - - if !config.Verbose { - //if verbose is true then the GinkgoWriter emits to stdout. Don't _also_ redirect GinkgoWriter output as that will result in duplication. - ginkgoWriter.AndRedirectTo(reporter.debugFile) - } - outputInterceptor.StreamTo(reporter.debugFile) //This is not working - - stenographer := stenographer.New(false, true, reporter.debugFile) - config.Succinct = false - config.Verbose = true - config.FullTrace = true - reporter.nestedReporter = reporters.NewDefaultReporter(config, stenographer) - } - - return reporter -} - -func (reporter *ForwardingReporter) post(path string, data interface{}) { - encoded, _ := json.Marshal(data) - buffer := bytes.NewBuffer(encoded) - reporter.poster.Post(reporter.serverHost+path, "application/json", buffer) -} - -func (reporter *ForwardingReporter) SpecSuiteWillBegin(conf config.GinkgoConfigType, summary *types.SuiteSummary) { - data := struct { - Config config.GinkgoConfigType `json:"config"` - Summary *types.SuiteSummary `json:"suite-summary"` - }{ - conf, - summary, - } - - reporter.outputInterceptor.StartInterceptingOutput() - if reporter.debugMode { - reporter.nestedReporter.SpecSuiteWillBegin(conf, summary) - reporter.debugFile.Sync() - } - reporter.post("/SpecSuiteWillBegin", data) -} - -func (reporter *ForwardingReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { - output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() - reporter.outputInterceptor.StartInterceptingOutput() - setupSummary.CapturedOutput = output - if reporter.debugMode { - reporter.nestedReporter.BeforeSuiteDidRun(setupSummary) - reporter.debugFile.Sync() - } - reporter.post("/BeforeSuiteDidRun", setupSummary) -} - -func (reporter *ForwardingReporter) SpecWillRun(specSummary *types.SpecSummary) { - if reporter.debugMode { - reporter.nestedReporter.SpecWillRun(specSummary) - reporter.debugFile.Sync() - } - reporter.post("/SpecWillRun", specSummary) -} - -func (reporter *ForwardingReporter) SpecDidComplete(specSummary *types.SpecSummary) { - output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() - reporter.outputInterceptor.StartInterceptingOutput() - specSummary.CapturedOutput = output - if reporter.debugMode { - reporter.nestedReporter.SpecDidComplete(specSummary) - reporter.debugFile.Sync() - } - reporter.post("/SpecDidComplete", specSummary) -} - -func (reporter *ForwardingReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { - output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() - reporter.outputInterceptor.StartInterceptingOutput() - setupSummary.CapturedOutput = output - if reporter.debugMode { - reporter.nestedReporter.AfterSuiteDidRun(setupSummary) - reporter.debugFile.Sync() - } - reporter.post("/AfterSuiteDidRun", setupSummary) -} - -func (reporter *ForwardingReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { - reporter.outputInterceptor.StopInterceptingAndReturnOutput() - if reporter.debugMode { - reporter.nestedReporter.SpecSuiteDidEnd(summary) - reporter.debugFile.Sync() - } - reporter.post("/SpecSuiteDidEnd", summary) -} diff --git a/internal/remote/forwarding_reporter_test.go b/internal/remote/forwarding_reporter_test.go deleted file mode 100644 index 0d7e4769c2..0000000000 --- a/internal/remote/forwarding_reporter_test.go +++ /dev/null @@ -1,181 +0,0 @@ -package remote_test - -import ( - "encoding/json" - - . "github.com/onsi/ginkgo" - "github.com/onsi/ginkgo/config" - . "github.com/onsi/ginkgo/internal/remote" - "github.com/onsi/ginkgo/types" - . "github.com/onsi/gomega" -) - -var _ = Describe("ForwardingReporter", func() { - var ( - reporter *ForwardingReporter - interceptor *fakeOutputInterceptor - poster *fakePoster - suiteSummary *types.SuiteSummary - specSummary *types.SpecSummary - setupSummary *types.SetupSummary - serverHost string - ) - - BeforeEach(func() { - serverHost = "http://127.0.0.1:7788" - - poster = newFakePoster() - - interceptor = &fakeOutputInterceptor{ - InterceptedOutput: "The intercepted output!", - } - - reporter = NewForwardingReporter(config.DefaultReporterConfigType{}, serverHost, poster, interceptor, nil, "") - - suiteSummary = &types.SuiteSummary{ - SuiteDescription: "My Test Suite", - } - - setupSummary = &types.SetupSummary{ - State: types.SpecStatePassed, - } - - specSummary = &types.SpecSummary{ - ComponentTexts: []string{"My", "Spec"}, - State: types.SpecStatePassed, - } - }) - - Context("When a suite begins", func() { - BeforeEach(func() { - reporter.SpecSuiteWillBegin(config.GinkgoConfig, suiteSummary) - }) - - It("should start intercepting output", func() { - Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) - }) - - It("should POST the SuiteSummary and Ginkgo Config to the Ginkgo server", func() { - Ω(poster.posts).Should(HaveLen(1)) - Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/SpecSuiteWillBegin")) - Ω(poster.posts[0].bodyType).Should(Equal("application/json")) - - var sentData struct { - SentConfig config.GinkgoConfigType `json:"config"` - SentSuiteSummary *types.SuiteSummary `json:"suite-summary"` - } - - err := json.Unmarshal(poster.posts[0].bodyContent, &sentData) - Ω(err).ShouldNot(HaveOccurred()) - - Ω(sentData.SentConfig).Should(Equal(config.GinkgoConfig)) - Ω(sentData.SentSuiteSummary).Should(Equal(suiteSummary)) - }) - }) - - Context("when a BeforeSuite completes", func() { - BeforeEach(func() { - reporter.BeforeSuiteDidRun(setupSummary) - }) - - It("should stop, then start intercepting output", func() { - Ω(interceptor.DidStopInterceptingOutput).Should(BeTrue()) - Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) - }) - - It("should POST the SetupSummary to the Ginkgo server", func() { - Ω(poster.posts).Should(HaveLen(1)) - Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/BeforeSuiteDidRun")) - Ω(poster.posts[0].bodyType).Should(Equal("application/json")) - - var summary *types.SetupSummary - err := json.Unmarshal(poster.posts[0].bodyContent, &summary) - Ω(err).ShouldNot(HaveOccurred()) - setupSummary.CapturedOutput = interceptor.InterceptedOutput - Ω(summary).Should(Equal(setupSummary)) - }) - }) - - Context("when an AfterSuite completes", func() { - BeforeEach(func() { - reporter.AfterSuiteDidRun(setupSummary) - }) - - It("should stop, then start intercepting output", func() { - Ω(interceptor.DidStopInterceptingOutput).Should(BeTrue()) - Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) - }) - - It("should POST the SetupSummary to the Ginkgo server", func() { - Ω(poster.posts).Should(HaveLen(1)) - Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/AfterSuiteDidRun")) - Ω(poster.posts[0].bodyType).Should(Equal("application/json")) - - var summary *types.SetupSummary - err := json.Unmarshal(poster.posts[0].bodyContent, &summary) - Ω(err).ShouldNot(HaveOccurred()) - setupSummary.CapturedOutput = interceptor.InterceptedOutput - Ω(summary).Should(Equal(setupSummary)) - }) - }) - - Context("When a spec will run", func() { - BeforeEach(func() { - reporter.SpecWillRun(specSummary) - }) - - It("should POST the SpecSummary to the Ginkgo server", func() { - Ω(poster.posts).Should(HaveLen(1)) - Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/SpecWillRun")) - Ω(poster.posts[0].bodyType).Should(Equal("application/json")) - - var summary *types.SpecSummary - err := json.Unmarshal(poster.posts[0].bodyContent, &summary) - Ω(err).ShouldNot(HaveOccurred()) - Ω(summary).Should(Equal(specSummary)) - }) - - Context("When a spec completes", func() { - BeforeEach(func() { - specSummary.State = types.SpecStatePanicked - reporter.SpecDidComplete(specSummary) - }) - - It("should POST the SpecSummary to the Ginkgo server and include any intercepted output", func() { - Ω(poster.posts).Should(HaveLen(2)) - Ω(poster.posts[1].url).Should(Equal("http://127.0.0.1:7788/SpecDidComplete")) - Ω(poster.posts[1].bodyType).Should(Equal("application/json")) - - var summary *types.SpecSummary - err := json.Unmarshal(poster.posts[1].bodyContent, &summary) - Ω(err).ShouldNot(HaveOccurred()) - specSummary.CapturedOutput = interceptor.InterceptedOutput - Ω(summary).Should(Equal(specSummary)) - }) - - It("should stop, then start intercepting output", func() { - Ω(interceptor.DidStopInterceptingOutput).Should(BeTrue()) - Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) - }) - }) - }) - - Context("When a suite ends", func() { - BeforeEach(func() { - reporter.SpecSuiteDidEnd(suiteSummary) - }) - - It("should POST the SuiteSummary to the Ginkgo server", func() { - Ω(poster.posts).Should(HaveLen(1)) - Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/SpecSuiteDidEnd")) - Ω(poster.posts[0].bodyType).Should(Equal("application/json")) - - var summary *types.SuiteSummary - - err := json.Unmarshal(poster.posts[0].bodyContent, &summary) - Ω(err).ShouldNot(HaveOccurred()) - - Ω(summary).Should(Equal(suiteSummary)) - }) - }) -}) diff --git a/internal/remote/output_interceptor.go b/internal/remote/output_interceptor.go deleted file mode 100644 index 5154abe87d..0000000000 --- a/internal/remote/output_interceptor.go +++ /dev/null @@ -1,13 +0,0 @@ -package remote - -import "os" - -/* -The OutputInterceptor is used by the ForwardingReporter to -intercept and capture all stdin and stderr output during a test run. -*/ -type OutputInterceptor interface { - StartInterceptingOutput() error - StopInterceptingAndReturnOutput() (string, error) - StreamTo(*os.File) -} diff --git a/internal/remote/remote_suite_test.go b/internal/remote/remote_suite_test.go deleted file mode 100644 index e6b4e9f32c..0000000000 --- a/internal/remote/remote_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package remote_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestRemote(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Remote Spec Forwarding Suite") -} diff --git a/internal/shuffle.go b/internal/shuffle.go new file mode 100644 index 0000000000..2e189b8ea8 --- /dev/null +++ b/internal/shuffle.go @@ -0,0 +1,49 @@ +package internal + +import ( + "math/rand" + "sort" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +func ShuffleSpecs(specs Specs, config config.GinkgoConfigType) Specs { + /* + Ginkgo has sophisticated suport for randomizing specs. Specs are guaranteed to have the same + order for a given seed across test runs. + + By default only top-level containers and specs are shuffled - this makes for a more intuitive debugging + experience - specs within a given container run in the order they appear in the file. + + Developers can set -randomizeAllSpecs to shuffle _all_ specs. + */ + r := rand.New(rand.NewSource(config.RandomSeed)) + + // We shuffle by partitioning specs by the id of the first node of a given type, then shuffling that partition + // When -randomizeAllSpecs is set we partition the specs by the id of the `It` - i.e. each spec has a unique id and this is equivalent to sorting all specs + // Otherwise, we partition by the id of the first container (or It if there is no container). This preserves the spec grouping by top-level container. + nodeTypesToShuffle := types.NodeTypesForContainerAndIt + if config.RandomizeAllSpecs { + nodeTypesToShuffle = []types.NodeType{types.NodeTypeIt} + } + + partition := specs.PartitionByFirstNodeWithType(nodeTypesToShuffle...) + + // To ensure a consistent shuffle, the partition slice must have the same order across spec runs. + // To do this we stable sort by codelocation of the node of type that was used to form the partition + sort.SliceStable(partition, func(i, j int) bool { + a := partition[i][0].FirstNodeWithType(nodeTypesToShuffle...) + b := partition[j][0].FirstNodeWithType(nodeTypesToShuffle...) + return a.CodeLocation.String() < b.CodeLocation.String() + }) + + //Shuffle the partition + shuffledSpecs := Specs{} + permutation := r.Perm(len(partition)) + for _, j := range permutation { + shuffledSpecs = append(shuffledSpecs, partition[j]...) + } + + return shuffledSpecs +} diff --git a/internal/shuffle_test.go b/internal/shuffle_test.go new file mode 100644 index 0000000000..da5df4d279 --- /dev/null +++ b/internal/shuffle_test.go @@ -0,0 +1,146 @@ +package internal_test + +import ( + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal" +) + +type SpecTexts []string + +func getTexts(specs Specs) SpecTexts { + out := []string{} + for _, spec := range specs { + out = append(out, spec.Text()) + } + return out +} + +func (tt SpecTexts) Join() string { + return strings.Join(tt, "") +} + +var _ = Describe("Shuffle", func() { + var conf config.GinkgoConfigType + var specs Specs + + BeforeEach(func() { + conf = config.GinkgoConfigType{} + conf.RandomSeed = 1 + + con1 := N(ntCon) + con2 := N(ntCon) + specs = Specs{ + S(N("A", ntIt)), + S(N("B", ntIt)), + S(con1, N("C", ntIt)), + S(con1, N("D", ntIt)), + S(con1, N(ntCon), N("E", ntIt)), + S(N("F", ntIt)), + S(con2, N("G", ntIt)), + S(con2, N("H", ntIt)), + } + }) + + Context("when configured to only randomize top-level specs", func() { + It("shuffles top level specs only", func() { + for conf.RandomSeed = 1; conf.RandomSeed < 10; conf.RandomSeed += 1 { + shuffledSpecs := internal.ShuffleSpecs(specs, conf) + Ω(getTexts(shuffledSpecs).Join()).Should(ContainSubstring("CDE")) + Ω(getTexts(shuffledSpecs).Join()).Should(ContainSubstring("GH")) + } + + conf.RandomSeed = 1 + specsSeed1 := internal.ShuffleSpecs(specs, conf) + conf.RandomSeed = 2 + specsSeed2 := internal.ShuffleSpecs(specs, conf) + Ω(getTexts(specsSeed1)).ShouldNot(Equal(getTexts(specsSeed2))) + }) + }) + + Context("when configured to randomize all specs", func() { + BeforeEach(func() { + conf.RandomizeAllSpecs = true + }) + + It("shuffles all specs", func() { + hasCDE := true + hasGH := true + for conf.RandomSeed = 1; conf.RandomSeed < 10; conf.RandomSeed += 1 { + shuffledSpecs := internal.ShuffleSpecs(specs, conf) + hasCDE, _ = ContainSubstring("CDE").Match(getTexts(shuffledSpecs).Join()) + hasGH, _ = ContainSubstring("GH").Match(getTexts(shuffledSpecs).Join()) + if !hasCDE && !hasGH { + break + } + } + + Ω(hasCDE || hasGH).Should(BeFalse(), "after 10 randomizations, we really shouldn't have gotten CDE and GH in order as all specs should be shuffled, not just top-level containers and specs") + + conf.RandomSeed = 1 + specsSeed1 := internal.ShuffleSpecs(specs, conf) + conf.RandomSeed = 2 + specsSeed2 := internal.ShuffleSpecs(specs, conf) + Ω(getTexts(specsSeed1)).ShouldNot(Equal(getTexts(specsSeed2))) + }) + }) + + Context("when passed the same seed", func() { + It("always generates the same order", func() { + for _, conf.RandomizeAllSpecs = range []bool{true, false} { + for conf.RandomSeed = 1; conf.RandomSeed < 10; conf.RandomSeed += 1 { + shuffledSpecs := internal.ShuffleSpecs(specs, conf) + for i := 0; i < 10; i++ { + reshuffledSpecs := internal.ShuffleSpecs(specs, conf) + Ω(shuffledSpecs).Should(Equal(reshuffledSpecs)) + } + } + } + }) + }) + + Context("when specs are in different files and the files are loaded in an undefined order", func() { + var specsInFileA, specsInFileB Specs + BeforeEach(func() { + con1 := N(ntCon, CL("file_A", 10)) + specsInFileA = Specs{ + S(N("A", ntIt, CL("file_A", 1))), + S(N("B", ntIt, CL("file_A", 5))), + S(con1, N("C", ntIt, CL("file_A", 15))), + S(con1, N("D", ntIt, CL("file_A", 20))), + S(con1, N(ntCon, CL("file_A", 25)), N("E", ntIt, CL("file_A", 30))), + } + + con2 := N(ntCon, CL("file_B", 10)) + specsInFileB = Specs{ + S(N("F", ntIt, CL("file_B", 1))), + S(con2, N("G", ntIt, CL("file_B", 15))), + S(con2, N("H", ntIt, CL("file_B", 20))), + } + + }) + + It("always generates a consistent randomization when given the same seed", func() { + for _, conf.RandomizeAllSpecs = range []bool{true, false} { + for conf.RandomSeed = 1; conf.RandomSeed < 10; conf.RandomSeed += 1 { + specsOrderAB := Specs{} + specsOrderAB = append(specsOrderAB, specsInFileA...) + specsOrderAB = append(specsOrderAB, specsInFileB...) + + specsOrderBA := Specs{} + specsOrderBA = append(specsOrderBA, specsInFileB...) + specsOrderBA = append(specsOrderBA, specsInFileA...) + + shuffledSpecsAB := internal.ShuffleSpecs(specsOrderAB, conf) + shuffledSpecsBA := internal.ShuffleSpecs(specsOrderBA, conf) + + Ω(getTexts(shuffledSpecsAB)).Should(Equal(getTexts(shuffledSpecsBA))) + } + } + }) + }) +}) diff --git a/internal/spec.go b/internal/spec.go new file mode 100644 index 0000000000..23ba2bb7f6 --- /dev/null +++ b/internal/spec.go @@ -0,0 +1,68 @@ +package internal + +import ( + "strings" + + "github.com/onsi/ginkgo/types" +) + +type Spec struct { + Nodes Nodes + Skip bool +} + +func (t Spec) Text() string { + texts := []string{} + for _, node := range t.Nodes { + if node.Text != "" { + texts = append(texts, node.Text) + } + } + return strings.Join(texts, " ") +} + +func (t Spec) FirstNodeWithType(nodeTypes ...types.NodeType) Node { + return t.Nodes.FirstNodeWithType(nodeTypes...) +} + +type Specs []Spec + +func (t Specs) HasAnySpecsMarkedPending() bool { + for _, test := range t { + if test.Nodes.HasNodeMarkedPending() { + return true + } + } + + return false +} + +func (t Specs) CountWithoutSkip() int { + n := 0 + for _, test := range t { + if !test.Skip { + n += 1 + } + } + return n +} + +func (t Specs) PartitionByFirstNodeWithType(nodeTypes ...types.NodeType) []Specs { + indexById := map[uint]int{} + partition := []Specs{} + for _, test := range t { + id := test.FirstNodeWithType(nodeTypes...).ID + if id == 0 { + continue + } + idx, found := indexById[id] + if !found { + partition = append(partition, Specs{}) + idx = len(partition) - 1 + indexById[id] = idx + } + partition[idx] = append(partition[idx], test) + } + + return partition +} diff --git a/internal/spec/spec.go b/internal/spec/spec.go deleted file mode 100644 index 6eef40a0e0..0000000000 --- a/internal/spec/spec.go +++ /dev/null @@ -1,247 +0,0 @@ -package spec - -import ( - "fmt" - "io" - "time" - - "sync" - - "github.com/onsi/ginkgo/internal/containernode" - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/types" -) - -type Spec struct { - subject leafnodes.SubjectNode - focused bool - announceProgress bool - - containers []*containernode.ContainerNode - - state types.SpecState - runTime time.Duration - startTime time.Time - failure types.SpecFailure - previousFailures bool - - stateMutex *sync.Mutex -} - -func New(subject leafnodes.SubjectNode, containers []*containernode.ContainerNode, announceProgress bool) *Spec { - spec := &Spec{ - subject: subject, - containers: containers, - focused: subject.Flag() == types.FlagTypeFocused, - announceProgress: announceProgress, - stateMutex: &sync.Mutex{}, - } - - spec.processFlag(subject.Flag()) - for i := len(containers) - 1; i >= 0; i-- { - spec.processFlag(containers[i].Flag()) - } - - return spec -} - -func (spec *Spec) processFlag(flag types.FlagType) { - if flag == types.FlagTypeFocused { - spec.focused = true - } else if flag == types.FlagTypePending { - spec.setState(types.SpecStatePending) - } -} - -func (spec *Spec) Skip() { - spec.setState(types.SpecStateSkipped) -} - -func (spec *Spec) Failed() bool { - return spec.getState() == types.SpecStateFailed || spec.getState() == types.SpecStatePanicked || spec.getState() == types.SpecStateTimedOut -} - -func (spec *Spec) Passed() bool { - return spec.getState() == types.SpecStatePassed -} - -func (spec *Spec) Flaked() bool { - return spec.getState() == types.SpecStatePassed && spec.previousFailures -} - -func (spec *Spec) Pending() bool { - return spec.getState() == types.SpecStatePending -} - -func (spec *Spec) Skipped() bool { - return spec.getState() == types.SpecStateSkipped -} - -func (spec *Spec) Focused() bool { - return spec.focused -} - -func (spec *Spec) IsMeasurement() bool { - return spec.subject.Type() == types.SpecComponentTypeMeasure -} - -func (spec *Spec) Summary(suiteID string) *types.SpecSummary { - componentTexts := make([]string, len(spec.containers)+1) - componentCodeLocations := make([]types.CodeLocation, len(spec.containers)+1) - - for i, container := range spec.containers { - componentTexts[i] = container.Text() - componentCodeLocations[i] = container.CodeLocation() - } - - componentTexts[len(spec.containers)] = spec.subject.Text() - componentCodeLocations[len(spec.containers)] = spec.subject.CodeLocation() - - runTime := spec.runTime - if runTime == 0 && !spec.startTime.IsZero() { - runTime = time.Since(spec.startTime) - } - - return &types.SpecSummary{ - IsMeasurement: spec.IsMeasurement(), - NumberOfSamples: spec.subject.Samples(), - ComponentTexts: componentTexts, - ComponentCodeLocations: componentCodeLocations, - State: spec.getState(), - RunTime: runTime, - Failure: spec.failure, - Measurements: spec.measurementsReport(), - SuiteID: suiteID, - } -} - -func (spec *Spec) ConcatenatedString() string { - s := "" - for _, container := range spec.containers { - s += container.Text() + " " - } - - return s + spec.subject.Text() -} - -func (spec *Spec) Run(writer io.Writer) { - if spec.getState() == types.SpecStateFailed { - spec.previousFailures = true - } - - spec.startTime = time.Now() - defer func() { - spec.runTime = time.Since(spec.startTime) - }() - - for sample := 0; sample < spec.subject.Samples(); sample++ { - spec.runSample(sample, writer) - - if spec.getState() != types.SpecStatePassed { - return - } - } -} - -func (spec *Spec) getState() types.SpecState { - spec.stateMutex.Lock() - defer spec.stateMutex.Unlock() - return spec.state -} - -func (spec *Spec) setState(state types.SpecState) { - spec.stateMutex.Lock() - defer spec.stateMutex.Unlock() - spec.state = state -} - -func (spec *Spec) runSample(sample int, writer io.Writer) { - spec.setState(types.SpecStatePassed) - spec.failure = types.SpecFailure{} - innerMostContainerIndexToUnwind := -1 - - defer func() { - for i := innerMostContainerIndexToUnwind; i >= 0; i-- { - container := spec.containers[i] - for _, justAfterEach := range container.SetupNodesOfType(types.SpecComponentTypeJustAfterEach) { - spec.announceSetupNode(writer, "JustAfterEach", container, justAfterEach) - justAfterEachState, justAfterEachFailure := justAfterEach.Run() - if justAfterEachState != types.SpecStatePassed && spec.state == types.SpecStatePassed { - spec.state = justAfterEachState - spec.failure = justAfterEachFailure - } - } - } - - for i := innerMostContainerIndexToUnwind; i >= 0; i-- { - container := spec.containers[i] - for _, afterEach := range container.SetupNodesOfType(types.SpecComponentTypeAfterEach) { - spec.announceSetupNode(writer, "AfterEach", container, afterEach) - afterEachState, afterEachFailure := afterEach.Run() - if afterEachState != types.SpecStatePassed && spec.getState() == types.SpecStatePassed { - spec.setState(afterEachState) - spec.failure = afterEachFailure - } - } - } - }() - - for i, container := range spec.containers { - innerMostContainerIndexToUnwind = i - for _, beforeEach := range container.SetupNodesOfType(types.SpecComponentTypeBeforeEach) { - spec.announceSetupNode(writer, "BeforeEach", container, beforeEach) - s, f := beforeEach.Run() - spec.failure = f - spec.setState(s) - if spec.getState() != types.SpecStatePassed { - return - } - } - } - - for _, container := range spec.containers { - for _, justBeforeEach := range container.SetupNodesOfType(types.SpecComponentTypeJustBeforeEach) { - spec.announceSetupNode(writer, "JustBeforeEach", container, justBeforeEach) - s, f := justBeforeEach.Run() - spec.failure = f - spec.setState(s) - if spec.getState() != types.SpecStatePassed { - return - } - } - } - - spec.announceSubject(writer, spec.subject) - s, f := spec.subject.Run() - spec.failure = f - spec.setState(s) -} - -func (spec *Spec) announceSetupNode(writer io.Writer, nodeType string, container *containernode.ContainerNode, setupNode leafnodes.BasicNode) { - if spec.announceProgress { - s := fmt.Sprintf("[%s] %s\n %s\n", nodeType, container.Text(), setupNode.CodeLocation().String()) - writer.Write([]byte(s)) - } -} - -func (spec *Spec) announceSubject(writer io.Writer, subject leafnodes.SubjectNode) { - if spec.announceProgress { - nodeType := "" - switch subject.Type() { - case types.SpecComponentTypeIt: - nodeType = "It" - case types.SpecComponentTypeMeasure: - nodeType = "Measure" - } - s := fmt.Sprintf("[%s] %s\n %s\n", nodeType, subject.Text(), subject.CodeLocation().String()) - writer.Write([]byte(s)) - } -} - -func (spec *Spec) measurementsReport() map[string]*types.SpecMeasurement { - if !spec.IsMeasurement() || spec.Failed() { - return map[string]*types.SpecMeasurement{} - } - - return spec.subject.(*leafnodes.MeasureNode).MeasurementsReport() -} diff --git a/internal/spec/spec_test.go b/internal/spec/spec_test.go deleted file mode 100644 index b4a2c9c797..0000000000 --- a/internal/spec/spec_test.go +++ /dev/null @@ -1,739 +0,0 @@ -package spec_test - -import ( - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/gbytes" - - . "github.com/onsi/ginkgo/internal/spec" - - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/internal/containernode" - Failer "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/types" -) - -var noneFlag = types.FlagTypeNone -var focusedFlag = types.FlagTypeFocused -var pendingFlag = types.FlagTypePending - -var _ = Describe("Spec", func() { - var ( - failer *Failer.Failer - codeLocation types.CodeLocation - nodesThatRan []string - spec *Spec - buffer *gbytes.Buffer - ) - - newBody := func(text string, fail bool) func() { - return func() { - nodesThatRan = append(nodesThatRan, text) - if fail { - failer.Fail(text, codeLocation) - } - } - } - - newIt := func(text string, flag types.FlagType, fail bool) *leafnodes.ItNode { - return leafnodes.NewItNode(text, newBody(text, fail), flag, codeLocation, 0, failer, 0) - } - - newItWithBody := func(text string, body interface{}) *leafnodes.ItNode { - return leafnodes.NewItNode(text, body, noneFlag, codeLocation, 0, failer, 0) - } - - newMeasure := func(text string, flag types.FlagType, fail bool, samples int) *leafnodes.MeasureNode { - return leafnodes.NewMeasureNode(text, func(Benchmarker) { - nodesThatRan = append(nodesThatRan, text) - if fail { - failer.Fail(text, codeLocation) - } - }, flag, codeLocation, samples, failer, 0) - } - - newBef := func(text string, fail bool) leafnodes.BasicNode { - return leafnodes.NewBeforeEachNode(newBody(text, fail), codeLocation, 0, failer, 0) - } - - newAft := func(text string, fail bool) leafnodes.BasicNode { - return leafnodes.NewAfterEachNode(newBody(text, fail), codeLocation, 0, failer, 0) - } - - newJusBef := func(text string, fail bool) leafnodes.BasicNode { - return leafnodes.NewJustBeforeEachNode(newBody(text, fail), codeLocation, 0, failer, 0) - } - - newJusAft := func(text string, fail bool) leafnodes.BasicNode { - return leafnodes.NewJustAfterEachNode(newBody(text, fail), codeLocation, 0, failer, 0) - } - - newContainer := func(text string, flag types.FlagType, setupNodes ...leafnodes.BasicNode) *containernode.ContainerNode { - c := containernode.New(text, flag, codeLocation) - for _, node := range setupNodes { - c.PushSetupNode(node) - } - return c - } - - containers := func(containers ...*containernode.ContainerNode) []*containernode.ContainerNode { - return containers - } - - BeforeEach(func() { - buffer = gbytes.NewBuffer() - failer = Failer.New() - codeLocation = codelocation.New(0) - nodesThatRan = []string{} - }) - - Describe("marking specs focused and pending", func() { - It("should satisfy various caes", func() { - cases := []struct { - ContainerFlags []types.FlagType - SubjectFlag types.FlagType - Pending bool - Focused bool - }{ - {[]types.FlagType{}, noneFlag, false, false}, - {[]types.FlagType{}, focusedFlag, false, true}, - {[]types.FlagType{}, pendingFlag, true, false}, - {[]types.FlagType{noneFlag}, noneFlag, false, false}, - {[]types.FlagType{focusedFlag}, noneFlag, false, true}, - {[]types.FlagType{pendingFlag}, noneFlag, true, false}, - {[]types.FlagType{noneFlag}, focusedFlag, false, true}, - {[]types.FlagType{focusedFlag}, focusedFlag, false, true}, - {[]types.FlagType{pendingFlag}, focusedFlag, true, true}, - {[]types.FlagType{noneFlag}, pendingFlag, true, false}, - {[]types.FlagType{focusedFlag}, pendingFlag, true, true}, - {[]types.FlagType{pendingFlag}, pendingFlag, true, false}, - {[]types.FlagType{focusedFlag, noneFlag}, noneFlag, false, true}, - {[]types.FlagType{noneFlag, focusedFlag}, noneFlag, false, true}, - {[]types.FlagType{pendingFlag, noneFlag}, noneFlag, true, false}, - {[]types.FlagType{noneFlag, pendingFlag}, noneFlag, true, false}, - {[]types.FlagType{focusedFlag, pendingFlag}, noneFlag, true, true}, - } - - for i, c := range cases { - subject := newIt("it node", c.SubjectFlag, false) - containers := []*containernode.ContainerNode{} - for _, flag := range c.ContainerFlags { - containers = append(containers, newContainer("container", flag)) - } - - spec := New(subject, containers, false) - Ω(spec.Pending()).Should(Equal(c.Pending), "Case %d: %#v", i, c) - Ω(spec.Focused()).Should(Equal(c.Focused), "Case %d: %#v", i, c) - - if c.Pending { - Ω(spec.Summary("").State).Should(Equal(types.SpecStatePending)) - } - } - }) - }) - - Describe("Skip", func() { - It("should be skipped", func() { - spec := New(newIt("it node", noneFlag, false), containers(newContainer("container", noneFlag)), false) - Ω(spec.Skipped()).Should(BeFalse()) - spec.Skip() - Ω(spec.Skipped()).Should(BeTrue()) - Ω(spec.Summary("").State).Should(Equal(types.SpecStateSkipped)) - }) - }) - - Describe("IsMeasurement", func() { - It("should be true if the subject is a measurement node", func() { - spec := New(newIt("it node", noneFlag, false), containers(newContainer("container", noneFlag)), false) - Ω(spec.IsMeasurement()).Should(BeFalse()) - Ω(spec.Summary("").IsMeasurement).Should(BeFalse()) - Ω(spec.Summary("").NumberOfSamples).Should(Equal(1)) - - spec = New(newMeasure("measure node", noneFlag, false, 10), containers(newContainer("container", noneFlag)), false) - Ω(spec.IsMeasurement()).Should(BeTrue()) - Ω(spec.Summary("").IsMeasurement).Should(BeTrue()) - Ω(spec.Summary("").NumberOfSamples).Should(Equal(10)) - }) - }) - - Describe("Passed", func() { - It("should pass when the subject passed", func() { - spec := New(newIt("it node", noneFlag, false), containers(), false) - spec.Run(buffer) - - Ω(spec.Passed()).Should(BeTrue()) - Ω(spec.Failed()).Should(BeFalse()) - Ω(spec.Summary("").State).Should(Equal(types.SpecStatePassed)) - Ω(spec.Summary("").Failure).Should(BeZero()) - }) - }) - - Describe("Flaked", func() { - It("should work if Run is called twice and gets different results", func() { - i := 0 - spec := New(newItWithBody("flaky it", func() { - i++ - if i == 1 { - failer.Fail("oops", codeLocation) - } - }), containers(), false) - spec.Run(buffer) - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(spec.Flaked()).Should(BeFalse()) - Ω(spec.Summary("").State).Should(Equal(types.SpecStateFailed)) - Ω(spec.Summary("").Failure.Message).Should(Equal("oops")) - spec.Run(buffer) - Ω(spec.Passed()).Should(BeTrue()) - Ω(spec.Failed()).Should(BeFalse()) - Ω(spec.Flaked()).Should(BeTrue()) - Ω(spec.Summary("").State).Should(Equal(types.SpecStatePassed)) - }) - }) - - Describe("Failed", func() { - It("should be failed if the failure was panic", func() { - spec := New(newItWithBody("panicky it", func() { - panic("bam") - }), containers(), false) - spec.Run(buffer) - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(spec.Summary("").State).Should(Equal(types.SpecStatePanicked)) - Ω(spec.Summary("").Failure.Message).Should(Equal("Test Panicked")) - Ω(spec.Summary("").Failure.ForwardedPanic).Should(Equal("bam")) - }) - - It("should be failed if the failure was a timeout", func() { - spec := New(newItWithBody("sleepy it", func(done Done) {}), containers(), false) - spec.Run(buffer) - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(spec.Summary("").State).Should(Equal(types.SpecStateTimedOut)) - Ω(spec.Summary("").Failure.Message).Should(Equal("Timed out")) - }) - - It("should be failed if the failure was... a failure", func() { - spec := New(newItWithBody("failing it", func() { - failer.Fail("bam", codeLocation) - }), containers(), false) - spec.Run(buffer) - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(spec.Summary("").State).Should(Equal(types.SpecStateFailed)) - Ω(spec.Summary("").Failure.Message).Should(Equal("bam")) - }) - }) - - Describe("Concatenated string", func() { - It("should concatenate the texts of the containers and the subject", func() { - spec := New( - newIt("it node", noneFlag, false), - containers( - newContainer("outer container", noneFlag), - newContainer("inner container", noneFlag), - ), - false, - ) - - Ω(spec.ConcatenatedString()).Should(Equal("outer container inner container it node")) - }) - }) - - Describe("running it specs", func() { - Context("with just an it", func() { - Context("that succeeds", func() { - It("should run the it and report on its success", func() { - spec := New(newIt("it node", noneFlag, false), containers(), false) - spec.Run(buffer) - Ω(spec.Passed()).Should(BeTrue()) - Ω(spec.Failed()).Should(BeFalse()) - Ω(nodesThatRan).Should(Equal([]string{"it node"})) - }) - }) - - Context("that fails", func() { - It("should run the it and report on its success", func() { - spec := New(newIt("it node", noneFlag, true), containers(), false) - spec.Run(buffer) - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(spec.Summary("").Failure.Message).Should(Equal("it node")) - Ω(nodesThatRan).Should(Equal([]string{"it node"})) - }) - }) - }) - - Context("with a full set of setup nodes", func() { - var failingNodes map[string]bool - - BeforeEach(func() { - failingNodes = map[string]bool{} - }) - - JustBeforeEach(func() { - spec = New( - newIt("it node", noneFlag, failingNodes["it node"]), - containers( - newContainer("outer container", noneFlag, - newBef("outer bef A", failingNodes["outer bef A"]), - newBef("outer bef B", failingNodes["outer bef B"]), - newJusBef("outer jusbef A", failingNodes["outer jusbef A"]), - newJusBef("outer jusbef B", failingNodes["outer jusbef B"]), - newJusAft("outer jusaft A", failingNodes["outer jusaft A"]), - newJusAft("outer jusaft B", failingNodes["outer jusaft B"]), - newAft("outer aft A", failingNodes["outer aft A"]), - newAft("outer aft B", failingNodes["outer aft B"]), - ), - newContainer("inner container", noneFlag, - newBef("inner bef A", failingNodes["inner bef A"]), - newBef("inner bef B", failingNodes["inner bef B"]), - newJusBef("inner jusbef A", failingNodes["inner jusbef A"]), - newJusBef("inner jusbef B", failingNodes["inner jusbef B"]), - newJusAft("inner jusaft A", failingNodes["inner jusaft A"]), - newJusAft("inner jusaft B", failingNodes["inner jusaft B"]), - newAft("inner aft A", failingNodes["inner aft A"]), - newAft("inner aft B", failingNodes["inner aft B"]), - ), - ), - false, - ) - spec.Run(buffer) - }) - - Context("that all pass", func() { - It("should walk through the nodes in the correct order", func() { - Ω(spec.Passed()).Should(BeTrue()) - Ω(spec.Failed()).Should(BeFalse()) - Ω(nodesThatRan).Should(Equal([]string{ - "outer bef A", - "outer bef B", - "inner bef A", - "inner bef B", - "outer jusbef A", - "outer jusbef B", - "inner jusbef A", - "inner jusbef B", - "it node", - "inner jusaft A", - "inner jusaft B", - "outer jusaft A", - "outer jusaft B", - "inner aft A", - "inner aft B", - "outer aft A", - "outer aft B", - })) - }) - }) - - Context("when the subject fails", func() { - BeforeEach(func() { - failingNodes["it node"] = true - }) - - It("should run the afters", func() { - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(nodesThatRan).Should(Equal([]string{ - "outer bef A", - "outer bef B", - "inner bef A", - "inner bef B", - "outer jusbef A", - "outer jusbef B", - "inner jusbef A", - "inner jusbef B", - "it node", - "inner jusaft A", - "inner jusaft B", - "outer jusaft A", - "outer jusaft B", - "inner aft A", - "inner aft B", - "outer aft A", - "outer aft B", - })) - Ω(spec.Summary("").Failure.Message).Should(Equal("it node")) - }) - }) - - Context("when an inner before fails", func() { - BeforeEach(func() { - failingNodes["inner bef A"] = true - }) - - It("should not run any other befores, but it should run the subsequent afters", func() { - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(nodesThatRan).Should(Equal([]string{ - "outer bef A", - "outer bef B", - "inner bef A", - "inner jusaft A", - "inner jusaft B", - "outer jusaft A", - "outer jusaft B", - "inner aft A", - "inner aft B", - "outer aft A", - "outer aft B", - })) - Ω(spec.Summary("").Failure.Message).Should(Equal("inner bef A")) - }) - }) - - Context("when an outer before fails", func() { - BeforeEach(func() { - failingNodes["outer bef B"] = true - }) - - It("should not run any other befores, but it should run the subsequent afters", func() { - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(nodesThatRan).Should(Equal([]string{ - "outer bef A", - "outer bef B", - "outer jusaft A", - "outer jusaft B", - "outer aft A", - "outer aft B", - })) - Ω(spec.Summary("").Failure.Message).Should(Equal("outer bef B")) - }) - }) - - Context("when an after fails", func() { - BeforeEach(func() { - failingNodes["inner aft B"] = true - }) - - It("should run all other afters, but mark the test as failed", func() { - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(nodesThatRan).Should(Equal([]string{ - "outer bef A", - "outer bef B", - "inner bef A", - "inner bef B", - "outer jusbef A", - "outer jusbef B", - "inner jusbef A", - "inner jusbef B", - "it node", - "inner jusaft A", - "inner jusaft B", - "outer jusaft A", - "outer jusaft B", - "inner aft A", - "inner aft B", - "outer aft A", - "outer aft B", - })) - Ω(spec.Summary("").Failure.Message).Should(Equal("inner aft B")) - }) - }) - - Context("when a just before each fails", func() { - BeforeEach(func() { - failingNodes["outer jusbef B"] = true - }) - - It("should run the afters, but not the subject", func() { - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(nodesThatRan).Should(Equal([]string{ - "outer bef A", - "outer bef B", - "inner bef A", - "inner bef B", - "outer jusbef A", - "outer jusbef B", - "inner jusaft A", - "inner jusaft B", - "outer jusaft A", - "outer jusaft B", - "inner aft A", - "inner aft B", - "outer aft A", - "outer aft B", - })) - Ω(spec.Summary("").Failure.Message).Should(Equal("outer jusbef B")) - }) - }) - - Context("when a just after each fails", func() { - BeforeEach(func() { - failingNodes["outer jusaft A"] = true - }) - - It("should run all other afters, but mark the test as failed", func() { - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(nodesThatRan).Should(Equal([]string{ - "outer bef A", - "outer bef B", - "inner bef A", - "inner bef B", - "outer jusbef A", - "outer jusbef B", - "inner jusbef A", - "inner jusbef B", - "it node", - "inner jusaft A", - "inner jusaft B", - "outer jusaft A", - "outer jusaft B", - "inner aft A", - "inner aft B", - "outer aft A", - "outer aft B", - })) - Ω(spec.Summary("").Failure.Message).Should(Equal("outer jusaft A")) - }) - }) - - Context("when an after fails after an earlier node has failed", func() { - BeforeEach(func() { - failingNodes["it node"] = true - failingNodes["inner aft B"] = true - }) - - It("should record the earlier failure", func() { - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(nodesThatRan).Should(Equal([]string{ - "outer bef A", - "outer bef B", - "inner bef A", - "inner bef B", - "outer jusbef A", - "outer jusbef B", - "inner jusbef A", - "inner jusbef B", - "it node", - "inner jusaft A", - "inner jusaft B", - "outer jusaft A", - "outer jusaft B", - "inner aft A", - "inner aft B", - "outer aft A", - "outer aft B", - })) - Ω(spec.Summary("").Failure.Message).Should(Equal("it node")) - }) - }) - }) - }) - - Describe("running measurement specs", func() { - Context("when the measurement succeeds", func() { - It("should run N samples", func() { - spec = New( - newMeasure("measure node", noneFlag, false, 3), - containers( - newContainer("container", noneFlag, - newBef("bef A", false), - newJusBef("jusbef A", false), - newJusAft("jusaft A", false), - newAft("aft A", false), - ), - ), - false, - ) - spec.Run(buffer) - - Ω(spec.Passed()).Should(BeTrue()) - Ω(spec.Failed()).Should(BeFalse()) - Ω(nodesThatRan).Should(Equal([]string{ - "bef A", - "jusbef A", - "measure node", - "jusaft A", - "aft A", - "bef A", - "jusbef A", - "measure node", - "jusaft A", - "aft A", - "bef A", - "jusbef A", - "measure node", - "jusaft A", - "aft A", - })) - }) - }) - - Context("when the measurement fails", func() { - It("should bail after the failure occurs", func() { - spec = New( - newMeasure("measure node", noneFlag, true, 3), - containers( - newContainer("container", noneFlag, - newBef("bef A", false), - newJusBef("jusbef A", false), - newJusAft("jusaft A", false), - newAft("aft A", false), - ), - ), - false, - ) - spec.Run(buffer) - - Ω(spec.Passed()).Should(BeFalse()) - Ω(spec.Failed()).Should(BeTrue()) - Ω(nodesThatRan).Should(Equal([]string{ - "bef A", - "jusbef A", - "measure node", - "jusaft A", - "aft A", - })) - }) - }) - }) - - Describe("Summary", func() { - var ( - subjectCodeLocation types.CodeLocation - outerContainerCodeLocation types.CodeLocation - innerContainerCodeLocation types.CodeLocation - summary *types.SpecSummary - ) - - BeforeEach(func() { - subjectCodeLocation = codelocation.New(0) - outerContainerCodeLocation = codelocation.New(0) - innerContainerCodeLocation = codelocation.New(0) - - spec = New( - leafnodes.NewItNode("it node", func() { - time.Sleep(10 * time.Millisecond) - }, noneFlag, subjectCodeLocation, 0, failer, 0), - containers( - containernode.New("outer container", noneFlag, outerContainerCodeLocation), - containernode.New("inner container", noneFlag, innerContainerCodeLocation), - ), - false, - ) - - spec.Run(buffer) - Ω(spec.Passed()).Should(BeTrue()) - summary = spec.Summary("suite id") - }) - - It("should have the suite id", func() { - Ω(summary.SuiteID).Should(Equal("suite id")) - }) - - It("should have the component texts and code locations", func() { - Ω(summary.ComponentTexts).Should(Equal([]string{"outer container", "inner container", "it node"})) - Ω(summary.ComponentCodeLocations).Should(Equal([]types.CodeLocation{outerContainerCodeLocation, innerContainerCodeLocation, subjectCodeLocation})) - }) - - It("should have a runtime", func() { - Ω(summary.RunTime).Should(BeNumerically(">=", 10*time.Millisecond)) - }) - - It("should have a runtime which remains consistent after spec run", func() { - totalRunTime := summary.RunTime - Ω(totalRunTime).Should(BeNumerically(">=", 10*time.Millisecond)) - - Consistently(func() time.Duration { return spec.Summary("suite id").RunTime }).Should(Equal(totalRunTime)) - }) - - It("should not be a measurement, or have a measurement summary", func() { - Ω(summary.IsMeasurement).Should(BeFalse()) - Ω(summary.Measurements).Should(BeEmpty()) - }) - }) - - Describe("Summaries for measurements", func() { - var summary *types.SpecSummary - - BeforeEach(func() { - spec = New(leafnodes.NewMeasureNode("measure node", func(b Benchmarker) { - b.RecordValue("a value", 7, "some info") - b.RecordValueWithPrecision("another value", 8, "ns", 5, "more info") - }, noneFlag, codeLocation, 4, failer, 0), containers(), false) - spec.Run(buffer) - Ω(spec.Passed()).Should(BeTrue()) - summary = spec.Summary("suite id") - }) - - It("should include the number of samples", func() { - Ω(summary.NumberOfSamples).Should(Equal(4)) - }) - - It("should be a measurement", func() { - Ω(summary.IsMeasurement).Should(BeTrue()) - }) - - It("should have the measurements report", func() { - Ω(summary.Measurements).Should(HaveKey("a value")) - report := summary.Measurements["a value"] - Ω(report.Name).Should(Equal("a value")) - Ω(report.Info).Should(Equal("some info")) - Ω(report.Results).Should(Equal([]float64{7, 7, 7, 7})) - - Ω(summary.Measurements).Should(HaveKey("another value")) - report = summary.Measurements["another value"] - Ω(report.Name).Should(Equal("another value")) - Ω(report.Info).Should(Equal("more info")) - Ω(report.Results).Should(Equal([]float64{8, 8, 8, 8})) - Ω(report.Units).Should(Equal("ns")) - Ω(report.Precision).Should(Equal(5)) - }) - }) - - Describe("When told to emit progress", func() { - It("should emit progress to the writer as it runs Befores, JustBefores, Afters, and Its", func() { - spec = New( - newIt("it node", noneFlag, false), - containers( - newContainer("outer container", noneFlag, - newBef("outer bef A", false), - newJusBef("outer jusbef A", false), - newJusAft("outer jusaft A", false), - newAft("outer aft A", false), - ), - newContainer("inner container", noneFlag, - newBef("inner bef A", false), - newJusBef("inner jusbef A", false), - newJusAft("inner jusaft A", false), - newAft("inner aft A", false), - ), - ), - true, - ) - spec.Run(buffer) - - Ω(buffer).Should(gbytes.Say(`\[BeforeEach\] outer container`)) - Ω(buffer).Should(gbytes.Say(`\[BeforeEach\] inner container`)) - Ω(buffer).Should(gbytes.Say(`\[JustBeforeEach\] outer container`)) - Ω(buffer).Should(gbytes.Say(`\[JustBeforeEach\] inner container`)) - Ω(buffer).Should(gbytes.Say(`\[It\] it node`)) - Ω(buffer).Should(gbytes.Say(`\[JustAfterEach\] inner container`)) - Ω(buffer).Should(gbytes.Say(`\[JustAfterEach\] outer container`)) - Ω(buffer).Should(gbytes.Say(`\[AfterEach\] inner container`)) - Ω(buffer).Should(gbytes.Say(`\[AfterEach\] outer container`)) - }) - - It("should emit progress to the writer as it runs Befores, JustBefores, JustAfters, Afters, and Measures", func() { - spec = New( - newMeasure("measure node", noneFlag, false, 2), - containers(), - true, - ) - spec.Run(buffer) - - Ω(buffer).Should(gbytes.Say(`\[Measure\] measure node`)) - Ω(buffer).Should(gbytes.Say(`\[Measure\] measure node`)) - }) - }) -}) diff --git a/internal/spec/specs.go b/internal/spec/specs.go deleted file mode 100644 index 0a24139fb1..0000000000 --- a/internal/spec/specs.go +++ /dev/null @@ -1,144 +0,0 @@ -package spec - -import ( - "math/rand" - "regexp" - "sort" - "strings" -) - -type Specs struct { - specs []*Spec - names []string - - hasProgrammaticFocus bool - RegexScansFilePath bool -} - -func NewSpecs(specs []*Spec) *Specs { - names := make([]string, len(specs)) - for i, spec := range specs { - names[i] = spec.ConcatenatedString() - } - return &Specs{ - specs: specs, - names: names, - } -} - -func (e *Specs) Specs() []*Spec { - return e.specs -} - -func (e *Specs) HasProgrammaticFocus() bool { - return e.hasProgrammaticFocus -} - -func (e *Specs) Shuffle(r *rand.Rand) { - sort.Sort(e) - permutation := r.Perm(len(e.specs)) - shuffledSpecs := make([]*Spec, len(e.specs)) - names := make([]string, len(e.specs)) - for i, j := range permutation { - shuffledSpecs[i] = e.specs[j] - names[i] = e.names[j] - } - e.specs = shuffledSpecs - e.names = names -} - -func (e *Specs) ApplyFocus(description string, focus, skip []string) { - if len(focus)+len(skip) == 0 { - e.applyProgrammaticFocus() - } else { - e.applyRegExpFocusAndSkip(description, focus, skip) - } -} - -func (e *Specs) applyProgrammaticFocus() { - e.hasProgrammaticFocus = false - for _, spec := range e.specs { - if spec.Focused() && !spec.Pending() { - e.hasProgrammaticFocus = true - break - } - } - - if e.hasProgrammaticFocus { - for _, spec := range e.specs { - if !spec.Focused() { - spec.Skip() - } - } - } -} - -// toMatch returns a byte[] to be used by regex matchers. When adding new behaviours to the matching function, -// this is the place which we append to. -func (e *Specs) toMatch(description string, i int) []byte { - if i > len(e.names) { - return nil - } - if e.RegexScansFilePath { - return []byte( - description + " " + - e.names[i] + " " + - e.specs[i].subject.CodeLocation().FileName) - } else { - return []byte( - description + " " + - e.names[i]) - } -} - -func (e *Specs) applyRegExpFocusAndSkip(description string, focus, skip []string) { - var focusFilter, skipFilter *regexp.Regexp - if len(focus) > 0 { - focusFilter = regexp.MustCompile(strings.Join(focus, "|")) - } - if len(skip) > 0 { - skipFilter = regexp.MustCompile(strings.Join(skip, "|")) - } - - for i, spec := range e.specs { - matchesFocus := true - matchesSkip := false - - toMatch := e.toMatch(description, i) - - if focusFilter != nil { - matchesFocus = focusFilter.Match(toMatch) - } - - if skipFilter != nil { - matchesSkip = skipFilter.Match(toMatch) - } - - if !matchesFocus || matchesSkip { - spec.Skip() - } - } -} - -func (e *Specs) SkipMeasurements() { - for _, spec := range e.specs { - if spec.IsMeasurement() { - spec.Skip() - } - } -} - -//sort.Interface - -func (e *Specs) Len() int { - return len(e.specs) -} - -func (e *Specs) Less(i, j int) bool { - return e.names[i] < e.names[j] -} - -func (e *Specs) Swap(i, j int) { - e.names[i], e.names[j] = e.names[j], e.names[i] - e.specs[i], e.specs[j] = e.specs[j], e.specs[i] -} diff --git a/internal/spec/specs_test.go b/internal/spec/specs_test.go deleted file mode 100644 index bfe35fd135..0000000000 --- a/internal/spec/specs_test.go +++ /dev/null @@ -1,292 +0,0 @@ -package spec_test - -import ( - "math/rand" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/spec" - . "github.com/onsi/gomega" - - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/internal/containernode" - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/types" -) - -var _ = Describe("Specs", func() { - var specs *Specs - - newSpec := func(text string, flag types.FlagType) *Spec { - subject := leafnodes.NewItNode(text, func() {}, flag, codelocation.New(0), 0, nil, 0) - return New(subject, []*containernode.ContainerNode{}, false) - } - - newMeasureSpec := func(text string, flag types.FlagType) *Spec { - subject := leafnodes.NewMeasureNode(text, func(Benchmarker) {}, flag, codelocation.New(0), 0, nil, 0) - return New(subject, []*containernode.ContainerNode{}, false) - } - - newSpecs := func(args ...interface{}) *Specs { - specs := []*Spec{} - for index := 0; index < len(args)-1; index += 2 { - specs = append(specs, newSpec(args[index].(string), args[index+1].(types.FlagType))) - } - return NewSpecs(specs) - } - - specTexts := func(specs *Specs) []string { - texts := []string{} - for _, spec := range specs.Specs() { - texts = append(texts, spec.ConcatenatedString()) - } - return texts - } - - willRunTexts := func(specs *Specs) []string { - texts := []string{} - for _, spec := range specs.Specs() { - if !(spec.Skipped() || spec.Pending()) { - texts = append(texts, spec.ConcatenatedString()) - } - } - return texts - } - - skippedTexts := func(specs *Specs) []string { - texts := []string{} - for _, spec := range specs.Specs() { - if spec.Skipped() { - texts = append(texts, spec.ConcatenatedString()) - } - } - return texts - } - - pendingTexts := func(specs *Specs) []string { - texts := []string{} - for _, spec := range specs.Specs() { - if spec.Pending() { - texts = append(texts, spec.ConcatenatedString()) - } - } - return texts - } - - Describe("Shuffling specs", func() { - It("should shuffle the specs using the passed in randomizer", func() { - specs17 := newSpecs("C", noneFlag, "A", noneFlag, "B", noneFlag) - specs17.Shuffle(rand.New(rand.NewSource(17))) - texts17 := specTexts(specs17) - - specs17Again := newSpecs("C", noneFlag, "A", noneFlag, "B", noneFlag) - specs17Again.Shuffle(rand.New(rand.NewSource(17))) - texts17Again := specTexts(specs17Again) - - specs15 := newSpecs("C", noneFlag, "A", noneFlag, "B", noneFlag) - specs15.Shuffle(rand.New(rand.NewSource(15))) - texts15 := specTexts(specs15) - - specsUnshuffled := newSpecs("C", noneFlag, "A", noneFlag, "B", noneFlag) - textsUnshuffled := specTexts(specsUnshuffled) - - Ω(textsUnshuffled).Should(Equal([]string{"C", "A", "B"})) - - Ω(texts17).Should(Equal(texts17Again)) - Ω(texts17).ShouldNot(Equal(texts15)) - Ω(texts17).ShouldNot(Equal(textsUnshuffled)) - Ω(texts15).ShouldNot(Equal(textsUnshuffled)) - - Ω(texts17).Should(HaveLen(3)) - Ω(texts17).Should(ContainElement("A")) - Ω(texts17).Should(ContainElement("B")) - Ω(texts17).Should(ContainElement("C")) - - Ω(texts15).Should(HaveLen(3)) - Ω(texts15).Should(ContainElement("A")) - Ω(texts15).Should(ContainElement("B")) - Ω(texts15).Should(ContainElement("C")) - }) - }) - - Describe("with no programmatic focus", func() { - BeforeEach(func() { - specs = newSpecs("A1", noneFlag, "A2", noneFlag, "B1", noneFlag, "B2", pendingFlag) - specs.ApplyFocus("", []string{}, []string{}) - }) - - It("should not report as having programmatic specs", func() { - Ω(specs.HasProgrammaticFocus()).Should(BeFalse()) - }) - }) - - Describe("Applying focus/skip", func() { - var ( - description string - focus, skip []string - ) - - BeforeEach(func() { - description = "" - focus = []string{} - skip = []string{} - }) - - JustBeforeEach(func() { - specs = newSpecs("A1", focusedFlag, "A2", noneFlag, "B1", focusedFlag, "B2", pendingFlag) - specs.ApplyFocus(description, focus, skip) - }) - - Context("with neither a focus string nor a skip string", func() { - It("should apply the programmatic focus", func() { - Ω(willRunTexts(specs)).Should(Equal([]string{"A1", "B1"})) - Ω(skippedTexts(specs)).Should(Equal([]string{"A2", "B2"})) - Ω(pendingTexts(specs)).Should(BeEmpty()) - }) - - It("should report as having programmatic specs", func() { - Ω(specs.HasProgrammaticFocus()).Should(BeTrue()) - }) - }) - - Context("with a focus regexp", func() { - BeforeEach(func() { - focus = []string{"A"} - }) - - It("should override the programmatic focus", func() { - Ω(willRunTexts(specs)).Should(Equal([]string{"A1", "A2"})) - Ω(skippedTexts(specs)).Should(Equal([]string{"B1", "B2"})) - Ω(pendingTexts(specs)).Should(BeEmpty()) - }) - - It("should not report as having programmatic specs", func() { - Ω(specs.HasProgrammaticFocus()).Should(BeFalse()) - }) - }) - - Context("with a focus regexp", func() { - BeforeEach(func() { - focus = []string{"B"} - }) - - It("should not override any pendings", func() { - Ω(willRunTexts(specs)).Should(Equal([]string{"B1"})) - Ω(skippedTexts(specs)).Should(Equal([]string{"A1", "A2"})) - Ω(pendingTexts(specs)).Should(Equal([]string{"B2"})) - }) - }) - - Context("with a description", func() { - BeforeEach(func() { - description = "C" - focus = []string{"C"} - }) - - It("should include the description in the focus determination", func() { - Ω(willRunTexts(specs)).Should(Equal([]string{"A1", "A2", "B1"})) - Ω(skippedTexts(specs)).Should(BeEmpty()) - Ω(pendingTexts(specs)).Should(Equal([]string{"B2"})) - }) - }) - - Context("with a description", func() { - BeforeEach(func() { - description = "C" - skip = []string{"C"} - }) - - It("should include the description in the focus determination", func() { - Ω(willRunTexts(specs)).Should(BeEmpty()) - Ω(skippedTexts(specs)).Should(Equal([]string{"A1", "A2", "B1", "B2"})) - Ω(pendingTexts(specs)).Should(BeEmpty()) - }) - }) - - Context("with a skip regexp", func() { - BeforeEach(func() { - skip = []string{"A"} - }) - - It("should override the programmatic focus", func() { - Ω(willRunTexts(specs)).Should(Equal([]string{"B1"})) - Ω(skippedTexts(specs)).Should(Equal([]string{"A1", "A2"})) - Ω(pendingTexts(specs)).Should(Equal([]string{"B2"})) - }) - - It("should not report as having programmatic specs", func() { - Ω(specs.HasProgrammaticFocus()).Should(BeFalse()) - }) - }) - - Context("with both a focus and a skip regexp", func() { - BeforeEach(func() { - focus = []string{"1"} - skip = []string{"B"} - }) - - It("should AND the two", func() { - Ω(willRunTexts(specs)).Should(Equal([]string{"A1"})) - Ω(skippedTexts(specs)).Should(Equal([]string{"A2", "B1", "B2"})) - Ω(pendingTexts(specs)).Should(BeEmpty()) - }) - - It("should not report as having programmatic specs", func() { - Ω(specs.HasProgrammaticFocus()).Should(BeFalse()) - }) - }) - }) - - Describe("With a focused spec within a pending context and a pending spec within a focused context", func() { - BeforeEach(func() { - pendingInFocused := New( - leafnodes.NewItNode("PendingInFocused", func() {}, pendingFlag, codelocation.New(0), 0, nil, 0), - []*containernode.ContainerNode{ - containernode.New("", focusedFlag, codelocation.New(0)), - }, false) - - focusedInPending := New( - leafnodes.NewItNode("FocusedInPending", func() {}, focusedFlag, codelocation.New(0), 0, nil, 0), - []*containernode.ContainerNode{ - containernode.New("", pendingFlag, codelocation.New(0)), - }, false) - - specs = NewSpecs([]*Spec{ - newSpec("A", noneFlag), - newSpec("B", noneFlag), - pendingInFocused, - focusedInPending, - }) - specs.ApplyFocus("", []string{}, []string{}) - }) - - It("should not have a programmatic focus and should run all tests", func() { - Ω(willRunTexts(specs)).Should(Equal([]string{"A", "B"})) - Ω(skippedTexts(specs)).Should(BeEmpty()) - Ω(pendingTexts(specs)).Should(ConsistOf(ContainSubstring("PendingInFocused"), ContainSubstring("FocusedInPending"))) - }) - }) - - Describe("skipping measurements", func() { - BeforeEach(func() { - specs = NewSpecs([]*Spec{ - newSpec("A", noneFlag), - newSpec("B", noneFlag), - newSpec("C", pendingFlag), - newMeasureSpec("measurementA", noneFlag), - newMeasureSpec("measurementB", pendingFlag), - }) - }) - - It("should skip measurements", func() { - Ω(willRunTexts(specs)).Should(Equal([]string{"A", "B", "measurementA"})) - Ω(skippedTexts(specs)).Should(BeEmpty()) - Ω(pendingTexts(specs)).Should(Equal([]string{"C", "measurementB"})) - - specs.SkipMeasurements() - - Ω(willRunTexts(specs)).Should(Equal([]string{"A", "B"})) - Ω(skippedTexts(specs)).Should(Equal([]string{"measurementA", "measurementB"})) - Ω(pendingTexts(specs)).Should(Equal([]string{"C"})) - }) - }) -}) diff --git a/internal/spec_iterator/index_computer.go b/internal/spec_iterator/index_computer.go deleted file mode 100644 index 82272554aa..0000000000 --- a/internal/spec_iterator/index_computer.go +++ /dev/null @@ -1,55 +0,0 @@ -package spec_iterator - -func ParallelizedIndexRange(length int, parallelTotal int, parallelNode int) (startIndex int, count int) { - if length == 0 { - return 0, 0 - } - - // We have more nodes than tests. Trivial case. - if parallelTotal >= length { - if parallelNode > length { - return 0, 0 - } else { - return parallelNode - 1, 1 - } - } - - // This is the minimum amount of tests that a node will be required to run - minTestsPerNode := length / parallelTotal - - // This is the maximum amount of tests that a node will be required to run - // The algorithm guarantees that this would be equal to at least the minimum amount - // and at most one more - maxTestsPerNode := minTestsPerNode - if length%parallelTotal != 0 { - maxTestsPerNode++ - } - - // Number of nodes that will have to run the maximum amount of tests per node - numMaxLoadNodes := length % parallelTotal - - // Number of nodes that precede the current node and will have to run the maximum amount of tests per node - var numPrecedingMaxLoadNodes int - if parallelNode > numMaxLoadNodes { - numPrecedingMaxLoadNodes = numMaxLoadNodes - } else { - numPrecedingMaxLoadNodes = parallelNode - 1 - } - - // Number of nodes that precede the current node and will have to run the minimum amount of tests per node - var numPrecedingMinLoadNodes int - if parallelNode <= numMaxLoadNodes { - numPrecedingMinLoadNodes = 0 - } else { - numPrecedingMinLoadNodes = parallelNode - numMaxLoadNodes - 1 - } - - // Evaluate the test start index and number of tests to run - startIndex = numPrecedingMaxLoadNodes*maxTestsPerNode + numPrecedingMinLoadNodes*minTestsPerNode - if parallelNode > numMaxLoadNodes { - count = minTestsPerNode - } else { - count = maxTestsPerNode - } - return -} diff --git a/internal/spec_iterator/index_computer_test.go b/internal/spec_iterator/index_computer_test.go deleted file mode 100644 index 65da9837c6..0000000000 --- a/internal/spec_iterator/index_computer_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package spec_iterator_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/spec_iterator" - . "github.com/onsi/gomega" -) - -var _ = Describe("ParallelizedIndexRange", func() { - var startIndex, count int - - It("should return the correct index range for 4 tests on 2 nodes", func() { - startIndex, count = ParallelizedIndexRange(4, 2, 1) - Ω(startIndex).Should(Equal(0)) - Ω(count).Should(Equal(2)) - - startIndex, count = ParallelizedIndexRange(4, 2, 2) - Ω(startIndex).Should(Equal(2)) - Ω(count).Should(Equal(2)) - }) - - It("should return the correct index range for 5 tests on 2 nodes", func() { - startIndex, count = ParallelizedIndexRange(5, 2, 1) - Ω(startIndex).Should(Equal(0)) - Ω(count).Should(Equal(3)) - - startIndex, count = ParallelizedIndexRange(5, 2, 2) - Ω(startIndex).Should(Equal(3)) - Ω(count).Should(Equal(2)) - }) - - It("should return the correct index range for 5 tests on 3 nodes", func() { - startIndex, count = ParallelizedIndexRange(5, 3, 1) - Ω(startIndex).Should(Equal(0)) - Ω(count).Should(Equal(2)) - - startIndex, count = ParallelizedIndexRange(5, 3, 2) - Ω(startIndex).Should(Equal(2)) - Ω(count).Should(Equal(2)) - - startIndex, count = ParallelizedIndexRange(5, 3, 3) - Ω(startIndex).Should(Equal(4)) - Ω(count).Should(Equal(1)) - }) - - It("should return the correct index range for 5 tests on 4 nodes", func() { - startIndex, count = ParallelizedIndexRange(5, 4, 1) - Ω(startIndex).Should(Equal(0)) - Ω(count).Should(Equal(2)) - - startIndex, count = ParallelizedIndexRange(5, 4, 2) - Ω(startIndex).Should(Equal(2)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 4, 3) - Ω(startIndex).Should(Equal(3)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 4, 4) - Ω(startIndex).Should(Equal(4)) - Ω(count).Should(Equal(1)) - }) - - It("should return the correct index range for 5 tests on 5 nodes", func() { - startIndex, count = ParallelizedIndexRange(5, 5, 1) - Ω(startIndex).Should(Equal(0)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 5, 2) - Ω(startIndex).Should(Equal(1)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 5, 3) - Ω(startIndex).Should(Equal(2)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 5, 4) - Ω(startIndex).Should(Equal(3)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 5, 5) - Ω(startIndex).Should(Equal(4)) - Ω(count).Should(Equal(1)) - }) - - It("should return the correct index range for 5 tests on 6 nodes", func() { - startIndex, count = ParallelizedIndexRange(5, 6, 1) - Ω(startIndex).Should(Equal(0)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 6, 2) - Ω(startIndex).Should(Equal(1)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 6, 3) - Ω(startIndex).Should(Equal(2)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 6, 4) - Ω(startIndex).Should(Equal(3)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 6, 5) - Ω(startIndex).Should(Equal(4)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(5, 6, 6) - Ω(count).Should(Equal(0)) - }) - - It("should return the correct index range for 5 tests on 7 nodes", func() { - startIndex, count = ParallelizedIndexRange(5, 7, 6) - Ω(count).Should(Equal(0)) - - startIndex, count = ParallelizedIndexRange(5, 7, 7) - Ω(count).Should(Equal(0)) - }) - - It("should return the correct index range for 11 tests on 7 nodes", func() { - startIndex, count = ParallelizedIndexRange(11, 7, 1) - Ω(startIndex).Should(Equal(0)) - Ω(count).Should(Equal(2)) - - startIndex, count = ParallelizedIndexRange(11, 7, 2) - Ω(startIndex).Should(Equal(2)) - Ω(count).Should(Equal(2)) - - startIndex, count = ParallelizedIndexRange(11, 7, 3) - Ω(startIndex).Should(Equal(4)) - Ω(count).Should(Equal(2)) - - startIndex, count = ParallelizedIndexRange(11, 7, 4) - Ω(startIndex).Should(Equal(6)) - Ω(count).Should(Equal(2)) - - startIndex, count = ParallelizedIndexRange(11, 7, 5) - Ω(startIndex).Should(Equal(8)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(11, 7, 6) - Ω(startIndex).Should(Equal(9)) - Ω(count).Should(Equal(1)) - - startIndex, count = ParallelizedIndexRange(11, 7, 7) - Ω(startIndex).Should(Equal(10)) - Ω(count).Should(Equal(1)) - }) - -}) diff --git a/internal/spec_iterator/parallel_spec_iterator.go b/internal/spec_iterator/parallel_spec_iterator.go deleted file mode 100644 index 99f548bca4..0000000000 --- a/internal/spec_iterator/parallel_spec_iterator.go +++ /dev/null @@ -1,59 +0,0 @@ -package spec_iterator - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/onsi/ginkgo/internal/spec" -) - -type ParallelIterator struct { - specs []*spec.Spec - host string - client *http.Client -} - -func NewParallelIterator(specs []*spec.Spec, host string) *ParallelIterator { - return &ParallelIterator{ - specs: specs, - host: host, - client: &http.Client{}, - } -} - -func (s *ParallelIterator) Next() (*spec.Spec, error) { - resp, err := s.client.Get(s.host + "/counter") - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode) - } - - var counter Counter - err = json.NewDecoder(resp.Body).Decode(&counter) - if err != nil { - return nil, err - } - - if counter.Index >= len(s.specs) { - return nil, ErrClosed - } - - return s.specs[counter.Index], nil -} - -func (s *ParallelIterator) NumberOfSpecsPriorToIteration() int { - return len(s.specs) -} - -func (s *ParallelIterator) NumberOfSpecsToProcessIfKnown() (int, bool) { - return -1, false -} - -func (s *ParallelIterator) NumberOfSpecsThatWillBeRunIfKnown() (int, bool) { - return -1, false -} diff --git a/internal/spec_iterator/parallel_spec_iterator_test.go b/internal/spec_iterator/parallel_spec_iterator_test.go deleted file mode 100644 index c5a762fd5d..0000000000 --- a/internal/spec_iterator/parallel_spec_iterator_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package spec_iterator_test - -import ( - "net/http" - - . "github.com/onsi/ginkgo/internal/spec_iterator" - "github.com/onsi/gomega/ghttp" - - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/internal/containernode" - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/internal/spec" - "github.com/onsi/ginkgo/types" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("ParallelSpecIterator", func() { - var specs []*spec.Spec - var iterator *ParallelIterator - var server *ghttp.Server - - newSpec := func(text string, flag types.FlagType) *spec.Spec { - subject := leafnodes.NewItNode(text, func() {}, flag, codelocation.New(0), 0, nil, 0) - return spec.New(subject, []*containernode.ContainerNode{}, false) - } - - BeforeEach(func() { - specs = []*spec.Spec{ - newSpec("A", types.FlagTypePending), - newSpec("B", types.FlagTypeNone), - newSpec("C", types.FlagTypeNone), - newSpec("D", types.FlagTypeNone), - } - specs[3].Skip() - - server = ghttp.NewServer() - - iterator = NewParallelIterator(specs, "http://"+server.Addr()) - }) - - AfterEach(func() { - server.Close() - }) - - It("should report the total number of specs", func() { - Ω(iterator.NumberOfSpecsPriorToIteration()).Should(Equal(4)) - }) - - It("should not report the number to be processed", func() { - n, known := iterator.NumberOfSpecsToProcessIfKnown() - Ω(n).Should(Equal(-1)) - Ω(known).Should(BeFalse()) - }) - - It("should not report the number that will be run", func() { - n, known := iterator.NumberOfSpecsThatWillBeRunIfKnown() - Ω(n).Should(Equal(-1)) - Ω(known).Should(BeFalse()) - }) - - Describe("iterating", func() { - Describe("when the server returns well-formed responses", func() { - BeforeEach(func() { - server.AppendHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, Counter{Index: 0}), - ghttp.RespondWithJSONEncoded(http.StatusOK, Counter{Index: 1}), - ghttp.RespondWithJSONEncoded(http.StatusOK, Counter{Index: 3}), - ghttp.RespondWithJSONEncoded(http.StatusOK, Counter{Index: 4}), - ) - }) - - It("should return the specs in question", func() { - Ω(iterator.Next()).Should(Equal(specs[0])) - Ω(iterator.Next()).Should(Equal(specs[1])) - Ω(iterator.Next()).Should(Equal(specs[3])) - spec, err := iterator.Next() - Ω(spec).Should(BeNil()) - Ω(err).Should(MatchError(ErrClosed)) - }) - }) - - Describe("when the server 404s", func() { - BeforeEach(func() { - server.AppendHandlers( - ghttp.RespondWith(http.StatusNotFound, ""), - ) - }) - - It("should return an error", func() { - spec, err := iterator.Next() - Ω(spec).Should(BeNil()) - Ω(err).Should(MatchError("unexpected status code 404")) - }) - }) - - Describe("when the server returns gibberish", func() { - BeforeEach(func() { - server.AppendHandlers( - ghttp.RespondWith(http.StatusOK, "ß"), - ) - }) - - It("should error", func() { - spec, err := iterator.Next() - Ω(spec).Should(BeNil()) - Ω(err).ShouldNot(BeNil()) - }) - }) - }) -}) diff --git a/internal/spec_iterator/serial_spec_iterator.go b/internal/spec_iterator/serial_spec_iterator.go deleted file mode 100644 index a51c93b8b6..0000000000 --- a/internal/spec_iterator/serial_spec_iterator.go +++ /dev/null @@ -1,45 +0,0 @@ -package spec_iterator - -import ( - "github.com/onsi/ginkgo/internal/spec" -) - -type SerialIterator struct { - specs []*spec.Spec - index int -} - -func NewSerialIterator(specs []*spec.Spec) *SerialIterator { - return &SerialIterator{ - specs: specs, - index: 0, - } -} - -func (s *SerialIterator) Next() (*spec.Spec, error) { - if s.index >= len(s.specs) { - return nil, ErrClosed - } - - spec := s.specs[s.index] - s.index += 1 - return spec, nil -} - -func (s *SerialIterator) NumberOfSpecsPriorToIteration() int { - return len(s.specs) -} - -func (s *SerialIterator) NumberOfSpecsToProcessIfKnown() (int, bool) { - return len(s.specs), true -} - -func (s *SerialIterator) NumberOfSpecsThatWillBeRunIfKnown() (int, bool) { - count := 0 - for _, s := range s.specs { - if !s.Skipped() && !s.Pending() { - count += 1 - } - } - return count, true -} diff --git a/internal/spec_iterator/serial_spec_iterator_test.go b/internal/spec_iterator/serial_spec_iterator_test.go deleted file mode 100644 index dde4a344e8..0000000000 --- a/internal/spec_iterator/serial_spec_iterator_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package spec_iterator_test - -import ( - . "github.com/onsi/ginkgo/internal/spec_iterator" - - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/internal/containernode" - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/internal/spec" - "github.com/onsi/ginkgo/types" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("SerialSpecIterator", func() { - var specs []*spec.Spec - var iterator *SerialIterator - - newSpec := func(text string, flag types.FlagType) *spec.Spec { - subject := leafnodes.NewItNode(text, func() {}, flag, codelocation.New(0), 0, nil, 0) - return spec.New(subject, []*containernode.ContainerNode{}, false) - } - - BeforeEach(func() { - specs = []*spec.Spec{ - newSpec("A", types.FlagTypePending), - newSpec("B", types.FlagTypeNone), - newSpec("C", types.FlagTypeNone), - newSpec("D", types.FlagTypeNone), - } - specs[3].Skip() - - iterator = NewSerialIterator(specs) - }) - - It("should report the total number of specs", func() { - Ω(iterator.NumberOfSpecsPriorToIteration()).Should(Equal(4)) - }) - - It("should report the number to be processed", func() { - n, known := iterator.NumberOfSpecsToProcessIfKnown() - Ω(n).Should(Equal(4)) - Ω(known).Should(BeTrue()) - }) - - It("should report the number that will be run", func() { - n, known := iterator.NumberOfSpecsThatWillBeRunIfKnown() - Ω(n).Should(Equal(2)) - Ω(known).Should(BeTrue()) - }) - - Describe("iterating", func() { - It("should return the specs in order", func() { - Ω(iterator.Next()).Should(Equal(specs[0])) - Ω(iterator.Next()).Should(Equal(specs[1])) - Ω(iterator.Next()).Should(Equal(specs[2])) - Ω(iterator.Next()).Should(Equal(specs[3])) - spec, err := iterator.Next() - Ω(spec).Should(BeNil()) - Ω(err).Should(MatchError(ErrClosed)) - }) - }) -}) diff --git a/internal/spec_iterator/sharded_parallel_spec_iterator.go b/internal/spec_iterator/sharded_parallel_spec_iterator.go deleted file mode 100644 index ad4a3ea3c6..0000000000 --- a/internal/spec_iterator/sharded_parallel_spec_iterator.go +++ /dev/null @@ -1,47 +0,0 @@ -package spec_iterator - -import "github.com/onsi/ginkgo/internal/spec" - -type ShardedParallelIterator struct { - specs []*spec.Spec - index int - maxIndex int -} - -func NewShardedParallelIterator(specs []*spec.Spec, total int, node int) *ShardedParallelIterator { - startIndex, count := ParallelizedIndexRange(len(specs), total, node) - - return &ShardedParallelIterator{ - specs: specs, - index: startIndex, - maxIndex: startIndex + count, - } -} - -func (s *ShardedParallelIterator) Next() (*spec.Spec, error) { - if s.index >= s.maxIndex { - return nil, ErrClosed - } - - spec := s.specs[s.index] - s.index += 1 - return spec, nil -} - -func (s *ShardedParallelIterator) NumberOfSpecsPriorToIteration() int { - return len(s.specs) -} - -func (s *ShardedParallelIterator) NumberOfSpecsToProcessIfKnown() (int, bool) { - return s.maxIndex - s.index, true -} - -func (s *ShardedParallelIterator) NumberOfSpecsThatWillBeRunIfKnown() (int, bool) { - count := 0 - for i := s.index; i < s.maxIndex; i += 1 { - if !s.specs[i].Skipped() && !s.specs[i].Pending() { - count += 1 - } - } - return count, true -} diff --git a/internal/spec_iterator/sharded_parallel_spec_iterator_test.go b/internal/spec_iterator/sharded_parallel_spec_iterator_test.go deleted file mode 100644 index c3786e03aa..0000000000 --- a/internal/spec_iterator/sharded_parallel_spec_iterator_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package spec_iterator_test - -import ( - . "github.com/onsi/ginkgo/internal/spec_iterator" - - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/internal/containernode" - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/internal/spec" - "github.com/onsi/ginkgo/types" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("ShardedParallelSpecIterator", func() { - var specs []*spec.Spec - var iterator *ShardedParallelIterator - - newSpec := func(text string, flag types.FlagType) *spec.Spec { - subject := leafnodes.NewItNode(text, func() {}, flag, codelocation.New(0), 0, nil, 0) - return spec.New(subject, []*containernode.ContainerNode{}, false) - } - - BeforeEach(func() { - specs = []*spec.Spec{ - newSpec("A", types.FlagTypePending), - newSpec("B", types.FlagTypeNone), - newSpec("C", types.FlagTypeNone), - newSpec("D", types.FlagTypeNone), - } - specs[3].Skip() - - iterator = NewShardedParallelIterator(specs, 2, 1) - }) - - It("should report the total number of specs", func() { - Ω(iterator.NumberOfSpecsPriorToIteration()).Should(Equal(4)) - }) - - It("should report the number to be processed", func() { - n, known := iterator.NumberOfSpecsToProcessIfKnown() - Ω(n).Should(Equal(2)) - Ω(known).Should(BeTrue()) - }) - - It("should report the number that will be run", func() { - n, known := iterator.NumberOfSpecsThatWillBeRunIfKnown() - Ω(n).Should(Equal(1)) - Ω(known).Should(BeTrue()) - }) - - Describe("iterating", func() { - It("should return the specs in order", func() { - Ω(iterator.Next()).Should(Equal(specs[0])) - Ω(iterator.Next()).Should(Equal(specs[1])) - spec, err := iterator.Next() - Ω(spec).Should(BeNil()) - Ω(err).Should(MatchError(ErrClosed)) - }) - }) -}) diff --git a/internal/spec_iterator/spec_iterator.go b/internal/spec_iterator/spec_iterator.go deleted file mode 100644 index 74bffad646..0000000000 --- a/internal/spec_iterator/spec_iterator.go +++ /dev/null @@ -1,20 +0,0 @@ -package spec_iterator - -import ( - "errors" - - "github.com/onsi/ginkgo/internal/spec" -) - -var ErrClosed = errors.New("no more specs to run") - -type SpecIterator interface { - Next() (*spec.Spec, error) - NumberOfSpecsPriorToIteration() int - NumberOfSpecsToProcessIfKnown() (int, bool) - NumberOfSpecsThatWillBeRunIfKnown() (int, bool) -} - -type Counter struct { - Index int `json:"index"` -} diff --git a/internal/spec_iterator/spec_iterator_suite_test.go b/internal/spec_iterator/spec_iterator_suite_test.go deleted file mode 100644 index 5c08a77e36..0000000000 --- a/internal/spec_iterator/spec_iterator_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package spec_iterator_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestSpecIterator(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "SpecIterator Suite") -} diff --git a/internal/spec_test.go b/internal/spec_test.go new file mode 100644 index 0000000000..2fead4e750 --- /dev/null +++ b/internal/spec_test.go @@ -0,0 +1,119 @@ +package internal_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Spec and Specs", func() { + Describe("spec.Text", func() { + Context("when the spec has nodes with texts", func() { + It("returns the concatenated texts of its nodes (omitting any empty texts)", func() { + spec := S(N(), N("Oh death,"), N(), N("where is"), N("thy"), N(), N("sting?")) + Ω(spec.Text()).Should(Equal("Oh death, where is thy sting?")) + }) + }) + + Context("when the spec has no nodes", func() { + It("returns the empty string", func() { + Ω(Spec{}.Text()).Should(BeZero()) + }) + }) + }) + + Describe("spec.FirstNodeWithType", func() { + Context("when there are matching nodes", func() { + It("returns the first node matching any of the passed in node types", func() { + nBef := N(ntBef) + nIt := N(ntIt) + spec := S(N(ntCon), N(ntAf), nBef, N(ntBef), nIt, N(ntAf)) + Ω(spec.FirstNodeWithType(ntIt, ntBef)).Should(Equal(nBef)) + }) + }) + + Context("when no nodes match", func() { + spec := S(N(ntCon), N(ntIt), N(ntAf)) + Ω(spec.FirstNodeWithType(ntBef)).Should(BeZero()) + }) + }) + + Describe("specs.HasAnySpecsMarkedPending", func() { + Context("when there are no specs with any nodes marked pending", func() { + It("returns false", func() { + specs := Specs{ + S(N(), N(), N()), + S(N(), N()), + } + + Ω(specs.HasAnySpecsMarkedPending()).Should(BeFalse()) + }) + }) + + Context("when there is at least one spec with a node marked pending", func() { + It("returns true", func() { + specs := Specs{ + S(N(), N(), N()), + S(N(), N(MarkedPending(true)), N()), + S(N(), N()), + } + + Ω(specs.HasAnySpecsMarkedPending()).Should(BeTrue()) + }) + }) + }) + + Describe("specs.CountWithoutSkip()", func() { + It("returns the number of specs that have skip set to false", func() { + specs := Specs{{Skip: false}, {Skip: true}, {Skip: true}, {Skip: false}, {Skip: false}} + Ω(specs.CountWithoutSkip()).Should(Equal(3)) + }) + }) + + Describe("partitioning specs", func() { + var specs Specs + BeforeEach(func() { + sharedBefore := N(ntBef) + sharedContainer := N(ntCon) + otherSharedContainer := N(ntCon) + specs = Specs{ + S(sharedBefore, sharedContainer, N(ntIt)), + S(sharedBefore, N(ntIt)), + S(N(ntBef), sharedContainer, N(ntIt)), + S(sharedBefore, N(ntIt)), + S(N(ntCon), N(ntIt)), + S(otherSharedContainer, N(ntBef), N(ntIt)), + S(otherSharedContainer, N(ntBef), N(ntIt)), + } + }) + + It(`returns a slice of []Specs - where each entry is a group of specs for which + the first node that matches on of the passed in nodetypes has the same id`, func() { + Ω(specs.PartitionByFirstNodeWithType(ntIt)).Should(Equal([]Specs{ + Specs{specs[0]}, + Specs{specs[1]}, + Specs{specs[2]}, + Specs{specs[3]}, + Specs{specs[4]}, + Specs{specs[5]}, + Specs{specs[6]}, + }), "partitioning by It returns one grouping per spec as each spec has a unique It") + + Ω(specs.PartitionByFirstNodeWithType(ntIt, ntCon)).Should(Equal([]Specs{ + Specs{specs[0], specs[2]}, + Specs{specs[1]}, + Specs{specs[3]}, + Specs{specs[4]}, + Specs{specs[5], specs[6]}, + }), "partitioning by Container and It groups specs by common Container first, and It second") + + Ω(specs.PartitionByFirstNodeWithType(ntCon)).Should(Equal([]Specs{ + Specs{specs[0], specs[2]}, + Specs{specs[4]}, + Specs{specs[5], specs[6]}, + }), "partitioning by just Container will not pull in specs that have no container") + + Ω(specs.PartitionByFirstNodeWithType(ntAf)).Should(BeEmpty(), + "partitioning by a node type that doesn't appear matches against no specs and comes back empty") + }) + }) +}) diff --git a/internal/specrunner/random_id.go b/internal/specrunner/random_id.go deleted file mode 100644 index a0b8b62d52..0000000000 --- a/internal/specrunner/random_id.go +++ /dev/null @@ -1,15 +0,0 @@ -package specrunner - -import ( - "crypto/rand" - "fmt" -) - -func randomID() string { - b := make([]byte, 8) - _, err := rand.Read(b) - if err != nil { - return "" - } - return fmt.Sprintf("%x-%x-%x-%x", b[0:2], b[2:4], b[4:6], b[6:8]) -} diff --git a/internal/specrunner/spec_runner.go b/internal/specrunner/spec_runner.go deleted file mode 100644 index c9a0a60d84..0000000000 --- a/internal/specrunner/spec_runner.go +++ /dev/null @@ -1,411 +0,0 @@ -package specrunner - -import ( - "fmt" - "os" - "os/signal" - "sync" - "syscall" - - "github.com/onsi/ginkgo/internal/spec_iterator" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/internal/spec" - Writer "github.com/onsi/ginkgo/internal/writer" - "github.com/onsi/ginkgo/reporters" - "github.com/onsi/ginkgo/types" - - "time" -) - -type SpecRunner struct { - description string - beforeSuiteNode leafnodes.SuiteNode - iterator spec_iterator.SpecIterator - afterSuiteNode leafnodes.SuiteNode - reporters []reporters.Reporter - startTime time.Time - suiteID string - runningSpec *spec.Spec - writer Writer.WriterInterface - config config.GinkgoConfigType - interrupted bool - processedSpecs []*spec.Spec - lock *sync.Mutex -} - -func New(description string, beforeSuiteNode leafnodes.SuiteNode, iterator spec_iterator.SpecIterator, afterSuiteNode leafnodes.SuiteNode, reporters []reporters.Reporter, writer Writer.WriterInterface, config config.GinkgoConfigType) *SpecRunner { - return &SpecRunner{ - description: description, - beforeSuiteNode: beforeSuiteNode, - iterator: iterator, - afterSuiteNode: afterSuiteNode, - reporters: reporters, - writer: writer, - config: config, - suiteID: randomID(), - lock: &sync.Mutex{}, - } -} - -func (runner *SpecRunner) Run() bool { - if runner.config.DryRun { - runner.performDryRun() - return true - } - - runner.reportSuiteWillBegin() - signalRegistered := make(chan struct{}) - go runner.registerForInterrupts(signalRegistered) - <-signalRegistered - - suitePassed := runner.runBeforeSuite() - - if suitePassed { - suitePassed = runner.runSpecs() - } - - runner.blockForeverIfInterrupted() - - suitePassed = runner.runAfterSuite() && suitePassed - - runner.reportSuiteDidEnd(suitePassed) - - return suitePassed -} - -func (runner *SpecRunner) performDryRun() { - runner.reportSuiteWillBegin() - - if runner.beforeSuiteNode != nil { - summary := runner.beforeSuiteNode.Summary() - summary.State = types.SpecStatePassed - runner.reportBeforeSuite(summary) - } - - for { - spec, err := runner.iterator.Next() - if err == spec_iterator.ErrClosed { - break - } - if err != nil { - fmt.Println("failed to iterate over tests:\n" + err.Error()) - break - } - - runner.processedSpecs = append(runner.processedSpecs, spec) - - summary := spec.Summary(runner.suiteID) - runner.reportSpecWillRun(summary) - if summary.State == types.SpecStateInvalid { - summary.State = types.SpecStatePassed - } - runner.reportSpecDidComplete(summary, false) - } - - if runner.afterSuiteNode != nil { - summary := runner.afterSuiteNode.Summary() - summary.State = types.SpecStatePassed - runner.reportAfterSuite(summary) - } - - runner.reportSuiteDidEnd(true) -} - -func (runner *SpecRunner) runBeforeSuite() bool { - if runner.beforeSuiteNode == nil || runner.wasInterrupted() { - return true - } - - runner.writer.Truncate() - conf := runner.config - passed := runner.beforeSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost) - if !passed { - runner.writer.DumpOut() - } - runner.reportBeforeSuite(runner.beforeSuiteNode.Summary()) - return passed -} - -func (runner *SpecRunner) runAfterSuite() bool { - if runner.afterSuiteNode == nil { - return true - } - - runner.writer.Truncate() - conf := runner.config - passed := runner.afterSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost) - if !passed { - runner.writer.DumpOut() - } - runner.reportAfterSuite(runner.afterSuiteNode.Summary()) - return passed -} - -func (runner *SpecRunner) runSpecs() bool { - suiteFailed := false - skipRemainingSpecs := false - for { - spec, err := runner.iterator.Next() - if err == spec_iterator.ErrClosed { - break - } - if err != nil { - fmt.Println("failed to iterate over tests:\n" + err.Error()) - suiteFailed = true - break - } - - runner.processedSpecs = append(runner.processedSpecs, spec) - - if runner.wasInterrupted() { - break - } - if skipRemainingSpecs { - spec.Skip() - } - - if !spec.Skipped() && !spec.Pending() { - if passed := runner.runSpec(spec); !passed { - suiteFailed = true - } - } else if spec.Pending() && runner.config.FailOnPending { - runner.reportSpecWillRun(spec.Summary(runner.suiteID)) - suiteFailed = true - runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed()) - } else { - runner.reportSpecWillRun(spec.Summary(runner.suiteID)) - runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed()) - } - - if spec.Failed() && runner.config.FailFast { - skipRemainingSpecs = true - } - } - - return !suiteFailed -} - -func (runner *SpecRunner) runSpec(spec *spec.Spec) (passed bool) { - maxAttempts := 1 - if runner.config.FlakeAttempts > 0 { - // uninitialized configs count as 1 - maxAttempts = runner.config.FlakeAttempts - } - - for i := 0; i < maxAttempts; i++ { - runner.reportSpecWillRun(spec.Summary(runner.suiteID)) - runner.runningSpec = spec - spec.Run(runner.writer) - runner.runningSpec = nil - runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed()) - if !spec.Failed() { - return true - } - } - return false -} - -func (runner *SpecRunner) CurrentSpecSummary() (*types.SpecSummary, bool) { - if runner.runningSpec == nil { - return nil, false - } - - return runner.runningSpec.Summary(runner.suiteID), true -} - -func (runner *SpecRunner) registerForInterrupts(signalRegistered chan struct{}) { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - close(signalRegistered) - - <-c - signal.Stop(c) - runner.markInterrupted() - go runner.registerForHardInterrupts() - runner.writer.DumpOutWithHeader(` -Received interrupt. Emitting contents of GinkgoWriter... ---------------------------------------------------------- -`) - if runner.afterSuiteNode != nil { - fmt.Fprint(os.Stderr, ` ---------------------------------------------------------- -Received interrupt. Running AfterSuite... -^C again to terminate immediately -`) - runner.runAfterSuite() - } - runner.reportSuiteDidEnd(false) - os.Exit(1) -} - -func (runner *SpecRunner) registerForHardInterrupts() { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - - <-c - fmt.Fprintln(os.Stderr, "\nReceived second interrupt. Shutting down.") - os.Exit(1) -} - -func (runner *SpecRunner) blockForeverIfInterrupted() { - runner.lock.Lock() - interrupted := runner.interrupted - runner.lock.Unlock() - - if interrupted { - select {} - } -} - -func (runner *SpecRunner) markInterrupted() { - runner.lock.Lock() - defer runner.lock.Unlock() - runner.interrupted = true -} - -func (runner *SpecRunner) wasInterrupted() bool { - runner.lock.Lock() - defer runner.lock.Unlock() - return runner.interrupted -} - -func (runner *SpecRunner) reportSuiteWillBegin() { - runner.startTime = time.Now() - summary := runner.suiteWillBeginSummary() - for _, reporter := range runner.reporters { - reporter.SpecSuiteWillBegin(runner.config, summary) - } -} - -func (runner *SpecRunner) reportBeforeSuite(summary *types.SetupSummary) { - for _, reporter := range runner.reporters { - reporter.BeforeSuiteDidRun(summary) - } -} - -func (runner *SpecRunner) reportAfterSuite(summary *types.SetupSummary) { - for _, reporter := range runner.reporters { - reporter.AfterSuiteDidRun(summary) - } -} - -func (runner *SpecRunner) reportSpecWillRun(summary *types.SpecSummary) { - runner.writer.Truncate() - - for _, reporter := range runner.reporters { - reporter.SpecWillRun(summary) - } -} - -func (runner *SpecRunner) reportSpecDidComplete(summary *types.SpecSummary, failed bool) { - if len(summary.CapturedOutput) == 0 { - summary.CapturedOutput = string(runner.writer.Bytes()) - } - for i := len(runner.reporters) - 1; i >= 1; i-- { - runner.reporters[i].SpecDidComplete(summary) - } - - if failed { - runner.writer.DumpOut() - } - - runner.reporters[0].SpecDidComplete(summary) -} - -func (runner *SpecRunner) reportSuiteDidEnd(success bool) { - summary := runner.suiteDidEndSummary(success) - summary.RunTime = time.Since(runner.startTime) - for _, reporter := range runner.reporters { - reporter.SpecSuiteDidEnd(summary) - } -} - -func (runner *SpecRunner) countSpecsThatRanSatisfying(filter func(ex *spec.Spec) bool) (count int) { - count = 0 - - for _, spec := range runner.processedSpecs { - if filter(spec) { - count++ - } - } - - return count -} - -func (runner *SpecRunner) suiteDidEndSummary(success bool) *types.SuiteSummary { - numberOfSpecsThatWillBeRun := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { - return !ex.Skipped() && !ex.Pending() - }) - - numberOfPendingSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { - return ex.Pending() - }) - - numberOfSkippedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { - return ex.Skipped() - }) - - numberOfPassedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { - return ex.Passed() - }) - - numberOfFlakedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { - return ex.Flaked() - }) - - numberOfFailedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool { - return ex.Failed() - }) - - if runner.beforeSuiteNode != nil && !runner.beforeSuiteNode.Passed() && !runner.config.DryRun { - var known bool - numberOfSpecsThatWillBeRun, known = runner.iterator.NumberOfSpecsThatWillBeRunIfKnown() - if !known { - numberOfSpecsThatWillBeRun = runner.iterator.NumberOfSpecsPriorToIteration() - } - numberOfFailedSpecs = numberOfSpecsThatWillBeRun - } - - return &types.SuiteSummary{ - SuiteDescription: runner.description, - SuiteSucceeded: success, - SuiteID: runner.suiteID, - - NumberOfSpecsBeforeParallelization: runner.iterator.NumberOfSpecsPriorToIteration(), - NumberOfTotalSpecs: len(runner.processedSpecs), - NumberOfSpecsThatWillBeRun: numberOfSpecsThatWillBeRun, - NumberOfPendingSpecs: numberOfPendingSpecs, - NumberOfSkippedSpecs: numberOfSkippedSpecs, - NumberOfPassedSpecs: numberOfPassedSpecs, - NumberOfFailedSpecs: numberOfFailedSpecs, - NumberOfFlakedSpecs: numberOfFlakedSpecs, - } -} - -func (runner *SpecRunner) suiteWillBeginSummary() *types.SuiteSummary { - numTotal, known := runner.iterator.NumberOfSpecsToProcessIfKnown() - if !known { - numTotal = -1 - } - - numToRun, known := runner.iterator.NumberOfSpecsThatWillBeRunIfKnown() - if !known { - numToRun = -1 - } - - return &types.SuiteSummary{ - SuiteDescription: runner.description, - SuiteID: runner.suiteID, - - NumberOfSpecsBeforeParallelization: runner.iterator.NumberOfSpecsPriorToIteration(), - NumberOfTotalSpecs: numTotal, - NumberOfSpecsThatWillBeRun: numToRun, - NumberOfPendingSpecs: -1, - NumberOfSkippedSpecs: -1, - NumberOfPassedSpecs: -1, - NumberOfFailedSpecs: -1, - NumberOfFlakedSpecs: -1, - } -} diff --git a/internal/specrunner/spec_runner_suite_test.go b/internal/specrunner/spec_runner_suite_test.go deleted file mode 100644 index c8388fb6f7..0000000000 --- a/internal/specrunner/spec_runner_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package specrunner_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestSpecRunner(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Spec Runner Suite") -} diff --git a/internal/specrunner/spec_runner_test.go b/internal/specrunner/spec_runner_test.go deleted file mode 100644 index 772f569d8e..0000000000 --- a/internal/specrunner/spec_runner_test.go +++ /dev/null @@ -1,787 +0,0 @@ -package specrunner_test - -import ( - . "github.com/onsi/ginkgo" - "github.com/onsi/ginkgo/internal/spec_iterator" - . "github.com/onsi/ginkgo/internal/specrunner" - "github.com/onsi/ginkgo/types" - . "github.com/onsi/gomega" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/internal/codelocation" - "github.com/onsi/ginkgo/internal/containernode" - Failer "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/internal/spec" - Writer "github.com/onsi/ginkgo/internal/writer" - "github.com/onsi/ginkgo/reporters" -) - -var noneFlag = types.FlagTypeNone -var pendingFlag = types.FlagTypePending - -var _ = Describe("Spec Runner", func() { - var ( - reporter1 *reporters.FakeReporter - reporter2 *reporters.FakeReporter - failer *Failer.Failer - writer *Writer.FakeGinkgoWriter - - thingsThatRan []string - - runner *SpecRunner - ) - - newBefSuite := func(text string, fail bool) leafnodes.SuiteNode { - return leafnodes.NewBeforeSuiteNode(func() { - writer.AddEvent(text) - thingsThatRan = append(thingsThatRan, text) - if fail { - failer.Fail(text, codelocation.New(0)) - } - }, codelocation.New(0), 0, failer) - } - - newAftSuite := func(text string, fail bool) leafnodes.SuiteNode { - return leafnodes.NewAfterSuiteNode(func() { - writer.AddEvent(text) - thingsThatRan = append(thingsThatRan, text) - if fail { - failer.Fail(text, codelocation.New(0)) - } - }, codelocation.New(0), 0, failer) - } - - newSpec := func(text string, flag types.FlagType, fail bool) *spec.Spec { - subject := leafnodes.NewItNode(text, func() { - writer.AddEvent(text) - thingsThatRan = append(thingsThatRan, text) - if fail { - failer.Fail(text, codelocation.New(0)) - } - }, flag, codelocation.New(0), 0, failer, 0) - - return spec.New(subject, []*containernode.ContainerNode{}, false) - } - - newFlakySpec := func(text string, flag types.FlagType, failures int) *spec.Spec { - runs := 0 - subject := leafnodes.NewItNode(text, func() { - writer.AddEvent(text) - thingsThatRan = append(thingsThatRan, text) - runs++ - if runs < failures { - failer.Fail(text, codelocation.New(0)) - } - }, flag, codelocation.New(0), 0, failer, 0) - - return spec.New(subject, []*containernode.ContainerNode{}, false) - } - - newSpecWithBody := func(text string, body interface{}) *spec.Spec { - subject := leafnodes.NewItNode(text, body, noneFlag, codelocation.New(0), 0, failer, 0) - - return spec.New(subject, []*containernode.ContainerNode{}, false) - } - - newRunner := func(config config.GinkgoConfigType, beforeSuiteNode leafnodes.SuiteNode, afterSuiteNode leafnodes.SuiteNode, specs ...*spec.Spec) *SpecRunner { - iterator := spec_iterator.NewSerialIterator(specs) - return New("description", beforeSuiteNode, iterator, afterSuiteNode, []reporters.Reporter{reporter1, reporter2}, writer, config) - } - - BeforeEach(func() { - reporter1 = reporters.NewFakeReporter() - reporter2 = reporters.NewFakeReporter() - writer = Writer.NewFake() - failer = Failer.New() - - thingsThatRan = []string{} - }) - - Describe("Running and Reporting", func() { - var specA, pendingSpec, anotherPendingSpec, failedSpec, specB, skippedSpec *spec.Spec - var willRunCalls, didCompleteCalls []string - var conf config.GinkgoConfigType - - JustBeforeEach(func() { - willRunCalls = []string{} - didCompleteCalls = []string{} - specA = newSpec("spec A", noneFlag, false) - pendingSpec = newSpec("pending spec", pendingFlag, false) - anotherPendingSpec = newSpec("another pending spec", pendingFlag, false) - failedSpec = newSpec("failed spec", noneFlag, true) - specB = newSpec("spec B", noneFlag, false) - skippedSpec = newSpec("skipped spec", noneFlag, false) - skippedSpec.Skip() - - reporter1.SpecWillRunStub = func(specSummary *types.SpecSummary) { - willRunCalls = append(willRunCalls, "Reporter1") - } - reporter2.SpecWillRunStub = func(specSummary *types.SpecSummary) { - willRunCalls = append(willRunCalls, "Reporter2") - } - - reporter1.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { - didCompleteCalls = append(didCompleteCalls, "Reporter1") - } - reporter2.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { - didCompleteCalls = append(didCompleteCalls, "Reporter2") - } - - runner = newRunner(conf, newBefSuite("BefSuite", false), newAftSuite("AftSuite", false), specA, pendingSpec, anotherPendingSpec, failedSpec, specB, skippedSpec) - runner.Run() - }) - - BeforeEach(func() { - conf = config.GinkgoConfigType{RandomSeed: 17} - }) - - It("should skip skipped/pending tests", func() { - Ω(thingsThatRan).Should(Equal([]string{"BefSuite", "spec A", "failed spec", "spec B", "AftSuite"})) - }) - - It("should report to any attached reporters", func() { - Ω(reporter1.Config).Should(Equal(reporter2.Config)) - Ω(reporter1.BeforeSuiteSummary).Should(Equal(reporter2.BeforeSuiteSummary)) - Ω(reporter1.BeginSummary).Should(Equal(reporter2.BeginSummary)) - Ω(reporter1.SpecWillRunSummaries).Should(Equal(reporter2.SpecWillRunSummaries)) - Ω(reporter1.SpecSummaries).Should(Equal(reporter2.SpecSummaries)) - Ω(reporter1.AfterSuiteSummary).Should(Equal(reporter2.AfterSuiteSummary)) - Ω(reporter1.EndSummary).Should(Equal(reporter2.EndSummary)) - }) - - It("should report that a spec did end in reverse order", func() { - Ω(willRunCalls[0:4]).Should(Equal([]string{"Reporter1", "Reporter2", "Reporter1", "Reporter2"})) - Ω(didCompleteCalls[0:4]).Should(Equal([]string{"Reporter2", "Reporter1", "Reporter2", "Reporter1"})) - }) - - It("should report the passed in config", func() { - Ω(reporter1.Config.RandomSeed).Should(BeNumerically("==", 17)) - }) - - It("should report the beginning of the suite", func() { - Ω(reporter1.BeginSummary.SuiteDescription).Should(Equal("description")) - Ω(reporter1.BeginSummary.SuiteID).Should(MatchRegexp("[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}")) - Ω(reporter1.BeginSummary.NumberOfSpecsBeforeParallelization).Should(Equal(6)) - Ω(reporter1.BeginSummary.NumberOfTotalSpecs).Should(Equal(6)) - Ω(reporter1.BeginSummary.NumberOfSpecsThatWillBeRun).Should(Equal(3)) - Ω(reporter1.BeginSummary.NumberOfPendingSpecs).Should(Equal(-1)) - Ω(reporter1.BeginSummary.NumberOfSkippedSpecs).Should(Equal(-1)) - }) - - It("should report the end of the suite", func() { - Ω(reporter1.EndSummary.SuiteDescription).Should(Equal("description")) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) - Ω(reporter1.EndSummary.SuiteID).Should(MatchRegexp("[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}")) - Ω(reporter1.EndSummary.NumberOfSpecsBeforeParallelization).Should(Equal(6)) - Ω(reporter1.EndSummary.NumberOfTotalSpecs).Should(Equal(6)) - Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(3)) - Ω(reporter1.EndSummary.NumberOfPendingSpecs).Should(Equal(2)) - Ω(reporter1.EndSummary.NumberOfSkippedSpecs).Should(Equal(1)) - Ω(reporter1.EndSummary.NumberOfPassedSpecs).Should(Equal(2)) - Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(1)) - }) - - Context("when told to perform a dry run", func() { - BeforeEach(func() { - conf.DryRun = true - }) - - It("should report to the reporters", func() { - Ω(reporter1.Config).Should(Equal(reporter2.Config)) - Ω(reporter1.BeforeSuiteSummary).Should(Equal(reporter2.BeforeSuiteSummary)) - Ω(reporter1.BeginSummary).Should(Equal(reporter2.BeginSummary)) - Ω(reporter1.SpecWillRunSummaries).Should(Equal(reporter2.SpecWillRunSummaries)) - Ω(reporter1.SpecSummaries).Should(Equal(reporter2.SpecSummaries)) - Ω(reporter1.AfterSuiteSummary).Should(Equal(reporter2.AfterSuiteSummary)) - Ω(reporter1.EndSummary).Should(Equal(reporter2.EndSummary)) - }) - - It("should not actually run anything", func() { - Ω(thingsThatRan).Should(BeEmpty()) - }) - - It("report before and after suites as passed", func() { - Ω(reporter1.BeforeSuiteSummary.State).Should(Equal(types.SpecStatePassed)) - Ω(reporter1.AfterSuiteSummary.State).Should(Equal(types.SpecStatePassed)) - }) - - It("should report specs as passed", func() { - summaries := reporter1.SpecSummaries - Ω(summaries).Should(HaveLen(6)) - Ω(summaries[0].ComponentTexts).Should(ContainElement("spec A")) - Ω(summaries[0].State).Should(Equal(types.SpecStatePassed)) - Ω(summaries[1].ComponentTexts).Should(ContainElement("pending spec")) - Ω(summaries[1].State).Should(Equal(types.SpecStatePending)) - Ω(summaries[2].ComponentTexts).Should(ContainElement("another pending spec")) - Ω(summaries[2].State).Should(Equal(types.SpecStatePending)) - Ω(summaries[3].ComponentTexts).Should(ContainElement("failed spec")) - Ω(summaries[3].State).Should(Equal(types.SpecStatePassed)) - Ω(summaries[4].ComponentTexts).Should(ContainElement("spec B")) - Ω(summaries[4].State).Should(Equal(types.SpecStatePassed)) - Ω(summaries[5].ComponentTexts).Should(ContainElement("skipped spec")) - Ω(summaries[5].State).Should(Equal(types.SpecStateSkipped)) - }) - - It("should report the end of the suite", func() { - Ω(reporter1.EndSummary.SuiteDescription).Should(Equal("description")) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeTrue()) - Ω(reporter1.EndSummary.SuiteID).Should(MatchRegexp("[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}")) - Ω(reporter1.EndSummary.NumberOfSpecsBeforeParallelization).Should(Equal(6)) - Ω(reporter1.EndSummary.NumberOfTotalSpecs).Should(Equal(6)) - Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(3)) - Ω(reporter1.EndSummary.NumberOfPendingSpecs).Should(Equal(2)) - Ω(reporter1.EndSummary.NumberOfSkippedSpecs).Should(Equal(1)) - Ω(reporter1.EndSummary.NumberOfPassedSpecs).Should(Equal(0)) - Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(0)) - }) - - It("should not report a slow test", func() { - summaries := reporter1.SpecSummaries - for _, s := range summaries { - Expect(s.RunTime).To(BeZero()) - } - }) - }) - }) - - Describe("reporting on specs", func() { - var proceed chan bool - var ready chan bool - var finished chan bool - BeforeEach(func() { - ready = make(chan bool) - proceed = make(chan bool) - finished = make(chan bool) - skippedSpec := newSpec("SKIP", noneFlag, false) - skippedSpec.Skip() - - runner = newRunner( - config.GinkgoConfigType{}, - newBefSuite("BefSuite", false), - newAftSuite("AftSuite", false), - skippedSpec, - newSpec("PENDING", pendingFlag, false), - newSpecWithBody("RUN", func() { - close(ready) - <-proceed - }), - ) - go func() { - runner.Run() - close(finished) - }() - }) - - It("should report about pending/skipped specs", func() { - <-ready - Ω(reporter1.SpecWillRunSummaries).Should(HaveLen(3)) - - Ω(reporter1.SpecWillRunSummaries[0].ComponentTexts[0]).Should(Equal("SKIP")) - Ω(reporter1.SpecWillRunSummaries[1].ComponentTexts[0]).Should(Equal("PENDING")) - Ω(reporter1.SpecWillRunSummaries[2].ComponentTexts[0]).Should(Equal("RUN")) - - Ω(reporter1.SpecSummaries[0].ComponentTexts[0]).Should(Equal("SKIP")) - Ω(reporter1.SpecSummaries[1].ComponentTexts[0]).Should(Equal("PENDING")) - Ω(reporter1.SpecSummaries).Should(HaveLen(2)) - - close(proceed) - <-finished - - Ω(reporter1.SpecSummaries).Should(HaveLen(3)) - Ω(reporter1.SpecSummaries[2].ComponentTexts[0]).Should(Equal("RUN")) - }) - }) - - Describe("Running and Reporting when there's flakes", func() { - var specA, pendingSpec, flakySpec, failedSpec, specB, skippedSpec *spec.Spec - var willRunCalls, didCompleteCalls []string - var conf config.GinkgoConfigType - var failedSpecFlag = noneFlag - - JustBeforeEach(func() { - willRunCalls = []string{} - didCompleteCalls = []string{} - specA = newSpec("spec A", noneFlag, false) - pendingSpec = newSpec("pending spec", pendingFlag, false) - flakySpec = newFlakySpec("flaky spec", noneFlag, 3) - failedSpec = newSpec("failed spec", failedSpecFlag, true) - specB = newSpec("spec B", noneFlag, false) - skippedSpec = newSpec("skipped spec", noneFlag, false) - skippedSpec.Skip() - - reporter1.SpecWillRunStub = func(specSummary *types.SpecSummary) { - willRunCalls = append(willRunCalls, "Reporter1") - } - reporter2.SpecWillRunStub = func(specSummary *types.SpecSummary) { - willRunCalls = append(willRunCalls, "Reporter2") - } - - reporter1.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { - didCompleteCalls = append(didCompleteCalls, "Reporter1") - } - reporter2.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { - didCompleteCalls = append(didCompleteCalls, "Reporter2") - } - - runner = newRunner(conf, newBefSuite("BefSuite", false), newAftSuite("AftSuite", false), specA, pendingSpec, flakySpec, failedSpec, specB, skippedSpec) - runner.Run() - }) - - BeforeEach(func() { - failedSpecFlag = noneFlag - conf = config.GinkgoConfigType{ - RandomSeed: 17, - FlakeAttempts: 5, - } - }) - - It("should skip skipped/pending tests", func() { - Ω(thingsThatRan).Should(Equal([]string{"BefSuite", "spec A", "flaky spec", "flaky spec", "flaky spec", "failed spec", "failed spec", "failed spec", "failed spec", "failed spec", "spec B", "AftSuite"})) - }) - - It("should report to any attached reporters", func() { - Ω(reporter1.Config).Should(Equal(reporter2.Config)) - Ω(reporter1.BeforeSuiteSummary).Should(Equal(reporter2.BeforeSuiteSummary)) - Ω(reporter1.BeginSummary).Should(Equal(reporter2.BeginSummary)) - Ω(reporter1.SpecWillRunSummaries).Should(Equal(reporter2.SpecWillRunSummaries)) - Ω(reporter1.SpecSummaries).Should(Equal(reporter2.SpecSummaries)) - Ω(reporter1.AfterSuiteSummary).Should(Equal(reporter2.AfterSuiteSummary)) - Ω(reporter1.EndSummary).Should(Equal(reporter2.EndSummary)) - }) - - It("should report that a spec did end in reverse order", func() { - Ω(willRunCalls[0:4]).Should(Equal([]string{"Reporter1", "Reporter2", "Reporter1", "Reporter2"})) - Ω(didCompleteCalls[0:4]).Should(Equal([]string{"Reporter2", "Reporter1", "Reporter2", "Reporter1"})) - }) - - It("should report the passed in config", func() { - Ω(reporter1.Config.RandomSeed).Should(BeNumerically("==", 17)) - }) - - It("should report the beginning of the suite", func() { - Ω(reporter1.BeginSummary.SuiteDescription).Should(Equal("description")) - Ω(reporter1.BeginSummary.SuiteID).Should(MatchRegexp("[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}")) - Ω(reporter1.BeginSummary.NumberOfSpecsBeforeParallelization).Should(Equal(6)) - Ω(reporter1.BeginSummary.NumberOfTotalSpecs).Should(Equal(6)) - Ω(reporter1.BeginSummary.NumberOfSpecsThatWillBeRun).Should(Equal(4)) - Ω(reporter1.BeginSummary.NumberOfPendingSpecs).Should(Equal(-1)) - Ω(reporter1.BeginSummary.NumberOfSkippedSpecs).Should(Equal(-1)) - }) - - It("should report the end of the suite", func() { - Ω(reporter1.EndSummary.SuiteDescription).Should(Equal("description")) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) - Ω(reporter1.EndSummary.SuiteID).Should(MatchRegexp("[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}")) - Ω(reporter1.EndSummary.NumberOfSpecsBeforeParallelization).Should(Equal(6)) - Ω(reporter1.EndSummary.NumberOfTotalSpecs).Should(Equal(6)) - Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(4)) - Ω(reporter1.EndSummary.NumberOfPendingSpecs).Should(Equal(1)) - Ω(reporter1.EndSummary.NumberOfSkippedSpecs).Should(Equal(1)) - Ω(reporter1.EndSummary.NumberOfPassedSpecs).Should(Equal(3)) - Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(1)) - Ω(reporter1.EndSummary.NumberOfFlakedSpecs).Should(Equal(1)) - }) - - Context("when nothing fails", func() { - BeforeEach(func() { - failedSpecFlag = pendingFlag - }) - - It("the suite should pass even with flakes", func() { - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeTrue()) - Ω(reporter1.EndSummary.NumberOfFlakedSpecs).Should(Equal(1)) - }) - }) - - Context("when told to perform a dry run", func() { - BeforeEach(func() { - conf.DryRun = true - }) - - It("should report to the reporters", func() { - Ω(reporter1.Config).Should(Equal(reporter2.Config)) - Ω(reporter1.BeforeSuiteSummary).Should(Equal(reporter2.BeforeSuiteSummary)) - Ω(reporter1.BeginSummary).Should(Equal(reporter2.BeginSummary)) - Ω(reporter1.SpecWillRunSummaries).Should(Equal(reporter2.SpecWillRunSummaries)) - Ω(reporter1.SpecSummaries).Should(Equal(reporter2.SpecSummaries)) - Ω(reporter1.AfterSuiteSummary).Should(Equal(reporter2.AfterSuiteSummary)) - Ω(reporter1.EndSummary).Should(Equal(reporter2.EndSummary)) - }) - - It("should not actually run anything", func() { - Ω(thingsThatRan).Should(BeEmpty()) - }) - - It("report before and after suites as passed", func() { - Ω(reporter1.BeforeSuiteSummary.State).Should(Equal(types.SpecStatePassed)) - Ω(reporter1.AfterSuiteSummary.State).Should(Equal(types.SpecStatePassed)) - }) - - It("should report specs as passed", func() { - summaries := reporter1.SpecSummaries - Ω(summaries).Should(HaveLen(6)) - Ω(summaries[0].ComponentTexts).Should(ContainElement("spec A")) - Ω(summaries[0].State).Should(Equal(types.SpecStatePassed)) - Ω(summaries[1].ComponentTexts).Should(ContainElement("pending spec")) - Ω(summaries[1].State).Should(Equal(types.SpecStatePending)) - Ω(summaries[2].ComponentTexts).Should(ContainElement("flaky spec")) - Ω(summaries[2].State).Should(Equal(types.SpecStatePassed)) - Ω(summaries[3].ComponentTexts).Should(ContainElement("failed spec")) - Ω(summaries[3].State).Should(Equal(types.SpecStatePassed)) - Ω(summaries[4].ComponentTexts).Should(ContainElement("spec B")) - Ω(summaries[4].State).Should(Equal(types.SpecStatePassed)) - Ω(summaries[5].ComponentTexts).Should(ContainElement("skipped spec")) - Ω(summaries[5].State).Should(Equal(types.SpecStateSkipped)) - }) - - It("should report the end of the suite", func() { - Ω(reporter1.EndSummary.SuiteDescription).Should(Equal("description")) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeTrue()) - Ω(reporter1.EndSummary.SuiteID).Should(MatchRegexp("[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}")) - Ω(reporter1.EndSummary.NumberOfSpecsBeforeParallelization).Should(Equal(6)) - Ω(reporter1.EndSummary.NumberOfTotalSpecs).Should(Equal(6)) - Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(4)) - Ω(reporter1.EndSummary.NumberOfPendingSpecs).Should(Equal(1)) - Ω(reporter1.EndSummary.NumberOfSkippedSpecs).Should(Equal(1)) - Ω(reporter1.EndSummary.NumberOfPassedSpecs).Should(Equal(0)) - Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(0)) - }) - }) - }) - - Describe("Running BeforeSuite & AfterSuite", func() { - var success bool - var befSuite leafnodes.SuiteNode - var aftSuite leafnodes.SuiteNode - Context("with a nil BeforeSuite & AfterSuite", func() { - BeforeEach(func() { - runner = newRunner( - config.GinkgoConfigType{}, - nil, - nil, - newSpec("A", noneFlag, false), - newSpec("B", noneFlag, false), - ) - success = runner.Run() - }) - - It("should not report about the BeforeSuite", func() { - Ω(reporter1.BeforeSuiteSummary).Should(BeNil()) - }) - - It("should not report about the AfterSuite", func() { - Ω(reporter1.AfterSuiteSummary).Should(BeNil()) - }) - - It("should run the specs", func() { - Ω(thingsThatRan).Should(Equal([]string{"A", "B"})) - }) - }) - - Context("when the BeforeSuite & AfterSuite pass", func() { - BeforeEach(func() { - befSuite = newBefSuite("BefSuite", false) - aftSuite = newBefSuite("AftSuite", false) - runner = newRunner( - config.GinkgoConfigType{}, - befSuite, - aftSuite, - newSpec("A", noneFlag, false), - newSpec("B", noneFlag, false), - ) - success = runner.Run() - }) - - It("should run the BeforeSuite, the AfterSuite and the specs", func() { - Ω(thingsThatRan).Should(Equal([]string{"BefSuite", "A", "B", "AftSuite"})) - }) - - It("should report about the BeforeSuite", func() { - Ω(reporter1.BeforeSuiteSummary).Should(Equal(befSuite.Summary())) - }) - - It("should report about the AfterSuite", func() { - Ω(reporter1.AfterSuiteSummary).Should(Equal(aftSuite.Summary())) - }) - - It("should report success", func() { - Ω(success).Should(BeTrue()) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeTrue()) - Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(0)) - }) - - It("should not dump the writer", func() { - Ω(writer.EventStream).ShouldNot(ContainElement("DUMP")) - }) - }) - - Context("when the BeforeSuite fails", func() { - BeforeEach(func() { - befSuite = newBefSuite("BefSuite", true) - aftSuite = newBefSuite("AftSuite", false) - - skipped := newSpec("Skipped", noneFlag, false) - skipped.Skip() - - runner = newRunner( - config.GinkgoConfigType{}, - befSuite, - aftSuite, - newSpec("A", noneFlag, false), - newSpec("B", noneFlag, false), - newSpec("Pending", pendingFlag, false), - skipped, - ) - success = runner.Run() - }) - - It("should not run the specs, but it should run the AfterSuite", func() { - Ω(thingsThatRan).Should(Equal([]string{"BefSuite", "AftSuite"})) - }) - - It("should report about the BeforeSuite", func() { - Ω(reporter1.BeforeSuiteSummary).Should(Equal(befSuite.Summary())) - }) - - It("should report about the AfterSuite", func() { - Ω(reporter1.AfterSuiteSummary).Should(Equal(aftSuite.Summary())) - }) - - It("should report failure", func() { - Ω(success).Should(BeFalse()) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) - Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(2)) - Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(2)) - }) - - It("should dump the writer", func() { - Ω(writer.EventStream).Should(ContainElement("DUMP")) - }) - }) - - Context("when some other test fails", func() { - BeforeEach(func() { - aftSuite = newBefSuite("AftSuite", false) - - runner = newRunner( - config.GinkgoConfigType{}, - nil, - aftSuite, - newSpec("A", noneFlag, true), - ) - success = runner.Run() - }) - - It("should still run the AfterSuite", func() { - Ω(thingsThatRan).Should(Equal([]string{"A", "AftSuite"})) - }) - - It("should report about the AfterSuite", func() { - Ω(reporter1.AfterSuiteSummary).Should(Equal(aftSuite.Summary())) - }) - - It("should report failure", func() { - Ω(success).Should(BeFalse()) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) - Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(1)) - Ω(reporter1.EndSummary.NumberOfSpecsThatWillBeRun).Should(Equal(1)) - }) - }) - - Context("when the AfterSuite fails", func() { - BeforeEach(func() { - befSuite = newBefSuite("BefSuite", false) - aftSuite = newBefSuite("AftSuite", true) - runner = newRunner( - config.GinkgoConfigType{}, - befSuite, - aftSuite, - newSpec("A", noneFlag, false), - newSpec("B", noneFlag, false), - ) - success = runner.Run() - }) - - It("should run everything", func() { - Ω(thingsThatRan).Should(Equal([]string{"BefSuite", "A", "B", "AftSuite"})) - }) - - It("should report about the BeforeSuite", func() { - Ω(reporter1.BeforeSuiteSummary).Should(Equal(befSuite.Summary())) - }) - - It("should report about the AfterSuite", func() { - Ω(reporter1.AfterSuiteSummary).Should(Equal(aftSuite.Summary())) - }) - - It("should report failure", func() { - Ω(success).Should(BeFalse()) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) - Ω(reporter1.EndSummary.NumberOfFailedSpecs).Should(Equal(0)) - }) - - It("should dump the writer", func() { - Ω(writer.EventStream).Should(ContainElement("DUMP")) - }) - }) - }) - - Describe("When instructed to fail fast", func() { - BeforeEach(func() { - conf := config.GinkgoConfigType{ - FailFast: true, - } - runner = newRunner(conf, nil, newAftSuite("after-suite", false), newSpec("passing", noneFlag, false), newSpec("failing", noneFlag, true), newSpec("dont-see", noneFlag, true), newSpec("dont-see", noneFlag, true)) - }) - - It("should return false, report failure, and not run anything past the failing test", func() { - Ω(runner.Run()).Should(BeFalse()) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) - Ω(thingsThatRan).Should(Equal([]string{"passing", "failing", "after-suite"})) - }) - - It("should announce the subsequent specs as skipped", func() { - runner.Run() - Ω(reporter1.SpecSummaries).Should(HaveLen(4)) - Ω(reporter1.SpecSummaries[2].State).Should(Equal(types.SpecStateSkipped)) - Ω(reporter1.SpecSummaries[3].State).Should(Equal(types.SpecStateSkipped)) - }) - - It("should mark all subsequent specs as skipped", func() { - runner.Run() - Ω(reporter1.EndSummary.NumberOfSkippedSpecs).Should(Equal(2)) - }) - }) - - Describe("Marking failure and success", func() { - Context("when all tests pass", func() { - BeforeEach(func() { - runner = newRunner(config.GinkgoConfigType{}, nil, nil, newSpec("passing", noneFlag, false), newSpec("pending", pendingFlag, false)) - }) - - It("should return true and report success", func() { - Ω(runner.Run()).Should(BeTrue()) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeTrue()) - }) - }) - - Context("when a test fails", func() { - BeforeEach(func() { - runner = newRunner(config.GinkgoConfigType{}, nil, nil, newSpec("failing", noneFlag, true), newSpec("pending", pendingFlag, false)) - }) - - It("should return false and report failure", func() { - Ω(runner.Run()).Should(BeFalse()) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) - }) - }) - - Context("when there is a pending test, but pendings count as failures", func() { - BeforeEach(func() { - runner = newRunner(config.GinkgoConfigType{FailOnPending: true}, nil, nil, newSpec("passing", noneFlag, false), newSpec("pending", pendingFlag, false)) - }) - - It("should return false and report failure", func() { - Ω(runner.Run()).Should(BeFalse()) - Ω(reporter1.EndSummary.SuiteSucceeded).Should(BeFalse()) - }) - }) - }) - - Describe("Managing the writer", func() { - BeforeEach(func() { - runner = newRunner( - config.GinkgoConfigType{}, - nil, - nil, - newSpec("A", noneFlag, false), - newSpec("B", noneFlag, true), - newSpec("C", noneFlag, false), - ) - reporter1.SpecWillRunStub = func(specSummary *types.SpecSummary) { - writer.AddEvent("R1.WillRun") - } - reporter2.SpecWillRunStub = func(specSummary *types.SpecSummary) { - writer.AddEvent("R2.WillRun") - } - reporter1.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { - writer.AddEvent("R1.DidComplete") - } - reporter2.SpecDidCompleteStub = func(specSummary *types.SpecSummary) { - writer.AddEvent("R2.DidComplete") - } - runner.Run() - }) - - It("should truncate between tests, but only dump if a test fails", func() { - Ω(writer.EventStream).Should(Equal([]string{ - "TRUNCATE", - "R1.WillRun", - "R2.WillRun", - "A", - "BYTES", - "R2.DidComplete", - "R1.DidComplete", - "TRUNCATE", - "R1.WillRun", - "R2.WillRun", - "B", - "BYTES", - "R2.DidComplete", - "DUMP", - "R1.DidComplete", - "TRUNCATE", - "R1.WillRun", - "R2.WillRun", - "C", - "BYTES", - "R2.DidComplete", - "R1.DidComplete", - })) - }) - }) - - Describe("CurrentSpecSummary", func() { - It("should return the spec summary for the currently running spec", func() { - var summary *types.SpecSummary - runner = newRunner( - config.GinkgoConfigType{}, - nil, - nil, - newSpec("A", noneFlag, false), - newSpecWithBody("B", func() { - var ok bool - summary, ok = runner.CurrentSpecSummary() - Ω(ok).Should(BeTrue()) - }), - newSpec("C", noneFlag, false), - ) - runner.Run() - - Ω(summary.ComponentTexts).Should(Equal([]string{"B"})) - - summary, ok := runner.CurrentSpecSummary() - Ω(summary).Should(BeNil()) - Ω(ok).Should(BeFalse()) - }) - }) - - Describe("generating a suite id", func() { - It("should generate an id randomly", func() { - runnerA := newRunner(config.GinkgoConfigType{}, nil, nil) - runnerA.Run() - IDA := reporter1.BeginSummary.SuiteID - - runnerB := newRunner(config.GinkgoConfigType{}, nil, nil) - runnerB.Run() - IDB := reporter1.BeginSummary.SuiteID - - IDRegexp := "[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}" - Ω(IDA).Should(MatchRegexp(IDRegexp)) - Ω(IDB).Should(MatchRegexp(IDRegexp)) - - Ω(IDA).ShouldNot(Equal(IDB)) - }) - }) -}) diff --git a/internal/suite.go b/internal/suite.go new file mode 100644 index 0000000000..d41ea8cd1d --- /dev/null +++ b/internal/suite.go @@ -0,0 +1,396 @@ +package internal + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" +) + +type Phase uint + +const ( + PhaseBuildTopLevel Phase = iota + PhaseBuildTree + PhaseRun +) + +type Suite struct { + tree TreeNode + topLevelContainers Nodes + + phase Phase + + beforeSuiteNodeBuilder SuiteNodeBuilder + afterSuiteNodeBuilder SuiteNodeBuilder + + currentSpecSummary types.Summary +} + +func NewSuite() *Suite { + return &Suite{ + phase: PhaseBuildTopLevel, + } +} + +func (suite *Suite) BuildTree() error { + // During PhaseBuildTopLevel, the top level containers are stored in suite.topLevelCotainers and entered + // We now enter PhaseBuildTree where these top level containers are entered and added to the spec tree + suite.phase = PhaseBuildTree + for _, topLevelContainer := range suite.topLevelContainers { + err := suite.PushNode(topLevelContainer) + if err != nil { + return err + } + } + return nil +} + +func (suite *Suite) Run(description string, failer *Failer, reporter reporters.Reporter, writer WriterInterface, interruptHandler InterruptHandlerInterface, config config.GinkgoConfigType) (bool, bool) { + if suite.phase != PhaseBuildTree { + panic("cannot run before building the tree = call suite.BuildTree() first") + } + tree := ApplyNestedFocusPolicyToTree(suite.tree) + specs := GenerateSpecsFromTreeRoot(tree) + specs = ShuffleSpecs(specs, config) + specs, hasProgrammaticFocus := ApplyFocusToSpecs(specs, description, config) + + suite.phase = PhaseRun + if interruptHandler == nil { + interruptHandler = NewInterruptHandler() + } + success := suite.runSpecs(description, specs, failer, reporter, writer, interruptHandler, config) + return success, hasProgrammaticFocus +} + +/* + Tree Construction methods + + PushNode and PushSuiteNodeBuilder are used during PhaseBuildTopLevel and PhaseBuildTree +*/ + +func (suite *Suite) PushNode(node Node) error { + if suite.phase == PhaseRun { + return types.GinkgoErrors.PushingNodeInRunPhase(node.NodeType, node.CodeLocation) + } + + if node.NodeType == types.NodeTypeContainer { + // During PhaseBuildTopLevel we only track the top level containers without entering them + // We only enter the top level container nodes during PhaseBuildTree + // + // This ensures the tree is only constructed after `go spec` has called `flag.Parse()` and gives + // the user an opportunity to load configuration information in the `TestX` go spec hook just before `RunSpecs` + // is invoked. This makes the lifecycle easier to reason about and solves issues like #693. + if suite.phase == PhaseBuildTopLevel { + suite.topLevelContainers = append(suite.topLevelContainers, node) + return nil + } + if suite.phase == PhaseBuildTree { + parentTree := suite.tree + suite.tree = TreeNode{Node: node} + err := func() (err error) { + defer func() { + if e := recover(); e != nil { + err = types.GinkgoErrors.CaughtPanicDuringABuildPhase(node.CodeLocation) + } + }() + node.Body() + return err + }() + suite.tree = AppendTreeNodeChild(parentTree, suite.tree) + return err + } + } else { + suite.tree = AppendTreeNodeChild(suite.tree, TreeNode{Node: node}) + return nil + } + + return nil +} + +func (suite *Suite) PushSuiteNodeBuilder(nodeBuilder SuiteNodeBuilder) error { + if suite.phase == PhaseBuildTree { + return types.GinkgoErrors.SetupNodeInNestedContext(nodeBuilder.NodeType, nodeBuilder.CodeLocation) + } + + if suite.phase == PhaseRun { + return types.GinkgoErrors.SetupNodeDuringRunPhase(nodeBuilder.NodeType, nodeBuilder.CodeLocation) + } + + switch nodeBuilder.NodeType { + case types.NodeTypeBeforeSuite, types.NodeTypeSynchronizedBeforeSuite: + if suite.beforeSuiteNodeBuilder.NodeType != types.NodeTypeInvalid { + return types.GinkgoErrors.MultipleBeforeSuiteNodes(nodeBuilder.NodeType, nodeBuilder.CodeLocation, suite.beforeSuiteNodeBuilder.NodeType, suite.beforeSuiteNodeBuilder.CodeLocation) + } + suite.beforeSuiteNodeBuilder = nodeBuilder + case types.NodeTypeAfterSuite, types.NodeTypeSynchronizedAfterSuite: + if suite.afterSuiteNodeBuilder.NodeType != types.NodeTypeInvalid { + return types.GinkgoErrors.MultipleAfterSuiteNodes(nodeBuilder.NodeType, nodeBuilder.CodeLocation, suite.beforeSuiteNodeBuilder.NodeType, suite.beforeSuiteNodeBuilder.CodeLocation) + } + suite.afterSuiteNodeBuilder = nodeBuilder + default: + panic("invalid configuration of SuiteNodeBuilder") + } + + return nil +} + +/* + Spec Running methods - used during PhaseRun +*/ +func (suite *Suite) CurrentSpecSummary() (types.Summary, bool) { + if suite.currentSpecSummary.State == types.SpecStateInvalid { + return types.Summary{}, false + } + return suite.currentSpecSummary, true +} + +func (suite *Suite) runSpecs(description string, specs Specs, failer *Failer, reporter reporters.Reporter, writer WriterInterface, interruptHandler InterruptHandlerInterface, config config.GinkgoConfigType) bool { + suiteStartTime := time.Now() + + beforeSuiteNode := suite.beforeSuiteNodeBuilder.BuildNode(config, failer) + afterSuiteNode := suite.afterSuiteNodeBuilder.BuildNode(config, failer) + + numSpecsThatWillBeRun := specs.CountWithoutSkip() + suiteSummary := types.SuiteSummary{ + SuiteDescription: description, + NumberOfTotalSpecs: len(specs), + NumberOfSpecsThatWillBeRun: numSpecsThatWillBeRun, + } + + reporter.SpecSuiteWillBegin(config, suiteSummary) + + suitePassed := true + + interruptStatus := interruptHandler.Status() + if !beforeSuiteNode.IsZero() && !interruptStatus.Interrupted && numSpecsThatWillBeRun > 0 { + summary := types.Summary{LeafNodeType: beforeSuiteNode.NodeType, LeafNodeLocation: beforeSuiteNode.CodeLocation} + reporter.WillRun(summary) + + summary = suite.runSuiteNode(summary, beforeSuiteNode, failer, interruptStatus.Channel, writer, config) + reporter.DidRun(summary) + + if summary.State != types.SpecStatePassed { + suitePassed = false + } + } + + if suitePassed { + nextIndex := MakeNextIndexCounter(config) + + for { + idx, err := nextIndex() + if err != nil { + fmt.Println("failed to iterate over specs:\n" + err.Error()) + suitePassed = false + break + } + if idx >= len(specs) { + break + } + + spec := specs[idx] + + suite.currentSpecSummary = types.Summary{ + NodeTexts: spec.Nodes.WithType(types.NodeTypesForContainerAndIt...).Texts(), + NodeLocations: spec.Nodes.WithType(types.NodeTypesForContainerAndIt...).CodeLocations(), + LeafNodeLocation: spec.FirstNodeWithType(types.NodeTypeIt).CodeLocation, + LeafNodeType: types.NodeTypeIt, + } + + if (config.FailFast && !suitePassed) || interruptHandler.Status().Interrupted { + spec.Skip = true + } + + if spec.Skip { + suite.currentSpecSummary.State = types.SpecStateSkipped + if spec.Nodes.HasNodeMarkedPending() { + suite.currentSpecSummary.State = types.SpecStatePending + } + } + + reporter.WillRun(suite.currentSpecSummary) + + if !spec.Skip { + //runSpec updates suite.currentSpecSummary directly + suite.runSpec(spec, failer, interruptHandler, writer, config) + } + + reporter.DidRun(suite.currentSpecSummary) + + switch suite.currentSpecSummary.State { + case types.SpecStatePassed: + suiteSummary.NumberOfPassedSpecs += 1 + if suite.currentSpecSummary.NumAttempts > 1 { + suiteSummary.NumberOfFlakedSpecs += 1 + } + case types.SpecStateFailed, types.SpecStatePanicked, types.SpecStateInterrupted: + suitePassed = false + suiteSummary.NumberOfFailedSpecs += 1 + case types.SpecStatePending: + suiteSummary.NumberOfPendingSpecs += 1 + suiteSummary.NumberOfSkippedSpecs += 1 + case types.SpecStateSkipped: + suiteSummary.NumberOfSkippedSpecs += 1 + default: + } + + suite.currentSpecSummary = types.Summary{} + } + + if specs.HasAnySpecsMarkedPending() && config.FailOnPending { + suitePassed = false + } + } else { + suiteSummary.NumberOfSkippedSpecs = len(specs) + } + + if !afterSuiteNode.IsZero() && numSpecsThatWillBeRun > 0 { + summary := types.Summary{LeafNodeType: afterSuiteNode.NodeType, LeafNodeLocation: afterSuiteNode.CodeLocation} + reporter.WillRun(summary) + + summary = suite.runSuiteNode(summary, afterSuiteNode, failer, interruptHandler.Status().Channel, writer, config) + reporter.DidRun(summary) + if summary.State != types.SpecStatePassed { + suitePassed = false + } + } + + suiteSummary.SuiteSucceeded = suitePassed + suiteSummary.RunTime = time.Since(suiteStartTime) + reporter.SpecSuiteDidEnd(suiteSummary) + + return suitePassed +} + +// runSpec(spec) mutates currentSpecSummary. this is ugly +// but it allows the user to call CurrentGinkgoSpecDescription and get +// an up-to-date state of the spec **from within a running spec** +func (suite *Suite) runSpec(spec Spec, failer *Failer, interruptHandler InterruptHandlerInterface, writer WriterInterface, config config.GinkgoConfigType) { + if config.DryRun { + suite.currentSpecSummary.State = types.SpecStatePassed + return + } + + writer.Truncate() + t := time.Now() + maxAttempts := max(1, config.FlakeAttempts) + + for attempt := 0; attempt < maxAttempts; attempt++ { + suite.currentSpecSummary.NumAttempts = attempt + 1 + + if attempt > 0 { + fmt.Fprintf(writer, "\nGinkgo: Attempt #%d Failed. Retrying...\n", attempt) + } + + interruptStatus := interruptHandler.Status() + deepestNestingLevelAttained := -1 + nodes := spec.Nodes.WithType(types.NodeTypeBeforeEach).CopyAppend(spec.Nodes.WithType(types.NodeTypeJustBeforeEach)...).CopyAppend(spec.Nodes.WithType(types.NodeTypeIt)...) + for _, node := range nodes { + deepestNestingLevelAttained = max(deepestNestingLevelAttained, node.NestingLevel) + suite.currentSpecSummary.State, suite.currentSpecSummary.Failure = suite.runNode(node, failer, interruptStatus.Channel, spec.Nodes.BestTextFor(node), writer, config) + suite.currentSpecSummary.RunTime = time.Since(t) + if suite.currentSpecSummary.State != types.SpecStatePassed { + break + } + } + + cleanUpNodes := spec.Nodes.WithType(types.NodeTypeJustAfterEach).SortedByDescendingNestingLevel() + cleanUpNodes = cleanUpNodes.CopyAppend(spec.Nodes.WithType(types.NodeTypeAfterEach).SortedByDescendingNestingLevel()...) + cleanUpNodes = cleanUpNodes.WithinNestingLevel(deepestNestingLevelAttained) + for _, node := range cleanUpNodes { + state, failure := suite.runNode(node, failer, interruptHandler.Status().Channel, spec.Nodes.BestTextFor(node), writer, config) + suite.currentSpecSummary.RunTime = time.Since(t) + if suite.currentSpecSummary.State == types.SpecStatePassed { + suite.currentSpecSummary.State = state + suite.currentSpecSummary.Failure = failure + } + } + + suite.currentSpecSummary.RunTime = time.Since(t) + suite.currentSpecSummary.CapturedGinkgoWriterOutput = string(writer.Bytes()) + + if suite.currentSpecSummary.State == types.SpecStatePassed { + return + } + if interruptHandler.Status().Interrupted { + return + } + } +} + +func (suite *Suite) runNode(node Node, failer *Failer, interruptChannel chan interface{}, text string, writer WriterInterface, config config.GinkgoConfigType) (types.SpecState, types.Failure) { + if config.EmitSpecProgress { + if text == "" { + text = "TOP-LEVEL" + } + s := fmt.Sprintf("[%s] %s\n %s\n", node.NodeType.String(), text, node.CodeLocation.String()) + writer.Write([]byte(s)) + } + + failureNestingLevel := node.NestingLevel - 1 + if node.NodeType.Is(types.NodeTypeIt) { + failureNestingLevel = node.NestingLevel + } + + outcomeC := make(chan types.SpecState) + failureC := make(chan types.Failure) + + go func() { + finished := false + defer func() { + if e := recover(); e != nil || !finished { + failer.Panic(types.NewCodeLocation(2), e) + } + + outcome, failure := failer.Drain() + if outcome != types.SpecStatePassed { + failure.NodeType = node.NodeType + failure.NodeIndex = failureNestingLevel + } + + outcomeC <- outcome + failureC <- failure + }() + + node.Body() + finished = true + }() + + select { + case outcome := <-outcomeC: + failure := <-failureC + return outcome, failure + case <-interruptChannel: + return types.SpecStateInterrupted, types.Failure{ + Message: "interrupted by user", + Location: node.CodeLocation, + NodeType: node.NodeType, + NodeIndex: failureNestingLevel, + } + } +} + +func (suite *Suite) runSuiteNode(summary types.Summary, node Node, failer *Failer, interruptChannel chan interface{}, writer WriterInterface, config config.GinkgoConfigType) types.Summary { + if config.DryRun { + summary.State = types.SpecStatePassed + return summary + } + + writer.Truncate() + t := time.Now() + summary.State, summary.Failure = suite.runNode(node, failer, interruptChannel, "", writer, config) + summary.RunTime = time.Since(t) + summary.CapturedGinkgoWriterOutput = string(writer.Bytes()) + + return summary +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/internal/suite/suite.go b/internal/suite/suite.go deleted file mode 100644 index b4a83c432d..0000000000 --- a/internal/suite/suite.go +++ /dev/null @@ -1,227 +0,0 @@ -package suite - -import ( - "math/rand" - "net/http" - "time" - - "github.com/onsi/ginkgo/internal/spec_iterator" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/internal/containernode" - "github.com/onsi/ginkgo/internal/failer" - "github.com/onsi/ginkgo/internal/leafnodes" - "github.com/onsi/ginkgo/internal/spec" - "github.com/onsi/ginkgo/internal/specrunner" - "github.com/onsi/ginkgo/internal/writer" - "github.com/onsi/ginkgo/reporters" - "github.com/onsi/ginkgo/types" -) - -type ginkgoTestingT interface { - Fail() -} - -type deferredContainerNode struct { - text string - body func() - flag types.FlagType - codeLocation types.CodeLocation -} - -type Suite struct { - topLevelContainer *containernode.ContainerNode - currentContainer *containernode.ContainerNode - - deferredContainerNodes []deferredContainerNode - - containerIndex int - beforeSuiteNode leafnodes.SuiteNode - afterSuiteNode leafnodes.SuiteNode - runner *specrunner.SpecRunner - failer *failer.Failer - running bool - expandTopLevelNodes bool -} - -func New(failer *failer.Failer) *Suite { - topLevelContainer := containernode.New("[Top Level]", types.FlagTypeNone, types.CodeLocation{}) - - return &Suite{ - topLevelContainer: topLevelContainer, - currentContainer: topLevelContainer, - failer: failer, - containerIndex: 1, - deferredContainerNodes: []deferredContainerNode{}, - } -} - -func (suite *Suite) Run(t ginkgoTestingT, description string, reporters []reporters.Reporter, writer writer.WriterInterface, config config.GinkgoConfigType) (bool, bool) { - if config.ParallelTotal < 1 { - panic("ginkgo.parallel.total must be >= 1") - } - - if config.ParallelNode > config.ParallelTotal || config.ParallelNode < 1 { - panic("ginkgo.parallel.node is one-indexed and must be <= ginkgo.parallel.total") - } - - suite.expandTopLevelNodes = true - for _, deferredNode := range suite.deferredContainerNodes { - suite.PushContainerNode(deferredNode.text, deferredNode.body, deferredNode.flag, deferredNode.codeLocation) - } - - r := rand.New(rand.NewSource(config.RandomSeed)) - suite.topLevelContainer.Shuffle(r) - iterator, hasProgrammaticFocus := suite.generateSpecsIterator(description, config) - suite.runner = specrunner.New(description, suite.beforeSuiteNode, iterator, suite.afterSuiteNode, reporters, writer, config) - - suite.running = true - success := suite.runner.Run() - if !success { - t.Fail() - } - return success, hasProgrammaticFocus -} - -func (suite *Suite) generateSpecsIterator(description string, config config.GinkgoConfigType) (spec_iterator.SpecIterator, bool) { - specsSlice := []*spec.Spec{} - suite.topLevelContainer.BackPropagateProgrammaticFocus() - for _, collatedNodes := range suite.topLevelContainer.Collate() { - specsSlice = append(specsSlice, spec.New(collatedNodes.Subject, collatedNodes.Containers, config.EmitSpecProgress)) - } - - specs := spec.NewSpecs(specsSlice) - specs.RegexScansFilePath = config.RegexScansFilePath - - if config.RandomizeAllSpecs { - specs.Shuffle(rand.New(rand.NewSource(config.RandomSeed))) - } - - specs.ApplyFocus(description, config.FocusStrings, config.SkipStrings) - - if config.SkipMeasurements { - specs.SkipMeasurements() - } - - var iterator spec_iterator.SpecIterator - - if config.ParallelTotal > 1 { - iterator = spec_iterator.NewParallelIterator(specs.Specs(), config.SyncHost) - resp, err := http.Get(config.SyncHost + "/has-counter") - if err != nil || resp.StatusCode != http.StatusOK { - iterator = spec_iterator.NewShardedParallelIterator(specs.Specs(), config.ParallelTotal, config.ParallelNode) - } - } else { - iterator = spec_iterator.NewSerialIterator(specs.Specs()) - } - - return iterator, specs.HasProgrammaticFocus() -} - -func (suite *Suite) CurrentRunningSpecSummary() (*types.SpecSummary, bool) { - if !suite.running { - return nil, false - } - return suite.runner.CurrentSpecSummary() -} - -func (suite *Suite) SetBeforeSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { - if suite.beforeSuiteNode != nil { - panic("You may only call BeforeSuite once!") - } - suite.beforeSuiteNode = leafnodes.NewBeforeSuiteNode(body, codeLocation, timeout, suite.failer) -} - -func (suite *Suite) SetAfterSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { - if suite.afterSuiteNode != nil { - panic("You may only call AfterSuite once!") - } - suite.afterSuiteNode = leafnodes.NewAfterSuiteNode(body, codeLocation, timeout, suite.failer) -} - -func (suite *Suite) SetSynchronizedBeforeSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration) { - if suite.beforeSuiteNode != nil { - panic("You may only call BeforeSuite once!") - } - suite.beforeSuiteNode = leafnodes.NewSynchronizedBeforeSuiteNode(bodyA, bodyB, codeLocation, timeout, suite.failer) -} - -func (suite *Suite) SetSynchronizedAfterSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration) { - if suite.afterSuiteNode != nil { - panic("You may only call AfterSuite once!") - } - suite.afterSuiteNode = leafnodes.NewSynchronizedAfterSuiteNode(bodyA, bodyB, codeLocation, timeout, suite.failer) -} - -func (suite *Suite) PushContainerNode(text string, body func(), flag types.FlagType, codeLocation types.CodeLocation) { - /* - We defer walking the container nodes (which immediately evaluates the `body` function) - until `RunSpecs` is called. We do this by storing off the deferred container nodes. Then, when - `RunSpecs` is called we actually go through and add the container nodes to the test structure. - - This allows us to defer calling all the `body` functions until _after_ the top level functions - have been walked, _after_ func init()s have been called, and _after_ `go test` has called `flag.Parse()`. - - This allows users to load up configuration information in the `TestX` go test hook just before `RunSpecs` - is invoked and solves issues like #693 and makes the lifecycle easier to reason about. - - */ - if !suite.expandTopLevelNodes { - suite.deferredContainerNodes = append(suite.deferredContainerNodes, deferredContainerNode{text, body, flag, codeLocation}) - return - } - - container := containernode.New(text, flag, codeLocation) - suite.currentContainer.PushContainerNode(container) - - previousContainer := suite.currentContainer - suite.currentContainer = container - suite.containerIndex++ - - body() - - suite.containerIndex-- - suite.currentContainer = previousContainer -} - -func (suite *Suite) PushItNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, timeout time.Duration) { - if suite.running { - suite.failer.Fail("You may only call It from within a Describe, Context or When", codeLocation) - } - suite.currentContainer.PushSubjectNode(leafnodes.NewItNode(text, body, flag, codeLocation, timeout, suite.failer, suite.containerIndex)) -} - -func (suite *Suite) PushMeasureNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, samples int) { - if suite.running { - suite.failer.Fail("You may only call Measure from within a Describe, Context or When", codeLocation) - } - suite.currentContainer.PushSubjectNode(leafnodes.NewMeasureNode(text, body, flag, codeLocation, samples, suite.failer, suite.containerIndex)) -} - -func (suite *Suite) PushBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { - if suite.running { - suite.failer.Fail("You may only call BeforeEach from within a Describe, Context or When", codeLocation) - } - suite.currentContainer.PushSetupNode(leafnodes.NewBeforeEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) -} - -func (suite *Suite) PushJustBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { - if suite.running { - suite.failer.Fail("You may only call JustBeforeEach from within a Describe, Context or When", codeLocation) - } - suite.currentContainer.PushSetupNode(leafnodes.NewJustBeforeEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) -} - -func (suite *Suite) PushJustAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { - if suite.running { - suite.failer.Fail("You may only call JustAfterEach from within a Describe or Context", codeLocation) - } - suite.currentContainer.PushSetupNode(leafnodes.NewJustAfterEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) -} - -func (suite *Suite) PushAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) { - if suite.running { - suite.failer.Fail("You may only call AfterEach from within a Describe, Context or When", codeLocation) - } - suite.currentContainer.PushSetupNode(leafnodes.NewAfterEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex)) -} diff --git a/internal/suite/suite_suite_test.go b/internal/suite/suite_suite_test.go deleted file mode 100644 index ff69741f0d..0000000000 --- a/internal/suite/suite_suite_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package suite_test - -import ( - "fmt" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -var dynamicallyGeneratedTests = []string{} - -func Test(t *testing.T) { - RegisterFailHandler(Fail) - dynamicallyGeneratedTests = []string{"Test A", "Test B", "Test C"} - RunSpecs(t, "Suite") -} - -var numBeforeSuiteRuns = 0 -var numAfterSuiteRuns = 0 -var numDynamicallyGeneratedTests = 0 - -var _ = BeforeSuite(func() { - numBeforeSuiteRuns++ -}) - -var _ = AfterSuite(func() { - numAfterSuiteRuns++ - Ω(numBeforeSuiteRuns).Should(Equal(1)) - Ω(numAfterSuiteRuns).Should(Equal(1)) - Ω(numDynamicallyGeneratedTests).Should(Equal(3), "Expected three test to be dynamically generated") -}) - -var _ = Describe("Top-level cotnainer node lifecycle", func() { - for _, test := range dynamicallyGeneratedTests { - numDynamicallyGeneratedTests += 1 - It(fmt.Sprintf("runs dynamically generated test: %s", test), func() { - Ω(true).Should(BeTrue()) - }) - } -}) - -//Fakes -type fakeTestingT struct { - didFail bool -} - -func (fakeT *fakeTestingT) Fail() { - fakeT.didFail = true -} diff --git a/internal/suite/suite_test.go b/internal/suite/suite_test.go deleted file mode 100644 index 74cb116e66..0000000000 --- a/internal/suite/suite_test.go +++ /dev/null @@ -1,385 +0,0 @@ -package suite_test - -import ( - "bytes" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/suite" - . "github.com/onsi/gomega" - - "math/rand" - "time" - - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/internal/codelocation" - Failer "github.com/onsi/ginkgo/internal/failer" - Writer "github.com/onsi/ginkgo/internal/writer" - "github.com/onsi/ginkgo/reporters" - "github.com/onsi/ginkgo/types" -) - -var _ = Describe("Suite", func() { - var ( - specSuite *Suite - fakeT *fakeTestingT - fakeR *reporters.FakeReporter - writer *Writer.FakeGinkgoWriter - failer *Failer.Failer - ) - - BeforeEach(func() { - writer = Writer.NewFake() - fakeT = &fakeTestingT{} - fakeR = reporters.NewFakeReporter() - failer = Failer.New() - specSuite = New(failer) - }) - - Describe("running a suite", func() { - var ( - runOrder []string - randomizeAllSpecs bool - randomSeed int64 - focusStrings []string - parallelNode int - parallelTotal int - runResult bool - hasProgrammaticFocus bool - ) - - var f = func(runText string) func() { - return func() { - runOrder = append(runOrder, runText) - } - } - - BeforeEach(func() { - randomizeAllSpecs = false - randomSeed = 11 - parallelNode = 1 - parallelTotal = 1 - focusStrings = []string{} - - runOrder = make([]string, 0) - specSuite.SetBeforeSuiteNode(f("BeforeSuite"), codelocation.New(0), 0) - specSuite.PushBeforeEachNode(f("top BE"), codelocation.New(0), 0) - specSuite.PushJustBeforeEachNode(f("top JBE"), codelocation.New(0), 0) - specSuite.PushAfterEachNode(f("top AE"), codelocation.New(0), 0) - - specSuite.PushContainerNode("container", func() { - specSuite.PushBeforeEachNode(f("BE"), codelocation.New(0), 0) - specSuite.PushJustBeforeEachNode(f("JBE"), codelocation.New(0), 0) - specSuite.PushAfterEachNode(f("AE"), codelocation.New(0), 0) - specSuite.PushItNode("it", f("IT"), types.FlagTypeNone, codelocation.New(0), 0) - - specSuite.PushContainerNode("inner container", func() { - specSuite.PushItNode("inner it", f("inner IT"), types.FlagTypeNone, codelocation.New(0), 0) - }, types.FlagTypeNone, codelocation.New(0)) - }, types.FlagTypeNone, codelocation.New(0)) - - specSuite.PushContainerNode("container 2", func() { - specSuite.PushBeforeEachNode(f("BE 2"), codelocation.New(0), 0) - specSuite.PushItNode("it 2", f("IT 2"), types.FlagTypeNone, codelocation.New(0), 0) - }, types.FlagTypeNone, codelocation.New(0)) - - specSuite.PushItNode("top level it", f("top IT"), types.FlagTypeNone, codelocation.New(0), 0) - - specSuite.SetAfterSuiteNode(f("AfterSuite"), codelocation.New(0), 0) - }) - - JustBeforeEach(func() { - runResult, hasProgrammaticFocus = specSuite.Run(fakeT, "suite description", []reporters.Reporter{fakeR}, writer, config.GinkgoConfigType{ - RandomSeed: randomSeed, - RandomizeAllSpecs: randomizeAllSpecs, - FocusStrings: focusStrings, - ParallelNode: parallelNode, - ParallelTotal: parallelTotal, - }) - }) - - It("provides the config and suite description to the reporter", func() { - Ω(fakeR.Config.RandomSeed).Should(Equal(randomSeed)) - Ω(fakeR.Config.RandomizeAllSpecs).Should(Equal(randomizeAllSpecs)) - Ω(fakeR.BeginSummary.SuiteDescription).Should(Equal("suite description")) - }) - - It("reports that the BeforeSuite node ran", func() { - Ω(fakeR.BeforeSuiteSummary).ShouldNot(BeNil()) - }) - - It("reports that the AfterSuite node ran", func() { - Ω(fakeR.AfterSuiteSummary).ShouldNot(BeNil()) - }) - - It("provides information about the current test", func() { - description := CurrentGinkgoTestDescription() - Ω(description.ComponentTexts).Should(Equal([]string{"Suite", "running a suite", "provides information about the current test"})) - Ω(description.FullTestText).Should(Equal("Suite running a suite provides information about the current test")) - Ω(description.TestText).Should(Equal("provides information about the current test")) - Ω(description.IsMeasurement).Should(BeFalse()) - Ω(description.FileName).Should(ContainSubstring("suite_test.go")) - Ω(description.LineNumber).Should(BeNumerically(">", 50)) - Ω(description.LineNumber).Should(BeNumerically("<", 150)) - Ω(description.Failed).Should(BeFalse()) - Ω(description.Duration).Should(BeNumerically(">", 0)) - }) - - Measure("should run measurements", func(b Benchmarker) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - - runtime := b.Time("sleeping", func() { - sleepTime := time.Duration(r.Float64() * 0.01 * float64(time.Second)) - time.Sleep(sleepTime) - }) - Ω(runtime.Seconds()).Should(BeNumerically("<=", 1)) - Ω(runtime.Seconds()).Should(BeNumerically(">=", 0)) - - randomValue := r.Float64() * 10.0 - b.RecordValue("random value", randomValue) - Ω(randomValue).Should(BeNumerically("<=", 10.0)) - Ω(randomValue).Should(BeNumerically(">=", 0.0)) - - b.RecordValueWithPrecision("specific value", 123.4567, "ms", 2) - b.RecordValueWithPrecision("specific value", 234.5678, "ms", 2) - }, 10) - - It("creates a node hierarchy, converts it to a spec collection, and runs it", func() { - Ω(runOrder).Should(Equal([]string{ - "BeforeSuite", - "top BE", "BE", "top JBE", "JBE", "IT", "AE", "top AE", - "top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE", - "top BE", "BE 2", "top JBE", "IT 2", "top AE", - "top BE", "top JBE", "top IT", "top AE", - "AfterSuite", - })) - }) - Context("when in an AfterEach block", func() { - AfterEach(func() { - description := CurrentGinkgoTestDescription() - Ω(description.IsMeasurement).Should(BeFalse()) - Ω(description.FileName).Should(ContainSubstring("suite_test.go")) - Ω(description.Failed).Should(BeFalse()) - Ω(description.Duration).Should(BeNumerically(">", 0)) - }) - - It("still provides information about the current test", func() { - Ω(true).To(BeTrue()) - }) - }) - - Context("when told to randomize all specs", func() { - BeforeEach(func() { - randomizeAllSpecs = true - }) - - It("does", func() { - Ω(runOrder).Should(Equal([]string{ - "BeforeSuite", - "top BE", "top JBE", "top IT", "top AE", - "top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE", - "top BE", "BE", "top JBE", "JBE", "IT", "AE", "top AE", - "top BE", "BE 2", "top JBE", "IT 2", "top AE", - "AfterSuite", - })) - }) - }) - - Context("when provided with a filter", func() { - BeforeEach(func() { - focusStrings = []string{`inner`, `\d`} - }) - - It("converts the filter to a regular expression and uses it to filter the running specs", func() { - Ω(runOrder).Should(Equal([]string{ - "BeforeSuite", - "top BE", "BE", "top JBE", "JBE", "inner IT", "AE", "top AE", - "top BE", "BE 2", "top JBE", "IT 2", "top AE", - "AfterSuite", - })) - }) - - It("should not report a programmatic focus", func() { - Ω(hasProgrammaticFocus).Should(BeFalse()) - }) - }) - - Context("with a programatically focused spec", func() { - BeforeEach(func() { - specSuite.PushItNode("focused it", f("focused it"), types.FlagTypeFocused, codelocation.New(0), 0) - - specSuite.PushContainerNode("focused container", func() { - specSuite.PushItNode("inner focused it", f("inner focused it"), types.FlagTypeFocused, codelocation.New(0), 0) - specSuite.PushItNode("inner unfocused it", f("inner unfocused it"), types.FlagTypeNone, codelocation.New(0), 0) - }, types.FlagTypeFocused, codelocation.New(0)) - - }) - - It("should only run the focused test, applying backpropagation to favor most deeply focused leaf nodes", func() { - Ω(runOrder).Should(Equal([]string{ - "BeforeSuite", - "top BE", "top JBE", "focused it", "top AE", - "top BE", "top JBE", "inner focused it", "top AE", - "AfterSuite", - })) - }) - - It("should report a programmatic focus", func() { - Ω(hasProgrammaticFocus).Should(BeTrue()) - }) - }) - - Context("when the specs pass", func() { - It("doesn't report a failure", func() { - Ω(fakeT.didFail).Should(BeFalse()) - }) - - It("should return true", func() { - Ω(runResult).Should(BeTrue()) - }) - }) - - Context("when a spec fails", func() { - var location types.CodeLocation - BeforeEach(func() { - specSuite.PushItNode("top level it", func() { - location = codelocation.New(0) - failer.Fail("oops!", location) - }, types.FlagTypeNone, codelocation.New(0), 0) - }) - - It("should return false", func() { - Ω(runResult).Should(BeFalse()) - }) - - It("reports a failure", func() { - Ω(fakeT.didFail).Should(BeTrue()) - }) - - It("generates the correct failure data", func() { - Ω(fakeR.SpecSummaries[0].Failure.Message).Should(Equal("oops!")) - Ω(fakeR.SpecSummaries[0].Failure.Location).Should(Equal(location)) - }) - }) - - Context("when runnable nodes are nested within other runnable nodes", func() { - Context("when an It is nested", func() { - BeforeEach(func() { - specSuite.PushItNode("top level it", func() { - specSuite.PushItNode("nested it", f("oops"), types.FlagTypeNone, codelocation.New(0), 0) - }, types.FlagTypeNone, codelocation.New(0), 0) - }) - - It("should fail", func() { - Ω(fakeT.didFail).Should(BeTrue()) - }) - }) - - Context("when a Measure is nested", func() { - BeforeEach(func() { - specSuite.PushItNode("top level it", func() { - specSuite.PushMeasureNode("nested measure", func(Benchmarker) {}, types.FlagTypeNone, codelocation.New(0), 10) - }, types.FlagTypeNone, codelocation.New(0), 0) - }) - - It("should fail", func() { - Ω(fakeT.didFail).Should(BeTrue()) - }) - }) - - Context("when a BeforeEach is nested", func() { - BeforeEach(func() { - specSuite.PushItNode("top level it", func() { - specSuite.PushBeforeEachNode(f("nested bef"), codelocation.New(0), 0) - }, types.FlagTypeNone, codelocation.New(0), 0) - }) - - It("should fail", func() { - Ω(fakeT.didFail).Should(BeTrue()) - }) - }) - - Context("when a JustBeforeEach is nested", func() { - BeforeEach(func() { - specSuite.PushItNode("top level it", func() { - specSuite.PushJustBeforeEachNode(f("nested jbef"), codelocation.New(0), 0) - }, types.FlagTypeNone, codelocation.New(0), 0) - }) - - It("should fail", func() { - Ω(fakeT.didFail).Should(BeTrue()) - }) - }) - - Context("when a AfterEach is nested", func() { - BeforeEach(func() { - specSuite.PushItNode("top level it", func() { - specSuite.PushAfterEachNode(f("nested aft"), codelocation.New(0), 0) - }, types.FlagTypeNone, codelocation.New(0), 0) - }) - - It("should fail", func() { - Ω(fakeT.didFail).Should(BeTrue()) - }) - }) - }) - }) - - Describe("BeforeSuite", func() { - Context("when setting BeforeSuite more than once", func() { - It("should panic", func() { - specSuite.SetBeforeSuiteNode(func() {}, codelocation.New(0), 0) - - Ω(func() { - specSuite.SetBeforeSuiteNode(func() {}, codelocation.New(0), 0) - }).Should(Panic()) - - }) - }) - }) - - Describe("AfterSuite", func() { - Context("when setting AfterSuite more than once", func() { - It("should panic", func() { - specSuite.SetAfterSuiteNode(func() {}, codelocation.New(0), 0) - - Ω(func() { - specSuite.SetAfterSuiteNode(func() {}, codelocation.New(0), 0) - }).Should(Panic()) - }) - }) - }) - - Describe("By", func() { - It("writes to the GinkgoWriter", func() { - originalGinkgoWriter := GinkgoWriter - buffer := &bytes.Buffer{} - - GinkgoWriter = buffer - By("Saying Hello GinkgoWriter") - GinkgoWriter = originalGinkgoWriter - - Ω(buffer.String()).Should(ContainSubstring("STEP")) - Ω(buffer.String()).Should(ContainSubstring(": Saying Hello GinkgoWriter\n")) - }) - - It("calls the passed-in callback if present", func() { - a := 0 - By("calling the callback", func() { - a = 1 - }) - Ω(a).Should(Equal(1)) - }) - - It("panics if there is more than one callback", func() { - Ω(func() { - By("registering more than one callback", func() {}, func() {}) - }).Should(Panic()) - }) - }) - - Describe("GinkgoRandomSeed", func() { - It("returns the current config's random seed", func() { - Ω(GinkgoRandomSeed()).Should(Equal(config.GinkgoConfig.RandomSeed)) - }) - }) -}) diff --git a/internal/suite_node_builder.go b/internal/suite_node_builder.go new file mode 100644 index 0000000000..9f93f0a4b2 --- /dev/null +++ b/internal/suite_node_builder.go @@ -0,0 +1,183 @@ +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +var SUITE_NODE_POLLING_INTERVAL = 50 * time.Millisecond + +/* +The family of SuiteNodes - i.e. BeforeSuite, AfterSuite, SynchronizedBeforeSuite, and SynchronizedAfterSuite +have a set of properties and behaviors that cannot be determined until PhaseRun. + +Specifically, the Synchronized* family need information about parallelism (config.ParallelTotal, config.ParallelNode, config.ParallelHost) +that can only be obtained after the config object has been populated and this only happens once the test suite has begun. +Of course the suite DSl functions are typicaly called in PhaseBuildTopLevel. Given that, we use +this factory approach to save off information about the suite node at PhaseBuildTopLevel and then construct +the actual nodes when we have the config information in PhaseRun. + +Note that SuiteNodeBuilder takes care of packaging all the parallelisum behavior into the single node.Body(). This pushes +all the complexity of the suite node's behavior into this builder class. +*/ +type SuiteNodeBuilder struct { + NodeType types.NodeType + CodeLocation types.CodeLocation + + BeforeSuiteBody func() + SynchronizedBeforeSuiteNode1Body func() []byte + SynchronizedBeforeSuiteAllNodesBody func([]byte) + + AfterSuiteBody func() + SynchronizedAfterSuiteAllNodesBody func() + SynchronizedAfterSuiteNode1Body func() +} + +func (s SuiteNodeBuilder) BuildNode(config config.GinkgoConfigType, failer *Failer) Node { + node := Node{ + ID: UniqueNodeID(), + NodeType: s.NodeType, + CodeLocation: s.CodeLocation, + } + switch s.NodeType { + case types.NodeTypeBeforeSuite: + node.Body = s.BeforeSuiteBody + case types.NodeTypeSynchronizedBeforeSuite: + node.Body = s.buildSynchronizedBeforeSuiteBody(config, failer) + case types.NodeTypeAfterSuite: + node.Body = s.AfterSuiteBody + case types.NodeTypeSynchronizedAfterSuite: + node.Body = s.buildSynchronizedAfterSuiteBody(config, failer) + default: + return Node{} + } + + return node +} + +func (s SuiteNodeBuilder) buildSynchronizedBeforeSuiteBody(config config.GinkgoConfigType, failer *Failer) func() { + if config.ParallelTotal == 1 { + return func() { + data := s.SynchronizedBeforeSuiteNode1Body() + s.SynchronizedBeforeSuiteAllNodesBody(data) + } + } + + if config.ParallelNode == 1 { + return func() { + result := func() (result types.RemoteBeforeSuiteData) { + defer func() { + if e := recover(); e != nil { + failer.Panic(types.NewCodeLocation(2), e) + } + }() + + result.State = types.RemoteBeforeSuiteStateFailed + result.Data = s.SynchronizedBeforeSuiteNode1Body() + result.State = types.RemoteBeforeSuiteStatePassed + return + }() + + resp, err := http.Post(config.ParallelHost+"/BeforeSuiteState", "application/json", bytes.NewBuffer(result.ToJSON())) + if err != nil || resp.StatusCode != http.StatusOK { + failer.Fail("SynchronizedBeforeSuite failed to send data to other nodes", s.CodeLocation) + return + } + resp.Body.Close() + + if result.State == types.RemoteBeforeSuiteStatePassed { + s.SynchronizedBeforeSuiteAllNodesBody(result.Data) + } + } + } else { + return func() { + var result types.RemoteBeforeSuiteData + for { + result = types.RemoteBeforeSuiteData{} + err := s.pollEndpoint(config.ParallelHost+"/BeforeSuiteState", &result) + if err != nil { + failer.Fail("SynchronizedBeforeSuite Server Communication Issue:\n"+err.Error(), s.CodeLocation) + return + } + if result.State != types.RemoteBeforeSuiteStatePending { + break + } + time.Sleep(SUITE_NODE_POLLING_INTERVAL) + } + + switch result.State { + case types.RemoteBeforeSuiteStatePassed: + s.SynchronizedBeforeSuiteAllNodesBody(result.Data) + case types.RemoteBeforeSuiteStateFailed: + failer.Fail("SynchronizedBeforeSuite on Node 1 failed", s.CodeLocation) + case types.RemoteBeforeSuiteStateDisappeared: + failer.Fail("SynchronizedBeforeSuite on Node 1 disappeared before it could report back", s.CodeLocation) + } + } + } +} + +func (s SuiteNodeBuilder) buildSynchronizedAfterSuiteBody(config config.GinkgoConfigType, failer *Failer) func() { + if config.ParallelTotal == 1 { + return func() { + s.SynchronizedAfterSuiteAllNodesBody() + s.SynchronizedAfterSuiteNode1Body() + } + } + + if config.ParallelNode > 1 { + return func() { + s.SynchronizedAfterSuiteAllNodesBody() + } + } else { + return func() { + s.SynchronizedAfterSuiteAllNodesBody() + + for { + afterSuiteData := types.RemoteAfterSuiteData{} + err := s.pollEndpoint(config.ParallelHost+"/AfterSuiteState", &afterSuiteData) + if err != nil { + failer.Fail("SynchronizedAfterSuite Server Communication Issue:\n"+err.Error(), s.CodeLocation) + break + } + if afterSuiteData.CanRun { + break + } + time.Sleep(SUITE_NODE_POLLING_INTERVAL) + } + + s.SynchronizedAfterSuiteNode1Body() + } + } +} + +func (s SuiteNodeBuilder) pollEndpoint(endpoint string, data interface{}) error { + resp, err := http.Get(endpoint) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + resp.Body.Close() + + err = json.Unmarshal(body, data) + if err != nil { + return err + } + + return nil +} diff --git a/internal/suite_node_builder_test.go b/internal/suite_node_builder_test.go new file mode 100644 index 0000000000..c596ea4f4d --- /dev/null +++ b/internal/suite_node_builder_test.go @@ -0,0 +1,383 @@ +package internal_test + +import ( + "net/http" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal" + . "github.com/onsi/ginkgo/internal/test_helpers" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/ghttp" +) + +var _ = Describe("SuiteNodeBuilder", func() { + var rt *RunTracker + var builder internal.SuiteNodeBuilder + var node Node + var conf config.GinkgoConfigType + var failer *internal.Failer + + BeforeEach(func() { + rt = NewRunTracker() + conf = config.GinkgoConfigType{} + conf.ParallelTotal = 1 + conf.ParallelNode = 1 + failer = internal.NewFailer() + }) + + Describe("Building BeforeSuite Nodes", func() { + BeforeEach(func() { + builder = internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeBeforeSuite, + CodeLocation: cl, + BeforeSuiteBody: rt.T("before suite"), + } + + node = builder.BuildNode(conf, failer) + }) + + It("returns an appropriately configured node", func() { + Ω(node.ID).ShouldNot(BeZero()) + Ω(node.NodeType).Should(Equal(types.NodeTypeBeforeSuite)) + Ω(node.CodeLocation).Should(Equal(cl)) + }) + + It("configures the node with a body that simply calls the registered BeforeSuite body", func() { + node.Body() + Ω(rt).Should(HaveTracked("before suite")) + }) + }) + + Describe("Building AfterSuite Nodes", func() { + BeforeEach(func() { + builder = internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeAfterSuite, + CodeLocation: cl, + AfterSuiteBody: rt.T("after suite"), + } + + node = builder.BuildNode(conf, failer) + }) + + It("returns an appropriately configured node", func() { + Ω(node.ID).ShouldNot(BeZero()) + Ω(node.NodeType).Should(Equal(types.NodeTypeAfterSuite)) + Ω(node.CodeLocation).Should(Equal(cl)) + }) + + It("configures the node with a body that simply calls the registered AfterSuite body", func() { + node.Body() + Ω(rt).Should(HaveTracked("after suite")) + }) + + }) + + Describe("Building SynchronizedBeforeSuite Nodes", func() { + var failNode1 bool + var failLocation types.CodeLocation + BeforeEach(func() { + failNode1 = false + failLocation = CL("fail-location") + + builder = internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeSynchronizedBeforeSuite, + CodeLocation: cl, + SynchronizedBeforeSuiteNode1Body: func() []byte { + rt.Run("node1body") + if failNode1 { + failer.Fail("node 1 failed", failLocation) + panic("boom") // simulates Ginkgo DSL's Fail behavior + } + return []byte("snarfblat") + }, + SynchronizedBeforeSuiteAllNodesBody: func(data []byte) { + rt.Run("allnodesbody - " + string(data)) + }, + } + }) + + JustBeforeEach(func() { + node = builder.BuildNode(conf, failer) + }) + + It("returns an appropriately configured node", func() { + Ω(node.ID).ShouldNot(BeZero()) + Ω(node.NodeType).Should(Equal(types.NodeTypeSynchronizedBeforeSuite)) + Ω(node.CodeLocation).Should(Equal(cl)) + }) + + Context("when running in series", func() { + It("simply runs both functions, passing data from node1Body to allNodesBody", func() { + node.Body() + Ω(rt).Should(HaveTracked("node1body", "allnodesbody - snarfblat")) + Ω(failer.GetState()).Should(Equal(types.SpecStatePassed)) + }) + }) + + Context("when running in parallel", func() { + var server *ghttp.Server + BeforeEach(func() { + conf.ParallelTotal = 2 + server = ghttp.NewServer() + conf.ParallelHost = server.URL() + }) + + AfterEach(func() { + server.Close() + }) + + Context("on Node 1", func() { + var responseCode int + var expectedRemoteBeforeSuiteData types.RemoteBeforeSuiteData + BeforeEach(func() { + conf.ParallelNode = 1 + expectedRemoteBeforeSuiteData = types.RemoteBeforeSuiteData{ + State: types.RemoteBeforeSuiteStatePassed, + Data: []byte("snarfblat"), + } + responseCode = http.StatusOK + }) + + JustBeforeEach(func() { + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/BeforeSuiteState"), + ghttp.VerifyJSONRepresenting(expectedRemoteBeforeSuiteData), + ghttp.RespondWith(responseCode, nil), + )) + }) + + It("runs node1Body, posts to the server, then runs allnodesBody", func() { + node.Body() + Ω(server.ReceivedRequests()).Should(HaveLen(1)) + Ω(rt).Should(HaveTracked("node1body", "allnodesbody - snarfblat")) + Ω(failer.GetState()).Should(Equal(types.SpecStatePassed)) + }) + + Context("when node1Body fails (and panics - as that is what Ginkgo's Fail does)", func() { + BeforeEach(func() { + failNode1 = true + expectedRemoteBeforeSuiteData = types.RemoteBeforeSuiteData{ + State: types.RemoteBeforeSuiteStateFailed, + Data: nil, + } + }) + + It("registers a failure and posts such to the server", func() { + node.Body() + Ω(server.ReceivedRequests()).Should(HaveLen(1)) + Ω(failer.GetState()).Should(Equal(types.SpecStateFailed)) + Ω(failer.GetFailure().Location).Should(Equal(failLocation)) + }) + + It("does not run allNodesBody", func() { + node.Body() + Ω(rt).Should(HaveTracked("node1body")) + }) + }) + + Context("when the server post fails", func() { + BeforeEach(func() { + responseCode = http.StatusInternalServerError + }) + + It("sends out a failure", func() { + node.Body() + Ω(server.ReceivedRequests()).Should(HaveLen(1)) + Ω(failer.GetState()).Should(Equal(types.SpecStateFailed)) + Ω(failer.GetFailure().Message).Should(Equal("SynchronizedBeforeSuite failed to send data to other nodes")) + Ω(failer.GetFailure().Location).Should(Equal(cl)) + }) + + It("does not run allNodesBody", func() { + node.Body() + Ω(rt).Should(HaveTracked("node1body")) + }) + }) + }) + + Context("on other nodes", func() { + var responseCode int + var responseRemoteBeforeSuiteData types.RemoteBeforeSuiteData + + BeforeEach(func() { + conf.ParallelNode = 2 + responseCode = http.StatusOK + responseRemoteBeforeSuiteData = types.RemoteBeforeSuiteData{ + State: types.RemoteBeforeSuiteStatePassed, + Data: []byte("dinglehopper"), + } + }) + + JustBeforeEach(func() { + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/BeforeSuiteState"), + ghttp.RespondWithJSONEncoded(http.StatusOK, types.RemoteBeforeSuiteData{ + State: types.RemoteBeforeSuiteStatePending, + }), + ), ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/BeforeSuiteState"), + ghttp.RespondWithJSONEncoded(responseCode, responseRemoteBeforeSuiteData), + )) + }) + + It("never runs node1Body, but waits until the first node has finished to run allnodesBody passing in the correct data", func() { + node.Body() + Ω(server.ReceivedRequests()).Should(HaveLen(2)) + Ω(failer.GetState()).Should(Equal(types.SpecStatePassed)) + Ω(rt).Should(HaveTracked("allnodesbody - dinglehopper")) + }) + + sharedFailureBehavior := func(expectedFailureMessage string) { + It("registers a failure", func() { + node.Body() + Ω(server.ReceivedRequests()).Should(HaveLen(2)) + Ω(failer.GetState()).Should(Equal(types.SpecStateFailed)) + Ω(failer.GetFailure().Message).Should(Equal(expectedFailureMessage)) + Ω(failer.GetFailure().Location).Should(Equal(cl)) + }) + + It("does not run the allNodesBody", func() { + node.Body() + Ω(rt).Should(HaveTrackedNothing()) + }) + } + + Context("when the sync host returns an invalid status code", func() { + BeforeEach(func() { + responseCode = http.StatusInternalServerError + responseRemoteBeforeSuiteData = types.RemoteBeforeSuiteData{} + }) + sharedFailureBehavior("SynchronizedBeforeSuite Server Communication Issue:\nunexpected status code 500") + }) + + Context("when the first node fails", func() { + BeforeEach(func() { + responseRemoteBeforeSuiteData = types.RemoteBeforeSuiteData{ + State: types.RemoteBeforeSuiteStateFailed, + } + }) + + sharedFailureBehavior("SynchronizedBeforeSuite on Node 1 failed") + }) + + Context("when the first node disappears", func() { + BeforeEach(func() { + responseRemoteBeforeSuiteData = types.RemoteBeforeSuiteData{ + State: types.RemoteBeforeSuiteStateDisappeared, + } + }) + + sharedFailureBehavior("SynchronizedBeforeSuite on Node 1 disappeared before it could report back") + }) + }) + }) + }) + + Describe("Building SynchronizedAfterSuite Nodes", func() { + BeforeEach(func() { + builder = internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeSynchronizedAfterSuite, + CodeLocation: cl, + SynchronizedAfterSuiteAllNodesBody: rt.T("allnodesbody"), + SynchronizedAfterSuiteNode1Body: rt.T("node1body"), + } + }) + + JustBeforeEach(func() { + node = builder.BuildNode(conf, failer) + }) + + It("returns an appropriately configured node", func() { + Ω(node.ID).ShouldNot(BeZero()) + Ω(node.NodeType).Should(Equal(types.NodeTypeSynchronizedAfterSuite)) + Ω(node.CodeLocation).Should(Equal(cl)) + }) + + Context("when running in series", func() { + It("simply runs both allNodesBody followed by node1Body", func() { + node.Body() + Ω(rt).Should(HaveTracked("allnodesbody", "node1body")) + Ω(failer.GetState()).Should(Equal(types.SpecStatePassed)) + }) + }) + + Context("when running in parallel", func() { + var server *ghttp.Server + BeforeEach(func() { + conf.ParallelTotal = 2 + server = ghttp.NewServer() + conf.ParallelHost = server.URL() + }) + + AfterEach(func() { + server.Close() + }) + + Context("on Node 1", func() { + var responseCode int + BeforeEach(func() { + conf.ParallelNode = 1 + responseCode = http.StatusOK + }) + + JustBeforeEach(func() { + server.AppendHandlers(ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/AfterSuiteState"), + func(_ http.ResponseWriter, _ *http.Request) { + rt.Run("made-request") + }, + ghttp.RespondWithJSONEncoded(http.StatusOK, types.RemoteAfterSuiteData{ + CanRun: false, + }), + ), ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", "/AfterSuiteState"), + ghttp.RespondWithJSONEncoded(responseCode, types.RemoteAfterSuiteData{ + CanRun: true, + }), + )) + }) + + It("runs allNodesBody, waits for the sync host to give it the all-clear, then runs node1Body", func() { + node.Body() + Ω(server.ReceivedRequests()).Should(HaveLen(2)) + Ω(rt).Should(HaveTracked("allnodesbody", "made-request", "node1body")) + Ω(failer.GetState()).Should(Equal(types.SpecStatePassed)) + }) + + Context("when the sync host returns an invalid status code", func() { + BeforeEach(func() { + responseCode = http.StatusInternalServerError + }) + + It("registers a failure", func() { + node.Body() + Ω(server.ReceivedRequests()).Should(HaveLen(2)) + Ω(failer.GetState()).Should(Equal(types.SpecStateFailed)) + Ω(failer.GetFailure().Message).Should(Equal("SynchronizedAfterSuite Server Communication Issue:\nunexpected status code 500")) + Ω(failer.GetFailure().Location).Should(Equal(cl)) + }) + + It("does still runs node1Body, to ensure everything is cleaned up", func() { + node.Body() + Ω(rt).Should(HaveTracked("allnodesbody", "made-request", "node1body")) + }) + }) + }) + + Context("on other nodes", func() { + BeforeEach(func() { + conf.ParallelNode = 2 + }) + + It("only runs allNodesBody and never runs node1Body", func() { + node.Body() + Ω(server.ReceivedRequests()).Should(HaveLen(0)) + Ω(rt).Should(HaveTracked("allnodesbody")) + Ω(failer.GetState()).Should(Equal(types.SpecStatePassed)) + }) + }) + }) + }) +}) diff --git a/internal/suite_test.go b/internal/suite_test.go new file mode 100644 index 0000000000..659c2e6f17 --- /dev/null +++ b/internal/suite_test.go @@ -0,0 +1,210 @@ +package internal_test + +import ( + "fmt" + "io/ioutil" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/onsi/ginkgo/internal/test_helpers" + "github.com/onsi/ginkgo/types" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/internal" +) + +var _ = Describe("Suite", func() { + It("is heavily integration tested over in internal_integration", func() { + }) + + var suite *internal.Suite + var failer *internal.Failer + var reporter *FakeReporter + var writer *internal.Writer + var conf config.GinkgoConfigType + var rt *RunTracker + + BeforeEach(func() { + failer = internal.NewFailer() + reporter = &FakeReporter{} + writer = internal.NewWriter(ioutil.Discard) + conf = config.GinkgoConfigType{ + ParallelTotal: 1, + ParallelNode: 1, + } + rt = NewRunTracker() + suite = internal.NewSuite() + }) + + Describe("Constructing Trees", func() { + Describe("PhaseBuildTopLevel vs PhaseBuildTree", func() { + var err1, err2, err3 error + BeforeEach(func() { + err1 = suite.PushNode(N(ntCon, "a top-level container", func() { + rt.Run("traversing outer") + err2 = suite.PushNode(N(ntCon, "a nested container", func() { + rt.Run("traversing nested") + err3 = suite.PushNode(N(ntIt, "an it", rt.T("running it"))) + })) + })) + }) + + It("only traverses top-level containers when told to BuildTree", func() { + fmt.Fprintln(GinkgoWriter, "HELLO!") + Ω(rt).Should(HaveTrackedNothing()) + Ω(suite.BuildTree()).Should(Succeed()) + Ω(rt).Should(HaveTracked("traversing outer", "traversing nested")) + + rt.Reset() + suite.Run("suite", failer, reporter, writer, nil, conf) + Ω(rt).Should(HaveTracked("running it")) + + Ω(err1).ShouldNot(HaveOccurred()) + Ω(err2).ShouldNot(HaveOccurred()) + Ω(err3).ShouldNot(HaveOccurred()) + }) + }) + + Context("when pushing nodes during PhaseRun", func() { + var pushNodeErrDuringRun error + + BeforeEach(func() { + err := suite.PushNode(N(ntCon, "a top-level container", func() { + suite.PushNode(N(ntIt, "an it", func() { + rt.Run("in it") + pushNodeErrDuringRun = suite.PushNode(N(ntIt, "oops - illegal operation", cl, rt.T("illegal"))) + })) + })) + + Ω(err).ShouldNot(HaveOccurred()) + Ω(suite.BuildTree()).Should(Succeed()) + }) + + It("errors", func() { + suite.Run("suite", failer, reporter, writer, nil, conf) + Ω(pushNodeErrDuringRun).Should(HaveOccurred()) + Ω(rt).Should(HaveTracked("in it")) + }) + + }) + + Context("when the user attemps to fail during PhaseBuildTree", func() { + BeforeEach(func() { + suite.PushNode(N(ntCon, "a top-level container", func() { + failer.Fail("boom", cl) + panic("simulate ginkgo panic") + })) + }) + + It("errors", func() { + err := suite.BuildTree() + Ω(err.Error()).Should(ContainSubstring(cl.String())) + }) + }) + + Context("when the user panics during PhaseBuildTree", func() { + BeforeEach(func() { + suite.PushNode(N(ntCon, "a top-level container", func() { + panic("boom") + })) + }) + + It("errors", func() { + err := suite.BuildTree() + Ω(err).Should(HaveOccurred()) + }) + }) + + Describe("Suite Nodes", func() { + Context("when pushing suite nodes at the top level", func() { + BeforeEach(func() { + err := suite.PushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeBeforeSuite, + }) + Ω(err).ShouldNot(HaveOccurred()) + + err = suite.PushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeAfterSuite, + }) + Ω(err).ShouldNot(HaveOccurred()) + }) + + Context("when pushing more than one BeforeSuite node", func() { + It("errors", func() { + err := suite.PushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeBeforeSuite, + }) + Ω(err).Should(HaveOccurred()) + + err = suite.PushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeSynchronizedBeforeSuite, + }) + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when pushing more than one AfterSuite node", func() { + It("errors", func() { + err := suite.PushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeAfterSuite, + }) + Ω(err).Should(HaveOccurred()) + + err = suite.PushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeSynchronizedAfterSuite, + }) + Ω(err).Should(HaveOccurred()) + }) + }) + + }) + + Context("when pushing a suite node suring PhaseBuildTree", func() { + It("errors", func() { + var pushSuiteNodeErr error + err := suite.PushNode(N(ntCon, "top-level-container", func() { + pushSuiteNodeErr = suite.PushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeBeforeSuite, + CodeLocation: cl, + BeforeSuiteBody: func() {}, + }) + + })) + + Ω(err).ShouldNot(HaveOccurred()) + Ω(suite.BuildTree()).Should(Succeed()) + Ω(pushSuiteNodeErr).Should(HaveOccurred()) + }) + }) + + Context("when pushing a suite node suring PhaseRun", func() { + It("errors", func() { + var pushSuiteNodeErr error + err := suite.PushNode(N(ntIt, "top-level it", func() { + pushSuiteNodeErr = suite.PushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeBeforeSuite, + CodeLocation: cl, + BeforeSuiteBody: func() {}, + }) + })) + + Ω(err).ShouldNot(HaveOccurred()) + Ω(suite.BuildTree()).Should(Succeed()) + suite.Run("suite", failer, reporter, writer, nil, conf) + Ω(pushSuiteNodeErr).Should(HaveOccurred()) + }) + }) + + Context("when pushing a busted suite node", func() { + It("panics", func() { + Ω(func() { + suite.PushSuiteNodeBuilder(internal.SuiteNodeBuilder{ + NodeType: types.NodeTypeIt, + }) + }).Should(Panic()) + }) + }) + }) + }) +}) diff --git a/internal/test_helpers/fake_reporter.go b/internal/test_helpers/fake_reporter.go new file mode 100644 index 0000000000..fc739cfe51 --- /dev/null +++ b/internal/test_helpers/fake_reporter.go @@ -0,0 +1,206 @@ +package test_helpers + +import ( + "reflect" + + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +/* + +A FakeReporter and collection of matchers to match against reported suite and spec summaries + +*/ + +type Summaries []types.Summary + +func (s Summaries) FindByLeafNodeType(nodeType ...types.NodeType) types.Summary { + for _, summary := range s { + if summary.LeafNodeType.Is(nodeType...) { + return summary + } + } + + return types.Summary{} +} + +func (s Summaries) Find(name string) types.Summary { + for _, summary := range s { + if len(summary.NodeTexts) > 0 && summary.NodeTexts[len(summary.NodeTexts)-1] == name { + return summary + } + } + + return types.Summary{} +} + +func (s Summaries) Names() []string { + out := []string{} + for _, summary := range s { + if len(summary.NodeTexts) > 0 { + out = append(out, summary.NodeTexts[len(summary.NodeTexts)-1]) + } + } + return out +} + +func (s Summaries) WithState(state types.SpecState) Summaries { + out := Summaries{} + for _, summary := range s { + if summary.State == state { + out = append(out, summary) + } + } + return out +} + +type FakeReporter struct { + Config config.GinkgoConfigType + Begin types.SuiteSummary + Will Summaries + Did Summaries + End types.SuiteSummary +} + +func (r *FakeReporter) SpecSuiteWillBegin(conf config.GinkgoConfigType, summary types.SuiteSummary) { + r.Begin = summary + r.Config = conf +} + +func (r *FakeReporter) WillRun(summary types.Summary) { + r.Will = append(r.Will, summary) +} + +func (r *FakeReporter) DidRun(summary types.Summary) { + r.Did = append(r.Did, summary) +} + +func (r *FakeReporter) SpecSuiteDidEnd(summary types.SuiteSummary) { + r.End = summary +} + +type NSpecs int +type NWillRun int +type NPassed int +type NSkipped int +type NFailed int +type NPending int +type NFlaked int + +func BeASuiteSummary(options ...interface{}) OmegaMatcher { + fields := Fields{} + fields["NumberOfTotalSpecs"] = Equal(0) + fields["NumberOfPassedSpecs"] = Equal(0) + fields["NumberOfSkippedSpecs"] = Equal(0) + fields["NumberOfFailedSpecs"] = Equal(0) + fields["NumberOfPendingSpecs"] = Equal(0) + fields["NumberOfFlakedSpecs"] = Equal(0) + + for _, option := range options { + t := reflect.TypeOf(option) + if t.Kind() == reflect.Bool { + if option.(bool) { + fields["SuiteSucceeded"] = BeTrue() + } else { + fields["SuiteSucceeded"] = BeFalse() + } + } else if t == reflect.TypeOf(NSpecs(0)) { + fields["NumberOfTotalSpecs"] = Equal(int(option.(NSpecs))) + } else if t == reflect.TypeOf(NWillRun(0)) { + fields["NumberOfSpecsThatWillBeRun"] = Equal(int(option.(NWillRun))) + } else if t == reflect.TypeOf(NPassed(0)) { + fields["NumberOfPassedSpecs"] = Equal(int(option.(NPassed))) + } else if t == reflect.TypeOf(NSkipped(0)) { + fields["NumberOfSkippedSpecs"] = Equal(int(option.(NSkipped))) + } else if t == reflect.TypeOf(NFailed(0)) { + fields["NumberOfFailedSpecs"] = Equal(int(option.(NFailed))) + } else if t == reflect.TypeOf(NPending(0)) { + fields["NumberOfPendingSpecs"] = Equal(int(option.(NPending))) + } else if t == reflect.TypeOf(NFlaked(0)) { + fields["NumberOfFlakedSpecs"] = Equal(int(option.(NFlaked))) + } + } + + return MatchFields(IgnoreExtras, fields) +} + +type CapturedOutput string +type NumAttempts int + +func HavePassed(options ...interface{}) OmegaMatcher { + fields := Fields{ + "State": Equal(types.SpecStatePassed), + "Failure": BeZero(), + } + for _, option := range options { + t := reflect.TypeOf(option) + if t == reflect.TypeOf(CapturedOutput("")) { + fields["CapturedGinkgoWriterOutput"] = Equal(string(option.(CapturedOutput))) + } else if t == reflect.TypeOf(NumAttempts(0)) { + fields["NumAttempts"] = Equal(int(option.(NumAttempts))) + } + } + return MatchFields(IgnoreExtras, fields) +} + +func BePending() OmegaMatcher { + return MatchFields(IgnoreExtras, Fields{ + "State": Equal(types.SpecStatePending), + "Failure": BeZero(), + }) +} + +func HaveBeenSkipped() OmegaMatcher { + return MatchFields(IgnoreExtras, Fields{ + "State": Equal(types.SpecStateSkipped), + "Failure": BeZero(), + }) +} + +func HaveFailed(options ...interface{}) OmegaMatcher { + fields := Fields{ + "State": Equal(types.SpecStateFailed), + } + failureFields := Fields{} + for _, option := range options { + t := reflect.TypeOf(option) + if t == reflect.TypeOf(CapturedOutput("")) { + fields["CapturedGinkgoWriterOutput"] = Equal(string(option.(CapturedOutput))) + } else if t.Kind() == reflect.String { + failureFields["Message"] = Equal(option.(string)) + } else if t == reflect.TypeOf(types.CodeLocation{}) { + failureFields["Location"] = Equal(option.(types.CodeLocation)) + } else if t == reflect.TypeOf(NumAttempts(0)) { + fields["NumAttempts"] = Equal(int(option.(NumAttempts))) + } + } + if len(failureFields) > 0 { + fields["Failure"] = MatchFields(IgnoreExtras, failureFields) + } + return MatchFields(IgnoreExtras, fields) +} + +func HavePanicked(options ...interface{}) OmegaMatcher { + fields := Fields{ + "State": Equal(types.SpecStatePanicked), + } + failureFields := Fields{} + for _, option := range options { + t := reflect.TypeOf(option) + if t == reflect.TypeOf(CapturedOutput("")) { + fields["CapturedGinkgoWriterOutput"] = Equal(string(option.(CapturedOutput))) + } else if t.Kind() == reflect.String { + failureFields["ForwardedPanic"] = Equal(option.(string)) + } else if t == reflect.TypeOf(NumAttempts(0)) { + fields["NumAttempts"] = Equal(int(option.(NumAttempts))) + } + } + if len(failureFields) > 0 { + fields["Failure"] = MatchFields(IgnoreExtras, failureFields) + } + return MatchFields(IgnoreExtras, fields) +} diff --git a/internal/test_helpers/markdown_heading_loader.go b/internal/test_helpers/markdown_heading_loader.go new file mode 100644 index 0000000000..5f8e94f231 --- /dev/null +++ b/internal/test_helpers/markdown_heading_loader.go @@ -0,0 +1,34 @@ +package test_helpers + +import ( + "io/ioutil" + "regexp" + "strings" +) + +var punctuationRE = regexp.MustCompile(`[^\w\-\s]`) + +func LoadMarkdownHeadingAnchors(filename string) []string { + b, err := ioutil.ReadFile(filename) + if err != nil { + return []string{} + } + + anchors := []string{} + lines := strings.Split(string(b), "\n") + for _, line := range lines { + line = strings.TrimLeft(line, " ") + line = strings.TrimRight(line, " ") + if !strings.HasPrefix(line, "#") { + continue + } + + line = strings.TrimLeft(line, "# ") + line = punctuationRE.ReplaceAllString(line, "") + line = strings.ToLower(line) + line = strings.ReplaceAll(line, " ", "-") + anchors = append(anchors, line) + } + + return anchors +} diff --git a/internal/test_helpers/run_tracker.go b/internal/test_helpers/run_tracker.go new file mode 100644 index 0000000000..e367b983c5 --- /dev/null +++ b/internal/test_helpers/run_tracker.go @@ -0,0 +1,115 @@ +package test_helpers + +import ( + "sync" + + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" +) + +/* + +RunTracker tracks invocations of functions - useful to assert orders in which nodes run + +*/ + +type RunTracker struct { + lock *sync.Mutex + trackedRuns []string + trackedData map[string]map[string]interface{} +} + +func NewRunTracker() *RunTracker { + return &RunTracker{ + lock: &sync.Mutex{}, + trackedData: map[string]map[string]interface{}{}, + } +} + +func (rt *RunTracker) Reset() { + rt.lock.Lock() + defer rt.lock.Unlock() + rt.trackedRuns = []string{} +} + +func (rt *RunTracker) Run(text string) { + rt.lock.Lock() + defer rt.lock.Unlock() + rt.trackedRuns = append(rt.trackedRuns, text) +} + +func (rt *RunTracker) RunWithData(text string, kv ...interface{}) { + rt.lock.Lock() + defer rt.lock.Unlock() + rt.trackedRuns = append(rt.trackedRuns, text) + data := map[string]interface{}{} + for i := 0; i < len(kv); i += 2 { + key := kv[i].(string) + value := kv[i+1] + data[key] = value + } + rt.trackedData[text] = data +} + +func (rt *RunTracker) TrackedRuns() []string { + rt.lock.Lock() + defer rt.lock.Unlock() + trackedRuns := make([]string, len(rt.trackedRuns)) + copy(trackedRuns, rt.trackedRuns) + return trackedRuns +} + +func (rt *RunTracker) DataFor(text string) map[string]interface{} { + rt.lock.Lock() + defer rt.lock.Unlock() + return rt.trackedData[text] +} + +func (rt *RunTracker) T(text string, callback ...func()) func() { + return func() { + rt.Run(text) + if len(callback) > 0 { + callback[0]() + } + } +} + +func (rt *RunTracker) C(text string, callback ...func()) func(args []string, additionalArgs []string) { + return func(args []string, additionalArgs []string) { + rt.RunWithData(text, "Args", args, "AdditionalArgs", additionalArgs) + if len(callback) > 0 { + callback[0]() + } + } +} + +func HaveRun(run string) OmegaMatcher { + return WithTransform(func(rt *RunTracker) []string { + return rt.TrackedRuns() + }, ContainElement(run)) +} + +func HaveRunWithData(run string, kv ...interface{}) OmegaMatcher { + matchers := []types.GomegaMatcher{} + for i := 0; i < len(kv); i += 2 { + matchers = append(matchers, HaveKeyWithValue(kv[i], kv[i+1])) + } + return And( + HaveRun(run), + WithTransform(func(rt *RunTracker) map[string]interface{} { + return rt.DataFor(run) + }, And(matchers...)), + ) +} + +func HaveTracked(runs ...string) OmegaMatcher { + return WithTransform(func(rt *RunTracker) []string { + return rt.TrackedRuns() + }, Equal(runs)) +} + +func HaveTrackedNothing() OmegaMatcher { + return WithTransform(func(rt *RunTracker) []string { + return rt.TrackedRuns() + }, BeEmpty()) +} diff --git a/internal/test_helpers/status_code_poller.go b/internal/test_helpers/status_code_poller.go new file mode 100644 index 0000000000..066b919b1c --- /dev/null +++ b/internal/test_helpers/status_code_poller.go @@ -0,0 +1,14 @@ +package test_helpers + +import "net/http" + +func StatusCodePoller(url string) func() int { + return func() int { + resp, err := http.Get(url) + if err != nil { + return 0 + } + resp.Body.Close() + return resp.StatusCode + } +} diff --git a/internal/tree.go b/internal/tree.go new file mode 100644 index 0000000000..a0471c8a4f --- /dev/null +++ b/internal/tree.go @@ -0,0 +1,67 @@ +package internal + +import "github.com/onsi/ginkgo/types" + +type TreeNode struct { + Node Node + Children TreeNodes +} + +type TreeNodes []TreeNode + +func (tn TreeNodes) Nodes() Nodes { + out := Nodes{} + for _, treeNode := range tn { + out = append(out, treeNode.Node) + } + return out +} + +func (tn TreeNodes) WithID(id uint) TreeNode { + for _, treeNode := range tn { + if treeNode.Node.ID == id { + return treeNode + } + } + + return TreeNode{} +} + +func AppendTreeNodeChild(treeNode TreeNode, child TreeNode) TreeNode { + treeNode.Children = append(treeNode.Children, child) + return treeNode +} + +func GenerateSpecsFromTreeRoot(tree TreeNode) Specs { + var walkTree func(nestingLevel int, lNodes Nodes, rNodes Nodes, trees TreeNodes) Specs + walkTree = func(nestingLevel int, lNodes Nodes, rNodes Nodes, trees TreeNodes) Specs { + tests := Specs{} + + nodes := Nodes{} + for _, node := range trees.Nodes() { + node.NestingLevel = nestingLevel + nodes = append(nodes, node) + } + + itsAndContainers := nodes.WithType(types.NodeTypesForContainerAndIt...) + for _, node := range itsAndContainers { + leftNodes, rightNodes := nodes.SplitAround(node) + leftNodes = leftNodes.WithoutType(types.NodeTypesForContainerAndIt...) + rightNodes = rightNodes.WithoutType(types.NodeTypesForContainerAndIt...) + + leftNodes = lNodes.CopyAppend(leftNodes...) + rightNodes = rightNodes.CopyAppend(rNodes...) + + if node.NodeType.Is(types.NodeTypeIt) { + tests = append(tests, Spec{Nodes: leftNodes.CopyAppend(node).CopyAppend(rightNodes...)}) + } else { + treeNode := trees.WithID(node.ID) + tests = append(tests, walkTree(nestingLevel+1, leftNodes.CopyAppend(node), rightNodes, treeNode.Children)...) + } + } + + return tests + } + + return walkTree(0, Nodes{}, Nodes{}, tree.Children) +} diff --git a/internal/tree_test.go b/internal/tree_test.go new file mode 100644 index 0000000000..fa35a7e84b --- /dev/null +++ b/internal/tree_test.go @@ -0,0 +1,168 @@ +package internal_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/internal" +) + +var _ = Describe("Trees (TreeNode and TreeNodes)", func() { + Describe("TreeNodes methods", func() { + var n1, n2, n3 Node + var childNode Node + var treeNodes TreeNodes + BeforeEach(func() { + n1, n2, n3 = N(), N(), N() + childNode = N() + treeNodes = TreeNodes{ + TN(n1, + TN(childNode), + ), + TN(n2), + TN(n3), + } + }) + + Describe("treenodes.Nodes", func() { + It("returns the root node of each node in the treenodes slice", func() { + Ω(treeNodes.Nodes()).Should(Equal(Nodes{n1, n2, n3})) + }) + }) + + Describe("treenodes.WithId", func() { + Context("when a tree with a root node with a matching id is found", func() { + It("returns that tree", func() { + Ω(treeNodes.WithID(n2.ID)).Should(Equal(TN(n2))) + }) + }) + + Context("when the id matches a child node's id", func() { + It("returns an empty tree as children are not included in the match", func() { + Ω(treeNodes.WithID(childNode.ID)).Should(BeZero()) + }) + }) + + Context("when the id cannot be found", func() { + It("returns an empty tree", func() { + Ω(treeNodes.WithID(1000000)).Should(BeZero()) //pretty sure it's a safe bet we don't ever get to 1_000_000 nodes in this test ;) + }) + }) + }) + }) + + Describe("AppendTreeNodeChild", func() { + It("appends the passed in child treenode to the parent's children and returns the parent", func() { + existingChildNode1 := N() + existingChildNode2 := N() + treeNode := TN(N(), + TN(existingChildNode1), + TN(existingChildNode2), + ) + newChildNode := N() + + result := internal.AppendTreeNodeChild(treeNode, TreeNode{Node: newChildNode}) + Ω(result.Children.Nodes()).Should(Equal(Nodes{existingChildNode1, existingChildNode2, newChildNode})) + }) + }) + + Describe("GenerateSpecsFromTreeRoot", func() { + var tree TreeNode + BeforeEach(func() { + tree = TreeNode{} + }) + + Context("when the tree is empty", func() { + It("returns an empty set of tests", func() { + Ω(internal.GenerateSpecsFromTreeRoot(tree)).Should(BeEmpty()) + }) + }) + + Context("when the tree has no Its", func() { + BeforeEach(func() { + tree = TN(Node{}, + TN(N(ntBef)), + TN(N(ntCon), + TN(N(ntBef)), + TN(N(ntAf)), + ), + TN(N(ntCon), + TN(N(ntCon), + TN(N(ntBef)), + TN(N(ntAf)), + ), + ), + TN(N(ntAf)), + ) + }) + + It("returns an empty set of tests", func() { + Ω(internal.GenerateSpecsFromTreeRoot(tree)).Should(BeEmpty()) + }) + }) + + Context("when the tree has nodes in it", func() { + var tests Specs + BeforeEach(func() { + tree = TN(Node{}, + TN(N(ntBef, "Bef #0")), + TN(N(ntIt, "It #1")), + TN(N(ntCon, "Container #1"), + TN(N(ntBef, "Bef #1")), + TN(N(ntAf, "Af #1")), + TN(N(ntIt, "It #2")), + ), + TN(N(ntCon, "Container #2"), + TN(N(ntBef, "Bef #2")), + TN(N(ntCon, "Nested Container"), + TN(N(ntBef, "Bef #4")), + TN(N(ntIt, "It #3")), + TN(N(ntIt, "It #4")), + TN(N(ntAf, "Af #2")), + ), + TN(N(ntIt, "It #5")), + TN(N(ntCon, "A Container With No Its"), + TN(N(ntBef, "Bef #5")), + ), + TN(N(ntAf, "Af #3")), + ), + TN(N(ntIt, "It #6")), + TN(N(ntAf, "Af #4")), + ) + + tests = internal.GenerateSpecsFromTreeRoot(tree) + }) + + It("constructs a flattened set of tests", func() { + Ω(tests).Should(HaveLen(6)) + expectedTexts := [][]string{ + {"Bef #0", "It #1", "Af #4"}, + {"Bef #0", "Container #1", "Bef #1", "Af #1", "It #2", "Af #4"}, + {"Bef #0", "Container #2", "Bef #2", "Nested Container", "Bef #4", "It #3", "Af #2", "Af #3", "Af #4"}, + {"Bef #0", "Container #2", "Bef #2", "Nested Container", "Bef #4", "It #4", "Af #2", "Af #3", "Af #4"}, + {"Bef #0", "Container #2", "Bef #2", "It #5", "Af #3", "Af #4"}, + {"Bef #0", "It #6", "Af #4"}, + } + for i, expectedText := range expectedTexts { + Ω(tests[i].Nodes.Texts()).Should(Equal(expectedText)) + } + }) + + It("ensures each node as the correct nesting level", func() { + extpectedNestingLevels := [][]int{ + {0, 0, 0}, + {0, 0, 1, 1, 1, 0}, + {0, 0, 1, 1, 2, 2, 2, 1, 0}, + {0, 0, 1, 1, 2, 2, 2, 1, 0}, + {0, 0, 1, 1, 1, 0}, + {0, 0, 0}, + } + for i, expectedNestingLevels := range extpectedNestingLevels { + for j, expectedNestingLevel := range expectedNestingLevels { + Ω(tests[i].Nodes[j].NestingLevel).Should(Equal(expectedNestingLevel)) + } + } + }) + }) + }) +}) diff --git a/internal/writer/writer.go b/internal/writer.go similarity index 51% rename from internal/writer/writer.go rename to internal/writer.go index 98eca3bdd2..ef67103796 100644 --- a/internal/writer/writer.go +++ b/internal/writer.go @@ -1,4 +1,4 @@ -package writer +package internal import ( "bytes" @@ -10,20 +10,17 @@ type WriterInterface interface { io.Writer Truncate() - DumpOut() - DumpOutWithHeader(header string) Bytes() []byte } type Writer struct { - buffer *bytes.Buffer - outWriter io.Writer - lock *sync.Mutex - stream bool - redirector io.Writer + buffer *bytes.Buffer + outWriter io.Writer + lock *sync.Mutex + stream bool } -func New(outWriter io.Writer) *Writer { +func NewWriter(outWriter io.Writer) *Writer { return &Writer{ buffer: &bytes.Buffer{}, lock: &sync.Mutex{}, @@ -32,10 +29,6 @@ func New(outWriter io.Writer) *Writer { } } -func (w *Writer) AndRedirectTo(writer io.Writer) { - w.redirector = writer -} - func (w *Writer) SetStream(stream bool) { w.lock.Lock() defer w.lock.Unlock() @@ -46,14 +39,10 @@ func (w *Writer) Write(b []byte) (n int, err error) { w.lock.Lock() defer w.lock.Unlock() - n, err = w.buffer.Write(b) - if w.redirector != nil { - w.redirector.Write(b) - } if w.stream { return w.outWriter.Write(b) } - return n, err + return w.buffer.Write(b) } func (w *Writer) Truncate() { @@ -62,14 +51,6 @@ func (w *Writer) Truncate() { w.buffer.Reset() } -func (w *Writer) DumpOut() { - w.lock.Lock() - defer w.lock.Unlock() - if !w.stream { - w.buffer.WriteTo(w.outWriter) - } -} - func (w *Writer) Bytes() []byte { w.lock.Lock() defer w.lock.Unlock() @@ -78,12 +59,3 @@ func (w *Writer) Bytes() []byte { copy(copied, b) return copied } - -func (w *Writer) DumpOutWithHeader(header string) { - w.lock.Lock() - defer w.lock.Unlock() - if !w.stream && w.buffer.Len() > 0 { - w.outWriter.Write([]byte(header)) - w.buffer.WriteTo(w.outWriter) - } -} diff --git a/internal/writer/fake_writer.go b/internal/writer/fake_writer.go deleted file mode 100644 index 6739c3f605..0000000000 --- a/internal/writer/fake_writer.go +++ /dev/null @@ -1,36 +0,0 @@ -package writer - -type FakeGinkgoWriter struct { - EventStream []string -} - -func NewFake() *FakeGinkgoWriter { - return &FakeGinkgoWriter{ - EventStream: []string{}, - } -} - -func (writer *FakeGinkgoWriter) AddEvent(event string) { - writer.EventStream = append(writer.EventStream, event) -} - -func (writer *FakeGinkgoWriter) Truncate() { - writer.EventStream = append(writer.EventStream, "TRUNCATE") -} - -func (writer *FakeGinkgoWriter) DumpOut() { - writer.EventStream = append(writer.EventStream, "DUMP") -} - -func (writer *FakeGinkgoWriter) DumpOutWithHeader(header string) { - writer.EventStream = append(writer.EventStream, "DUMP_WITH_HEADER: "+header) -} - -func (writer *FakeGinkgoWriter) Bytes() []byte { - writer.EventStream = append(writer.EventStream, "BYTES") - return nil -} - -func (writer *FakeGinkgoWriter) Write(data []byte) (n int, err error) { - return 0, nil -} diff --git a/internal/writer/writer_suite_test.go b/internal/writer/writer_suite_test.go deleted file mode 100644 index e206577919..0000000000 --- a/internal/writer/writer_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package writer_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestWriter(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Writer Suite") -} diff --git a/internal/writer/writer_test.go b/internal/writer/writer_test.go deleted file mode 100644 index 3e1d17c6d5..0000000000 --- a/internal/writer/writer_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package writer_test - -import ( - "github.com/onsi/gomega/gbytes" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/internal/writer" - . "github.com/onsi/gomega" -) - -var _ = Describe("Writer", func() { - var writer *Writer - var out *gbytes.Buffer - - BeforeEach(func() { - out = gbytes.NewBuffer() - writer = New(out) - }) - - It("should stream directly to the outbuffer by default", func() { - writer.Write([]byte("foo")) - Ω(out).Should(gbytes.Say("foo")) - }) - - It("should not emit the header when asked to DumpOutWitHeader", func() { - writer.Write([]byte("foo")) - writer.DumpOutWithHeader("my header") - Ω(out).ShouldNot(gbytes.Say("my header")) - Ω(out).Should(gbytes.Say("foo")) - }) - - Context("when told not to stream", func() { - BeforeEach(func() { - writer.SetStream(false) - }) - - It("should only write to the buffer when told to DumpOut", func() { - writer.Write([]byte("foo")) - Ω(out).ShouldNot(gbytes.Say("foo")) - writer.DumpOut() - Ω(out).Should(gbytes.Say("foo")) - }) - - It("should truncate the internal buffer when told to truncate", func() { - writer.Write([]byte("foo")) - writer.Truncate() - writer.DumpOut() - Ω(out).ShouldNot(gbytes.Say("foo")) - - writer.Write([]byte("bar")) - writer.DumpOut() - Ω(out).Should(gbytes.Say("bar")) - }) - - Describe("emitting a header", func() { - Context("when the buffer has content", func() { - It("should emit the header followed by the content", func() { - writer.Write([]byte("foo")) - writer.DumpOutWithHeader("my header") - - Ω(out).Should(gbytes.Say("my header")) - Ω(out).Should(gbytes.Say("foo")) - }) - }) - - Context("when the buffer has no content", func() { - It("should not emit the header", func() { - writer.DumpOutWithHeader("my header") - - Ω(out).ShouldNot(gbytes.Say("my header")) - }) - }) - }) - }) -}) diff --git a/internal/writer_test.go b/internal/writer_test.go new file mode 100644 index 0000000000..c7010ee2c9 --- /dev/null +++ b/internal/writer_test.go @@ -0,0 +1,60 @@ +package internal_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/internal" + "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("Writer", func() { + var writer *internal.Writer + var out *gbytes.Buffer + + BeforeEach(func() { + out = gbytes.NewBuffer() + writer = internal.NewWriter(out) + }) + + Context("when configured to stream (the default)", func() { + It("should stream directly to the passed in writer by default", func() { + writer.Write([]byte("foo")) + Ω(out).Should(gbytes.Say("foo")) + }) + + It("does not store the bytes", func() { + writer.Write([]byte("foo")) + Ω(out).Should(gbytes.Say("foo")) + Ω(writer.Bytes()).Should(BeEmpty()) + }) + }) + + Context("when told not to stream", func() { + BeforeEach(func() { + writer.SetStream(false) + }) + + It("should not write to the passed in writer", func() { + writer.Write([]byte("foo")) + Ω(out).ShouldNot(gbytes.Say("foo")) + }) + + Describe("Bytes()", func() { + BeforeEach(func() { + writer.Write([]byte("foo")) + }) + + It("returns all that's been written so far", func() { + Ω(writer.Bytes()).Should(Equal([]byte("foo"))) + }) + + It("clears when told to truncate", func() { + writer.Truncate() + Ω(writer.Bytes()).Should(BeEmpty()) + writer.Write([]byte("bar")) + Ω(writer.Bytes()).Should(Equal([]byte("bar"))) + }) + }) + }) +}) diff --git a/reporters/default_reporter.go b/reporters/default_reporter.go index f0c9f61410..20e99d9e15 100644 --- a/reporters/default_reporter.go +++ b/reporters/default_reporter.go @@ -8,80 +8,357 @@ These are documented [here](http://onsi.github.io/ginkgo/#running_tests) package reporters import ( + "fmt" + "io" + "runtime" + "strings" + "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/reporters/stenographer" + "github.com/onsi/ginkgo/formatter" "github.com/onsi/ginkgo/types" ) type DefaultReporter struct { - config config.DefaultReporterConfigType - stenographer stenographer.Stenographer - specSummaries []*types.SpecSummary + conf config.DefaultReporterConfigType + hasFailOnPending bool + writer io.Writer + + failures []types.Summary + + // managing the emission stream + lastChar string + lastEmissionWasDelimiter bool + + // rendering + specDenoter string + retryDenoter string + formatter formatter.Formatter +} + +func NewDefaultReporterUnderTest(conf config.DefaultReporterConfigType, writer io.Writer) *DefaultReporter { + reporter := NewDefaultReporter(conf, writer) + reporter.formatter = formatter.New(formatter.ColorModePassthrough) + + return reporter } -func NewDefaultReporter(config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *DefaultReporter { - return &DefaultReporter{ - config: config, - stenographer: stenographer, +func NewDefaultReporter(conf config.DefaultReporterConfigType, writer io.Writer) *DefaultReporter { + reporter := &DefaultReporter{ + conf: conf, + writer: writer, + + lastChar: "\n", + lastEmissionWasDelimiter: false, + + specDenoter: "•", + retryDenoter: "↺", + formatter: formatter.NewWithNoColorBool(conf.NoColor), } + if runtime.GOOS == "windows" { + reporter.specDenoter = "+" + reporter.retryDenoter = "R" + } + + return reporter } -func (reporter *DefaultReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { - reporter.stenographer.AnnounceSuite(summary.SuiteDescription, config.RandomSeed, config.RandomizeAllSpecs, reporter.config.Succinct) - if config.ParallelTotal > 1 { - reporter.stenographer.AnnounceParallelRun(config.ParallelNode, config.ParallelTotal, reporter.config.Succinct) +/* The Reporter Interface */ + +func (r *DefaultReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary types.SuiteSummary) { + r.hasFailOnPending = config.FailOnPending + if r.conf.Succinct { + r.emit(r.f("[%d] {{bold}}%s{{/}} ", config.RandomSeed, summary.SuiteDescription)) + r.emit(r.f("- %d/%d specs ", summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs)) + if config.ParallelTotal > 1 { + r.emit(r.f("- %d nodes ", config.ParallelTotal)) + } } else { - reporter.stenographer.AnnounceNumberOfSpecs(summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs, reporter.config.Succinct) + banner := r.f("Running Suite: %s", summary.SuiteDescription) + r.emitBlock(banner) + r.emitBlock(strings.Repeat("=", len(banner))) + + out := r.f("Random Seed: {{bold}}%d{{/}}", config.RandomSeed) + if config.RandomizeAllSpecs { + out += r.f(" - will randomize all specs") + } + r.emitBlock(out) + r.emit("\n") + r.emitBlock(r.f("Will run {{bold}}%d{{/}} of {{bold}}%d{{/}} specs", summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs)) + if config.ParallelTotal > 1 { + r.emitBlock(r.f("Running in parallel across {{bold}}%d{{/}} nodes", config.ParallelTotal)) + } } } -func (reporter *DefaultReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { - if setupSummary.State != types.SpecStatePassed { - reporter.stenographer.AnnounceBeforeSuiteFailure(setupSummary, reporter.config.Succinct, reporter.config.FullTrace) +func (r *DefaultReporter) WillRun(summary types.Summary) { + if !r.conf.Verbose || summary.State.Is(types.SpecStatePending, types.SpecStateSkipped) { + return } -} -func (reporter *DefaultReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { - if setupSummary.State != types.SpecStatePassed { - reporter.stenographer.AnnounceAfterSuiteFailure(setupSummary, reporter.config.Succinct, reporter.config.FullTrace) + r.emitDelimiter() + if summary.LeafNodeType.Is(types.NodeTypesForSuiteSetup...) { + r.emitBlock(r.f("{{bold}}[%s]{{/}}", summary.LeafNodeType.String())) + r.emitBlock(r.f("{{gray}}%s{{/}}", summary.LeafNodeLocation)) + } else { + lastIndex := len(summary.NodeTexts) - 1 + indentation := uint(0) + if lastIndex > 0 { + r.emitBlock(r.cycleJoin(summary.NodeTexts[0:lastIndex], " ")) + indentation = 1 + } + if lastIndex >= 0 { + r.emitBlock(r.fi(indentation, "{{bold}}%s{{/}}", summary.NodeTexts[lastIndex])) + r.emitBlock(r.fi(indentation, "{{gray}}%s{{/}}", summary.NodeLocations[lastIndex])) + } } } -func (reporter *DefaultReporter) SpecWillRun(specSummary *types.SpecSummary) { - if reporter.config.Verbose && !reporter.config.Succinct && specSummary.State != types.SpecStatePending && specSummary.State != types.SpecStateSkipped { - reporter.stenographer.AnnounceSpecWillRun(specSummary) +func (r *DefaultReporter) DidRun(summary types.Summary) { + var header, highlightColor string + includeRuntime, emitGinkgoWriterOutput, stream, denoter := true, true, false, r.specDenoter + succinctLocationBlock := r.conf.Succinct + + hasGW := summary.CapturedGinkgoWriterOutput != "" + hasStd := summary.CapturedStdOutErr != "" + + if summary.LeafNodeType.Is(types.NodeTypesForSuiteSetup...) { + denoter = fmt.Sprintf("[%s]", summary.LeafNodeType) } -} -func (reporter *DefaultReporter) SpecDidComplete(specSummary *types.SpecSummary) { - switch specSummary.State { + switch summary.State { case types.SpecStatePassed: - if specSummary.IsMeasurement { - reporter.stenographer.AnnounceSuccessfulMeasurement(specSummary, reporter.config.Succinct) - } else if specSummary.RunTime.Seconds() >= reporter.config.SlowSpecThreshold { - reporter.stenographer.AnnounceSuccessfulSlowSpec(specSummary, reporter.config.Succinct) + highlightColor, succinctLocationBlock = "{{green}}", !r.conf.Verbose + emitGinkgoWriterOutput = (r.conf.ReportPassed || r.conf.Verbose) && hasGW + if summary.LeafNodeType.Is(types.NodeTypesForSuiteSetup...) { + if r.conf.Verbose || hasStd { + header = fmt.Sprintf("%s PASSED", denoter) + } else { + return + } } else { - reporter.stenographer.AnnounceSuccessfulSpec(specSummary) - if reporter.config.ReportPassed { - reporter.stenographer.AnnounceCapturedOutput(specSummary.CapturedOutput) + header, stream = denoter, true + if summary.NumAttempts > 1 { + header, stream = fmt.Sprintf("%s [FLAKEY TEST - TOOK %d ATTEMPTS TO PASS]", r.retryDenoter, summary.NumAttempts), false } + if summary.RunTime.Seconds() > r.conf.SlowSpecThreshold { + header, stream = fmt.Sprintf("%s [SLOW TEST]", header), false + } + } + if hasStd || emitGinkgoWriterOutput { + stream = false } case types.SpecStatePending: - reporter.stenographer.AnnouncePendingSpec(specSummary, reporter.config.NoisyPendings && !reporter.config.Succinct) + highlightColor = "{{yellow}}" + includeRuntime, emitGinkgoWriterOutput = false, false + if r.conf.Succinct { + header, stream = "P", true + } else { + header, succinctLocationBlock = "P [PENDING]", !r.conf.Verbose + } case types.SpecStateSkipped: - reporter.stenographer.AnnounceSkippedSpec(specSummary, reporter.config.Succinct || !reporter.config.NoisySkippings, reporter.config.FullTrace) - case types.SpecStateTimedOut: - reporter.stenographer.AnnounceSpecTimedOut(specSummary, reporter.config.Succinct, reporter.config.FullTrace) - case types.SpecStatePanicked: - reporter.stenographer.AnnounceSpecPanicked(specSummary, reporter.config.Succinct, reporter.config.FullTrace) + highlightColor = "{{cyan}}" + if r.conf.Succinct || summary.Failure.Message == "" { + header, stream = "S", true + } else { + header, succinctLocationBlock = "S [SKIPPED]", !r.conf.Verbose + } case types.SpecStateFailed: - reporter.stenographer.AnnounceSpecFailed(specSummary, reporter.config.Succinct, reporter.config.FullTrace) + highlightColor, header = "{{red}}", fmt.Sprintf("%s [FAILED]", denoter) + r.failures = append(r.failures, summary) + case types.SpecStatePanicked: + highlightColor, header = "{{magenta}}", fmt.Sprintf("%s! [PANICKED]", denoter) + r.failures = append(r.failures, summary) + case types.SpecStateInterrupted: + highlightColor, header = "{{orange}}", fmt.Sprintf("%s! [INTERRUPTED]", denoter) + r.failures = append(r.failures, summary) + } + + // Emit stream and return + if stream { + r.emit(r.f(highlightColor + header + "{{/}}")) + return + } + + // Emit header + r.emitDelimiter() + if includeRuntime { + header = r.f("%s [%.3f seconds]", header, summary.RunTime.Seconds()) } + r.emitBlock(r.f(highlightColor + header + "{{/}}")) - reporter.specSummaries = append(reporter.specSummaries, specSummary) + // Emit Code Location Block + r.emitBlock(r.codeLocationBlock(summary, highlightColor, succinctLocationBlock)) + + //Emit Stdout/Stderr Output + if hasStd { + r.emitBlock("\n") + r.emitBlock(r.fi(1, "{{gray}}Begin Captured StdOut/StdErr Output >>{{/}}")) + r.emitBlock(r.fi(2, "%s", summary.CapturedStdOutErr)) + r.emitBlock(r.fi(1, "{{gray}}<< End Captured StdOut/StdErr Output{{/}}")) + } + + //Emit Captured GinkgoWriter Output + if emitGinkgoWriterOutput && hasGW { + r.emitBlock("\n") + r.emitBlock(r.fi(1, "{{gray}}Begin Captured GinkgoWriter Output >>{{/}}")) + r.emitBlock(r.fi(2, "%s", summary.CapturedGinkgoWriterOutput)) + r.emitBlock(r.fi(1, "{{gray}}<< End Captured GinkgoWriter Output{{/}}")) + } + + // Emit Failure Message + if !summary.Failure.IsZero() { + r.emitBlock("\n") + r.emitBlock(r.fi(1, highlightColor+"%s{{/}}", summary.Failure.Message)) + r.emitBlock(r.fi(1, highlightColor+"In {{bold}}[%s]{{/}}"+highlightColor+" at: {{bold}}%s{{/}}\n", summary.Failure.NodeType, summary.Failure.Location)) + if summary.Failure.ForwardedPanic != "" { + r.emitBlock("\n") + r.emitBlock(r.fi(1, highlightColor+"%s{{/}}", summary.Failure.ForwardedPanic)) + } + + if r.conf.FullTrace || summary.Failure.ForwardedPanic != "" { + r.emitBlock("\n") + r.emitBlock(r.fi(1, highlightColor+"Full Stack Trace{{/}}")) + r.emitBlock(r.fi(2, "%s", summary.Failure.Location.FullStackTrace)) + } + } + + r.emitDelimiter() } -func (reporter *DefaultReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { - reporter.stenographer.SummarizeFailures(reporter.specSummaries) - reporter.stenographer.AnnounceSpecRunCompletion(summary, reporter.config.Succinct) +func (r *DefaultReporter) SpecSuiteDidEnd(summary types.SuiteSummary) { + if len(r.failures) > 1 { + r.emitBlock("\n\n") + r.emitBlock(r.f("{{red}}{{bold}}Summarizing %d Failures:{{/}}", len(r.failures))) + for _, summary := range r.failures { + highlightColor, heading := "{{red}}", "[FAIL]" + if summary.State.Is(types.SpecStateInterrupted) { + highlightColor, heading = "{{orange}}", "[INTERRUPTED]" + } else if summary.State.Is(types.SpecStatePanicked) { + highlightColor, heading = "{{magenta}}", "[PANICKED!]" + } + + locationBlock := r.codeLocationBlock(summary, highlightColor, true) + r.emitBlock(r.fi(1, highlightColor+"%s{{/}} %s", heading, locationBlock)) + } + } + + //summarize the suite + if r.conf.Succinct && summary.SuiteSucceeded { + r.emit(r.f(" {{green}}SUCCESS!{{/}} %s ", summary.RunTime)) + return + } + + r.emitBlock("\n") + color, status := "{{green}}{{bold}}", "SUCCESS!" + if !summary.SuiteSucceeded { + color, status = "{{red}}{{bold}}", "FAIL!" + if r.hasFailOnPending && len(r.failures) == 0 && summary.NumberOfPendingSpecs > 0 { + color, status = "{{yellow}}{{bold}}", "FAIL! - Detected pending specs and --fail-on-pending is set" + } + } + r.emitBlock(r.f(color+"Ran %d of %d Specs in %.3f seconds{{/}}", summary.NumberOfSpecsThatRan(), summary.NumberOfTotalSpecs, summary.RunTime.Seconds())) + r.emit(r.f(color+"%s{{/}} -- ", status)) + r.emit(r.f("{{green}}{{bold}}%d Passed{{/}} | ", summary.NumberOfPassedSpecs)) + r.emit(r.f("{{red}}{{bold}}%d Failed{{/}} | ", summary.NumberOfFailedSpecs)) + if summary.NumberOfFlakedSpecs > 0 { + r.emit(r.f("{{light-yellow}}{{bold}}%d Flaked{{/}} | ", summary.NumberOfFlakedSpecs)) + } + r.emit(r.f("{{yellow}}{{bold}}%d Pending{{/}} | ", summary.NumberOfPendingSpecs)) + r.emit(r.f("{{cyan}}{{bold}}%d Skipped{{/}}\n", summary.NumberOfSkippedSpecs)) +} + +/* Emitting to the writer */ + +func (r *DefaultReporter) emit(s string) { + if len(s) > 0 { + r.lastChar = s[len(s)-1:] + r.lastEmissionWasDelimiter = false + r.writer.Write([]byte(s)) + } +} + +func (r *DefaultReporter) emitBlock(s string) { + if len(s) > 0 { + if r.lastChar != "\n" { + r.emit("\n") + } + r.emit(s) + if r.lastChar != "\n" { + r.emit("\n") + } + } +} + +func (r *DefaultReporter) emitDelimiter() { + if r.lastEmissionWasDelimiter { + return + } + r.emitBlock(r.f("{{gray}}%s{{/}}", strings.Repeat("-", 30))) + r.lastEmissionWasDelimiter = true +} + +/* Rendering text */ + +func (r *DefaultReporter) f(format string, args ...interface{}) string { + return r.formatter.F(format, args...) +} + +func (r *DefaultReporter) fi(indentation uint, format string, args ...interface{}) string { + return r.formatter.Fi(indentation, format, args...) +} + +func (r *DefaultReporter) cycleJoin(elements []string, joiner string) string { + return r.formatter.CycleJoin(elements, joiner, []string{"{{/}}", "{{gray}}"}) +} + +func (r *DefaultReporter) codeLocationBlock(summary types.Summary, highlightColor string, succinct bool) string { + out := "" + + if summary.LeafNodeType.Is(types.NodeTypesForSuiteSetup...) { + out = r.f(highlightColor+"{{bold}}[%s]{{/}}\n", summary.LeafNodeType) + if summary.Failure.IsZero() { + out += r.f("{{gray}}%s{{/}}\n", summary.LeafNodeLocation) + } else { + out += r.f("{{gray}}%s{{/}}\n", summary.Failure.Location) + } + return out + } + + if succinct { + texts := make([]string, len(summary.NodeTexts)) + copy(texts, summary.NodeTexts) + var codeLocation = summary.NodeLocations[len(summary.NodeLocations)-1] + if !summary.Failure.IsZero() { + codeLocation = summary.Failure.Location + if summary.Failure.NodeIndex == -1 { + texts = append([]string{r.f(highlightColor+"{{bold}}[%s]{{/}}", summary.Failure.NodeType)}, texts...) + } else if summary.Failure.NodeIndex < len(texts) { + i := summary.Failure.NodeIndex + texts[i] = r.f(highlightColor+"{{bold}}[%s] %s{{/}}", summary.Failure.NodeType, texts[i]) + } + } + out += r.f("%s\n", r.cycleJoin(texts, " ")) + out += r.f("{{gray}}%s{{/}}", codeLocation) + + return out + } + + indentation := uint(0) + if !summary.Failure.IsZero() && summary.Failure.NodeIndex == -1 { + out += r.fi(indentation, highlightColor+"{{bold}}TOP-LEVEL [%s]{{/}}\n", summary.Failure.NodeType) + out += r.fi(indentation, "{{gray}}%s{{/}}\n", summary.Failure.Location) + indentation += 1 + } + + for i := range summary.NodeTexts { + if !summary.Failure.IsZero() && i == summary.Failure.NodeIndex { + out += r.fi(indentation, highlightColor+"{{bold}}%s [%s]{{/}}\n", summary.NodeTexts[i], summary.Failure.NodeType) + } else { + out += r.fi(indentation, "%s\n", summary.NodeTexts[i]) + } + out += r.fi(indentation, "{{gray}}%s{{/}}\n", summary.NodeLocations[i]) + indentation += 1 + } + + return out } diff --git a/reporters/default_reporter_test.go b/reporters/default_reporter_test.go index 493d8bace2..1f399c1a3d 100644 --- a/reporters/default_reporter_test.go +++ b/reporters/default_reporter_test.go @@ -1,455 +1,887 @@ package reporters_test import ( + "reflect" + "runtime" + "strings" "time" . "github.com/onsi/ginkgo" "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/extensions/table" "github.com/onsi/ginkgo/reporters" - st "github.com/onsi/ginkgo/reporters/stenographer" "github.com/onsi/ginkgo/types" . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/gbytes" ) -var _ = Describe("DefaultReporter", func() { - var ( - reporter *reporters.DefaultReporter - reporterConfig config.DefaultReporterConfigType - stenographer *st.FakeStenographer - - ginkgoConfig config.GinkgoConfigType - suite *types.SuiteSummary - spec *types.SpecSummary - ) - - BeforeEach(func() { - stenographer = st.NewFakeStenographer() - reporterConfig = config.DefaultReporterConfigType{ - NoColor: false, - SlowSpecThreshold: 0.1, - NoisyPendings: false, - NoisySkippings: false, - Verbose: true, - FullTrace: true, +type StackTrace string + +const DELIMITER = `{{gray}}------------------------------{{/}}` + +var cl0 = types.CodeLocation{FileName: "cl0.go", LineNumber: 12, FullStackTrace: "full-trace\ncl-0"} +var cl1 = types.CodeLocation{FileName: "cl1.go", LineNumber: 37, FullStackTrace: "full-trace\ncl-1"} +var cl2 = types.CodeLocation{FileName: "cl2.go", LineNumber: 80, FullStackTrace: "full-trace\ncl-2"} +var cl3 = types.CodeLocation{FileName: "cl3.go", LineNumber: 103, FullStackTrace: "full-trace\ncl-3"} + +func CLS(cls ...types.CodeLocation) []types.CodeLocation { return cls } +func CTS(componentTexts ...string) []string { return componentTexts } + +type Location types.CodeLocation +type ForwardedPanic string + +// convenience helper to quickly make Failures +func F(options ...interface{}) types.Failure { + failure := types.Failure{} + for _, option := range options { + switch reflect.TypeOf(option) { + case reflect.TypeOf(""): + failure.Message = option.(string) + case reflect.TypeOf(Location{}): + failure.Location = types.CodeLocation(option.(Location)) + case reflect.TypeOf(ForwardedPanic("")): + failure.ForwardedPanic = string(option.(ForwardedPanic)) + case reflect.TypeOf(0): + failure.NodeIndex = option.(int) + case reflect.TypeOf(types.NodeTypeIt): + failure.NodeType = option.(types.NodeType) } + } + return failure +} + +type STD string +type GW string + +// convenience helper to quickly make summaries +func S(options ...interface{}) types.Summary { + summary := types.Summary{ + LeafNodeType: types.NodeTypeIt, + State: types.SpecStatePassed, + NumAttempts: 1, + RunTime: time.Second, + } + for _, option := range options { + switch reflect.TypeOf(option) { + case reflect.TypeOf([]string{}): + summary.NodeTexts = option.([]string) + case reflect.TypeOf([]types.CodeLocation{}): + summary.NodeLocations = option.([]types.CodeLocation) + case reflect.TypeOf(types.NodeTypeIt): + summary.LeafNodeType = option.(types.NodeType) + case reflect.TypeOf(types.CodeLocation{}): + summary.LeafNodeLocation = option.(types.CodeLocation) + case reflect.TypeOf(types.SpecStatePassed): + summary.State = option.(types.SpecState) + case reflect.TypeOf(time.Second): + summary.RunTime = option.(time.Duration) + case reflect.TypeOf(types.Failure{}): + summary.Failure = option.(types.Failure) + case reflect.TypeOf(0): + summary.NumAttempts = option.(int) + case reflect.TypeOf(STD("")): + summary.CapturedStdOutErr = string(option.(STD)) + case reflect.TypeOf(GW("")): + summary.CapturedGinkgoWriterOutput = string(option.(GW)) + } + } + return summary +} - reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) - }) - - call := st.NewFakeStenographerCall - - Describe("SpecSuiteWillBegin", func() { - BeforeEach(func() { - suite = &types.SuiteSummary{ - SuiteDescription: "A Sweet Suite", - NumberOfTotalSpecs: 10, - NumberOfSpecsThatWillBeRun: 8, - } - - ginkgoConfig = config.GinkgoConfigType{ - RandomSeed: 1138, - RandomizeAllSpecs: true, - } - }) - - Context("when a serial (non-parallel) suite begins", func() { - BeforeEach(func() { - ginkgoConfig.ParallelTotal = 1 - - reporter.SpecSuiteWillBegin(ginkgoConfig, suite) - }) - - It("should announce the suite, then announce the number of specs", func() { - Ω(stenographer.Calls()).Should(HaveLen(2)) - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuite", "A Sweet Suite", ginkgoConfig.RandomSeed, true, false))) - Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceNumberOfSpecs", 8, 10, false))) - }) - }) - - Context("when a parallel suite begins", func() { - BeforeEach(func() { - ginkgoConfig.ParallelTotal = 2 - ginkgoConfig.ParallelNode = 1 - suite.NumberOfSpecsBeforeParallelization = 20 - - reporter.SpecSuiteWillBegin(ginkgoConfig, suite) - }) - - It("should announce the suite, announce that it's a parallel run, then announce the number of specs", func() { - Ω(stenographer.Calls()).Should(HaveLen(2)) - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuite", "A Sweet Suite", ginkgoConfig.RandomSeed, true, false))) - Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceParallelRun", 1, 2, false))) - }) - }) - }) - - Describe("BeforeSuiteDidRun", func() { - Context("when the BeforeSuite passes", func() { - It("should announce nothing", func() { - reporter.BeforeSuiteDidRun(&types.SetupSummary{ - State: types.SpecStatePassed, - }) - - Ω(stenographer.Calls()).Should(BeEmpty()) - }) - }) - - Context("when the BeforeSuite fails", func() { - It("should announce the failure", func() { - summary := &types.SetupSummary{ - State: types.SpecStateFailed, - } - reporter.BeforeSuiteDidRun(summary) - - Ω(stenographer.Calls()).Should(HaveLen(1)) - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceBeforeSuiteFailure", summary, false, true))) - }) - }) - }) - - Describe("AfterSuiteDidRun", func() { - Context("when the AfterSuite passes", func() { - It("should announce nothing", func() { - reporter.AfterSuiteDidRun(&types.SetupSummary{ - State: types.SpecStatePassed, - }) - - Ω(stenographer.Calls()).Should(BeEmpty()) - }) - }) - - Context("when the AfterSuite fails", func() { - It("should announce the failure", func() { - summary := &types.SetupSummary{ - State: types.SpecStateFailed, - } - reporter.AfterSuiteDidRun(summary) - - Ω(stenographer.Calls()).Should(HaveLen(1)) - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceAfterSuiteFailure", summary, false, true))) - }) - }) - }) - - Describe("SpecWillRun", func() { - Context("When running in verbose mode", func() { - Context("and the spec will run", func() { - BeforeEach(func() { - spec = &types.SpecSummary{} - reporter.SpecWillRun(spec) - }) - - It("should announce that the spec will run", func() { - Ω(stenographer.Calls()).Should(HaveLen(1)) - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecWillRun", spec))) - }) - }) - - Context("and the spec will not run", func() { - Context("because it is pending", func() { - BeforeEach(func() { - spec = &types.SpecSummary{ - State: types.SpecStatePending, - } - reporter.SpecWillRun(spec) - }) - - It("should announce nothing", func() { - Ω(stenographer.Calls()).Should(BeEmpty()) - }) - }) - - Context("because it is skipped", func() { - BeforeEach(func() { - spec = &types.SpecSummary{ - State: types.SpecStateSkipped, - } - reporter.SpecWillRun(spec) - }) +type ConfigFlags uint8 - It("should announce nothing", func() { - Ω(stenographer.Calls()).Should(BeEmpty()) - }) - }) - }) - }) +const ( + Succinct ConfigFlags = 1 << iota + Verbose + ReportPassed + FullTrace +) - Context("When running in verbose & succinct mode", func() { - BeforeEach(func() { - reporterConfig.Succinct = true - reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) - spec = &types.SpecSummary{} - reporter.SpecWillRun(spec) - }) +func (cf ConfigFlags) Has(flag ConfigFlags) bool { return cf&flag != 0 } + +func C(flags ...ConfigFlags) config.DefaultReporterConfigType { + f := ConfigFlags(0) + if len(flags) > 0 { + f = flags[0] + } + Ω(f.Has(Verbose) && f.Has(Succinct)).Should(BeFalse(), "Being both verbose and succinct is a configuration error") + return config.DefaultReporterConfigType{ + NoColor: true, + SlowSpecThreshold: SlowSpecThreshold, + Succinct: f.Has(Succinct), + Verbose: f.Has(Verbose), + ReportPassed: f.Has(ReportPassed), + FullTrace: f.Has(FullTrace), + } +} + +const SlowSpecThreshold = 3.0 - It("should announce nothing", func() { - Ω(stenographer.Calls()).Should(BeEmpty()) - }) - }) - - Context("When not running in verbose mode", func() { - BeforeEach(func() { - reporterConfig.Verbose = false - reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) - spec = &types.SpecSummary{} - reporter.SpecWillRun(spec) - }) +var _ = Describe("DefaultReporter", func() { + var DENOTER = "•" + var RETRY_DENOTER = "↺" + if runtime.GOOS == "windows" { + DENOTER = "+" + RETRY_DENOTER = "R" + } + + var buf *gbytes.Buffer + verifyExpectedOutput := func(expected []string) { + if len(expected) == 0 { + ExpectWithOffset(1, buf.Contents()).Should(BeEmpty()) + } else { + ExpectWithOffset(1, string(buf.Contents())).Should(Equal(strings.Join(expected, "\n"))) + } + } - It("should announce nothing", func() { - Ω(stenographer.Calls()).Should(BeEmpty()) - }) - }) + BeforeEach(func() { + buf = gbytes.NewBuffer() + format.CharactersAroundMismatchToInclude = 100 }) - Describe("SpecDidComplete", func() { - JustBeforeEach(func() { - reporter.SpecDidComplete(spec) - }) - - BeforeEach(func() { - spec = &types.SpecSummary{} - }) - - Context("When the spec passed", func() { - BeforeEach(func() { - spec.State = types.SpecStatePassed - }) - - Context("When the spec was a measurement", func() { - BeforeEach(func() { - spec.IsMeasurement = true - }) - - It("should announce the measurement", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccessfulMeasurement", spec, false))) - }) - }) - - Context("When the spec is slow", func() { - BeforeEach(func() { - spec.RunTime = time.Second - }) - - It("should announce that it was slow", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccessfulSlowSpec", spec, false))) - }) - }) - - Context("When the spec is successful", func() { - It("should announce the successful spec", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccessfulSpec", spec))) - }) - - Context("When ReportPassed flag is set", func() { - BeforeEach(func() { - reporterConfig.ReportPassed = true - reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) - spec.CapturedOutput = "test scenario" - }) - - It("should announce the captured output", func() { - Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceCapturedOutput", spec.CapturedOutput))) - }) - }) - }) - }) - - Context("When the spec is pending", func() { - BeforeEach(func() { - spec.State = types.SpecStatePending - }) - - It("should announce the pending spec, succinctly", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnouncePendingSpec", spec, false))) - }) - }) - - Context("When the spec is skipped", func() { - BeforeEach(func() { - spec.State = types.SpecStateSkipped - }) - - It("should announce the skipped spec, succinctly", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSkippedSpec", spec, true, true))) - }) - }) - - Context("When the spec timed out", func() { - BeforeEach(func() { - spec.State = types.SpecStateTimedOut - }) - - It("should announce the timedout spec", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecTimedOut", spec, false, true))) - }) - }) - - Context("When the spec panicked", func() { - BeforeEach(func() { - spec.State = types.SpecStatePanicked - }) - - It("should announce the panicked spec", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecPanicked", spec, false, true))) - }) - }) - - Context("When the spec failed", func() { - BeforeEach(func() { - spec.State = types.SpecStateFailed - }) - - It("should announce the failed spec", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecFailed", spec, false, true))) - }) - }) - - Context("in noisy pendings mode", func() { - BeforeEach(func() { - reporterConfig.Succinct = false - reporterConfig.NoisyPendings = true - reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) - }) - - Context("When the spec is pending", func() { - BeforeEach(func() { - spec.State = types.SpecStatePending - }) - - It("should announce the pending spec, noisily", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnouncePendingSpec", spec, true))) - }) - }) - }) - - Context("in noisy skippings mode", func() { - BeforeEach(func() { - reporterConfig.Succinct = false - reporterConfig.NoisySkippings = true - reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) - }) - - Context("When the spec is skipped", func() { - BeforeEach(func() { - spec.State = types.SpecStateSkipped - }) - - It("should announce the skipped spec, noisily", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSkippedSpec", spec, false, true))) - }) - }) - }) - - Context("in succinct mode", func() { - BeforeEach(func() { - reporterConfig.Succinct = true - reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) - }) - - Context("When the spec passed", func() { - BeforeEach(func() { - spec.State = types.SpecStatePassed - }) - - Context("When the spec was a measurement", func() { - BeforeEach(func() { - spec.IsMeasurement = true - }) - - It("should announce the measurement", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccessfulMeasurement", spec, true))) - }) - }) - - Context("When the spec is slow", func() { - BeforeEach(func() { - spec.RunTime = time.Second - }) - - It("should announce that it was slow", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccessfulSlowSpec", spec, true))) - }) - }) - - Context("When the spec is successful", func() { - It("should announce the successful spec", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSuccessfulSpec", spec))) - }) - - Context("When ReportPassed flag is set", func() { - BeforeEach(func() { - reporterConfig.ReportPassed = true - reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) - spec.CapturedOutput = "test scenario" - }) - - It("should announce the captured output", func() { - Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceCapturedOutput", spec.CapturedOutput))) - }) - }) - }) - }) - - Context("When the spec is pending", func() { - BeforeEach(func() { - spec.State = types.SpecStatePending - }) - - It("should announce the pending spec, succinctly", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnouncePendingSpec", spec, false))) - }) - }) - - Context("When the spec is skipped", func() { - BeforeEach(func() { - spec.State = types.SpecStateSkipped - }) - - It("should announce the skipped spec", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSkippedSpec", spec, true, true))) - }) - }) - - Context("When the spec timed out", func() { - BeforeEach(func() { - spec.State = types.SpecStateTimedOut - }) - - It("should announce the timedout spec", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecTimedOut", spec, true, true))) - }) - }) + DescribeTable("Rendering SpecSuiteWillBegin", + func(conf config.DefaultReporterConfigType, gConf config.GinkgoConfigType, summary types.SuiteSummary, expected ...string) { + reporter := reporters.NewDefaultReporterUnderTest(conf, buf) + reporter.SpecSuiteWillBegin(gConf, summary) + verifyExpectedOutput(expected) + }, + Entry("Default Behavior", + C(), + config.GinkgoConfigType{RandomSeed: 17, ParallelTotal: 1}, + types.SuiteSummary{SuiteDescription: "My Suite", NumberOfSpecsThatWillBeRun: 15, NumberOfTotalSpecs: 20}, + "Running Suite: My Suite", + "=======================", + "Random Seed: {{bold}}17{{/}}", + "", + "Will run {{bold}}15{{/}} of {{bold}}20{{/}} specs", + "", + ), + Entry("When configured to randomize all specs", + C(), + config.GinkgoConfigType{RandomSeed: 17, ParallelTotal: 1, RandomizeAllSpecs: true}, + types.SuiteSummary{SuiteDescription: "My Suite", NumberOfSpecsThatWillBeRun: 15, NumberOfTotalSpecs: 20}, + "Running Suite: My Suite", + "=======================", + "Random Seed: {{bold}}17{{/}} - will randomize all specs", + "", + "Will run {{bold}}15{{/}} of {{bold}}20{{/}} specs", + "", + ), + Entry("when configured to run in parallel", + C(), + config.GinkgoConfigType{RandomSeed: 17, ParallelTotal: 3}, + types.SuiteSummary{SuiteDescription: "My Suite", NumberOfSpecsThatWillBeRun: 15, NumberOfTotalSpecs: 20}, + "Running Suite: My Suite", + "=======================", + "Random Seed: {{bold}}17{{/}}", + "", + "Will run {{bold}}15{{/}} of {{bold}}20{{/}} specs", + "Running in parallel across {{bold}}3{{/}} nodes", + "", + ), + Entry("when succinct and in series", + C(Succinct), + config.GinkgoConfigType{RandomSeed: 17, ParallelTotal: 1}, + types.SuiteSummary{SuiteDescription: "My Suite", NumberOfSpecsThatWillBeRun: 15, NumberOfTotalSpecs: 20}, + "[17] {{bold}}My Suite{{/}} - 15/20 specs ", + ), + Entry("when succinct and in parallel", + C(Succinct), + config.GinkgoConfigType{RandomSeed: 17, ParallelTotal: 3}, + types.SuiteSummary{SuiteDescription: "My Suite", NumberOfSpecsThatWillBeRun: 15, NumberOfTotalSpecs: 20}, + "[17] {{bold}}My Suite{{/}} - 15/20 specs - 3 nodes ", + ), + ) - Context("When the spec panicked", func() { - BeforeEach(func() { - spec.State = types.SpecStatePanicked - }) + DescribeTable("WillRun", + func(conf config.DefaultReporterConfigType, summary types.Summary, output ...string) { + reporter := reporters.NewDefaultReporterUnderTest(conf, buf) + reporter.WillRun(summary) + verifyExpectedOutput(output) + }, + Entry("when not verbose, it emits nothing", C(), S(CTS("A"), CLS(cl0))), + Entry("pending specs are not emitted", C(Verbose), S(types.SpecStatePending)), + Entry("skipped specs are not emitted", C(Verbose), S(types.SpecStateSkipped)), + Entry("setup nodes", C(Verbose), + S(types.NodeTypeBeforeSuite, cl0), + DELIMITER, + "{{bold}}[BeforeSuite]{{/}}", + "{{gray}}"+cl0.String()+"{{/}}", + "", + ), + Entry("top-level it nodes", C(Verbose), + S(CTS("My Test"), CLS(cl0)), + DELIMITER, + "{{bold}}My Test{{/}}", + "{{gray}}"+cl0.String()+"{{/}}", + "", + ), + Entry("nested it nodes", C(Verbose), + S(CTS("Container", "Nested Container", "My Test"), CLS(cl0, cl1, cl2)), + DELIMITER, + "{{/}}Container {{gray}}Nested Container{{/}}", + " {{bold}}My Test{{/}}", + " {{gray}}"+cl2.String()+"{{/}}", + "", + ), + ) - It("should announce the panicked spec", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecPanicked", spec, true, true))) - }) - }) + DescribeTable("DidRun", + func(conf config.DefaultReporterConfigType, summary types.Summary, output ...string) { + reporter := reporters.NewDefaultReporterUnderTest(conf, buf) + reporter.DidRun(summary) + verifyExpectedOutput(output) + }, + // Passing Tests + Entry("a passing test", + C(), + S(CTS("A"), CLS(cl0)), + "{{green}}"+DENOTER+"{{/}}", + ), + Entry("a passing test that was retried", + C(), + S(CTS("A", "B"), CLS(cl0, cl1), 2), + DELIMITER, + "{{green}}"+RETRY_DENOTER+" [FLAKEY TEST - TOOK 2 ATTEMPTS TO PASS] [1.000 seconds]{{/}}", + "{{/}}A {{gray}}B{{/}}", + "{{gray}}"+cl1.String()+"{{/}}", + DELIMITER, + "", + ), + Entry("a passing test that has ginkgo writer output", + C(), + S(CTS("A"), CLS(cl0), GW("GINKGO-WRITER-OUTPUT")), + "{{green}}"+DENOTER+"{{/}}", + ), + Entry("a passing test that has ginkgo writer output, with ReportPassed configured", + C(ReportPassed), + S(CTS("A", "B"), CLS(cl0, cl1), GW("GINKGO-WRITER-OUTPUT\nSHOULD EMIT")), + DELIMITER, + "{{green}}"+DENOTER+" [1.000 seconds]{{/}}", + "{{/}}A {{gray}}B{{/}}", + "{{gray}}"+cl1.String()+"{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GINKGO-WRITER-OUTPUT", + " SHOULD EMIT", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + DELIMITER, + "", + ), + Entry("a passing test that has ginkgo writer output, with Verbose configured", + C(Verbose), + S(CTS("A"), CLS(cl0), GW("GINKGO-WRITER-OUTPUT\nSHOULD EMIT")), + DELIMITER, + "{{green}}"+DENOTER+" [1.000 seconds]{{/}}", + "A", + "{{gray}}"+cl0.String()+"{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GINKGO-WRITER-OUTPUT", + " SHOULD EMIT", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + DELIMITER, + "", + ), + Entry("a slow passing test", + C(), + S(CTS("A", "B"), CLS(cl0, cl1), time.Minute, GW("GINKGO-WRITER-OUTPUT")), + DELIMITER, + "{{green}}"+DENOTER+" [SLOW TEST] [60.000 seconds]{{/}}", + "{{/}}A {{gray}}B{{/}}", + "{{gray}}"+cl1.String()+"{{/}}", + DELIMITER, + "", + ), + Entry("a passing test with captured stdout", + C(), + S(CTS("A", "B"), CLS(cl0, cl1), GW("GINKGO-WRITER-OUTPUT"), STD("STD-OUTPUT\nSHOULD EMIT")), + DELIMITER, + "{{green}}"+DENOTER+" [1.000 seconds]{{/}}", + "{{/}}A {{gray}}B{{/}}", + "{{gray}}"+cl1.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " SHOULD EMIT", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + DELIMITER, + "", + ), + Entry("a passing suite setup emits nothing", + C(), + S(types.NodeTypeBeforeSuite, cl0, GW("GINKGO-WRITER-OUTPUT")), + ), + Entry("a passing suite setup with verbose always emits", + C(Verbose), + S(types.NodeTypeBeforeSuite, cl0, GW("GINKGO-WRITER-OUTPUT")), + DELIMITER, + "{{green}}[BeforeSuite] PASSED [1.000 seconds]{{/}}", + "{{green}}{{bold}}[BeforeSuite]{{/}}", + "{{gray}}"+cl0.String()+"{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GINKGO-WRITER-OUTPUT", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + DELIMITER, + "", + ), + Entry("a passing suite setup with captured stdout always emits", + C(), + S(types.NodeTypeBeforeSuite, cl0, STD("STD-OUTPUT")), + DELIMITER, + "{{green}}[BeforeSuite] PASSED [1.000 seconds]{{/}}", + "{{green}}{{bold}}[BeforeSuite]{{/}}", + "{{gray}}"+cl0.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + DELIMITER, + "", + ), + + // Pending Tests + Entry("a pending test when succinct", + C(Succinct), + S(CTS("A"), CLS(cl0), types.SpecStatePending, GW("GW-OUTPUT"), STD("STD-OUTPUT")), + "{{yellow}}P{{/}}", + ), + Entry("a pending test normally", + C(), + S(CTS("A"), CLS(cl0), types.SpecStatePending, GW("GW-OUTPUT")), + DELIMITER, + "{{yellow}}P [PENDING]{{/}}", + "{{/}}A{{/}}", + "{{gray}}cl0.go:12{{/}}", + DELIMITER, + "", + ), + Entry("a pending test when verbose", + C(Verbose), + S(CTS("A", "B"), CLS(cl0, cl1), types.SpecStatePending, GW("GW-OUTPUT"), STD("STD-OUTPUT")), + DELIMITER, + "{{yellow}}P [PENDING]{{/}}", + "A", + "{{gray}}"+cl0.String()+"{{/}}", + " B", + " {{gray}}"+cl1.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + DELIMITER, + "", + ), + // Skipped Tests + Entry("a skipped test when succinct", + C(Succinct), + S(CTS("A"), CLS(cl0), types.SpecStateSkipped, GW("GW-OUTPUT"), STD("STD-OUTPUT"), + F("user skipped"), + ), + "{{cyan}}S{{/}}", + ), + Entry("a skipped test without a failure message", + C(), + S(CTS("A"), CLS(cl0), types.SpecStateSkipped, GW("GW-OUTPUT")), + "{{cyan}}S{{/}}", + ), + Entry("a skipped test with a failure message and verbose is *not* configured", + C(), + S(CTS("A", "B"), CLS(cl0, cl1), types.SpecStateSkipped, GW("GW-OUTPUT"), STD("STD-OUTPUT"), + F("user skipped", types.NodeTypeIt, Location(cl2), 1), + ), + DELIMITER, + "{{cyan}}S [SKIPPED] [1.000 seconds]{{/}}", + "{{/}}A {{gray}}{{cyan}}{{bold}}[It] B{{/}}{{/}}", + "{{gray}}"+cl2.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{cyan}}user skipped{{/}}", + " {{cyan}}In {{bold}}[It]{{/}}{{cyan}} at: {{bold}}"+cl2.String()+"{{/}}", + DELIMITER, + "", + ), + Entry("a skipped test with a failure message and verbose *is* configured", + C(Verbose), + S(CTS("A", "B"), CLS(cl0, cl1), types.SpecStateSkipped, GW("GW-OUTPUT"), STD("STD-OUTPUT"), + F("user skipped", types.NodeTypeIt, Location(cl2), 1), + ), + DELIMITER, + "{{cyan}}S [SKIPPED] [1.000 seconds]{{/}}", + "A", + "{{gray}}"+cl0.String()+"{{/}}", + " {{cyan}}{{bold}}B [It]{{/}}", + " {{gray}}"+cl1.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{cyan}}user skipped{{/}}", + " {{cyan}}In {{bold}}[It]{{/}}{{cyan}} at: {{bold}}"+cl2.String()+"{{/}}", + DELIMITER, + "", + ), + + //Failed tests + Entry("when a test has failed in an It", + C(), + S(CTS("Describe A", "Context B", "The Test"), CLS(cl0, cl1, cl2), + types.SpecStateFailed, 2, + GW("GW-OUTPUT\nIS EMITTED"), STD("STD-OUTPUT\nIS EMITTED"), + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeIt, 2), + ), + DELIMITER, + "{{red}}"+DENOTER+" [FAILED] [1.000 seconds]{{/}}", + "Describe A", + "{{gray}}"+cl0.String()+"{{/}}", + " Context B", + " {{gray}}"+cl1.String()+"{{/}}", + " {{red}}{{bold}}The Test [It]{{/}}", + " {{gray}}"+cl2.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{red}}FAILURE MESSAGE", + " WITH DETAILS{{/}}", + " {{red}}In {{bold}}[It]{{/}}{{red}} at: {{bold}}"+cl3.String()+"{{/}}", + DELIMITER, + "", + ), + Entry("when a test has failed in a setup/teardown node", + C(), + S(CTS("Describe A", "Context B", "The Test"), CLS(cl0, cl1, cl2), + types.SpecStateFailed, 2, + GW("GW-OUTPUT\nIS EMITTED"), STD("STD-OUTPUT\nIS EMITTED"), + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeJustBeforeEach, 1), + ), + DELIMITER, + "{{red}}"+DENOTER+" [FAILED] [1.000 seconds]{{/}}", + "Describe A", + "{{gray}}"+cl0.String()+"{{/}}", + " {{red}}{{bold}}Context B [JustBeforeEach]{{/}}", + " {{gray}}"+cl1.String()+"{{/}}", + " The Test", + " {{gray}}"+cl2.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{red}}FAILURE MESSAGE", + " WITH DETAILS{{/}}", + " {{red}}In {{bold}}[JustBeforeEach]{{/}}{{red}} at: {{bold}}"+cl3.String()+"{{/}}", + DELIMITER, + "", + ), + Entry("when a test has failed and Succinct is configured", + C(Succinct), + S(CTS("Describe A", "Context B", "The Test"), CLS(cl0, cl1, cl2), + types.SpecStateFailed, 2, + GW("GW-OUTPUT\nIS EMITTED"), STD("STD-OUTPUT\nIS EMITTED"), + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeJustBeforeEach, 1), + ), + DELIMITER, + "{{red}}"+DENOTER+" [FAILED] [1.000 seconds]{{/}}", + "{{/}}Describe A {{gray}}{{red}}{{bold}}[JustBeforeEach] Context B{{/}} {{/}}The Test{{/}}", + "{{gray}}"+cl3.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{red}}FAILURE MESSAGE", + " WITH DETAILS{{/}}", + " {{red}}In {{bold}}[JustBeforeEach]{{/}}{{red}} at: {{bold}}"+cl3.String()+"{{/}}", + DELIMITER, + "", + ), + Entry("when a test has failed and FullTrace is configured", + C(FullTrace), + S(CTS("Describe A", "Context B", "The Test"), CLS(cl0, cl1, cl2), + types.SpecStateFailed, 2, + GW("GW-OUTPUT\nIS EMITTED"), STD("STD-OUTPUT\nIS EMITTED"), + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeJustBeforeEach, 1), + ), + DELIMITER, + "{{red}}"+DENOTER+" [FAILED] [1.000 seconds]{{/}}", + "Describe A", + "{{gray}}"+cl0.String()+"{{/}}", + " {{red}}{{bold}}Context B [JustBeforeEach]{{/}}", + " {{gray}}"+cl1.String()+"{{/}}", + " The Test", + " {{gray}}"+cl2.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{red}}FAILURE MESSAGE", + " WITH DETAILS{{/}}", + " {{red}}In {{bold}}[JustBeforeEach]{{/}}{{red}} at: {{bold}}"+cl3.String()+"{{/}}", + "", + " {{red}}Full Stack Trace{{/}}", + " full-trace", + " cl-3", + DELIMITER, + "", + ), + Entry("when a suite setup node has failed", + C(), + S(types.NodeTypeSynchronizedBeforeSuite, cl0, types.SpecStateFailed, 1, + GW("GW-OUTPUT\nIS EMITTED"), STD("STD-OUTPUT\nIS EMITTED"), + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeSynchronizedBeforeSuite, 0), + ), + DELIMITER, + "{{red}}[SynchronizedBeforeSuite] [FAILED] [1.000 seconds]{{/}}", + "{{red}}{{bold}}[SynchronizedBeforeSuite]{{/}}", + "{{gray}}"+cl3.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{red}}FAILURE MESSAGE", + " WITH DETAILS{{/}}", + " {{red}}In {{bold}}[SynchronizedBeforeSuite]{{/}}{{red}} at: {{bold}}"+cl3.String()+"{{/}}", + DELIMITER, + "", + ), + + Entry("when a test has panicked and there is no forwarded panic", + C(), + S(CTS("Describe A", "Context B", "The Test"), CLS(cl0, cl1, cl2), + types.SpecStatePanicked, 2, + GW("GW-OUTPUT\nIS EMITTED"), STD("STD-OUTPUT\nIS EMITTED"), + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeJustBeforeEach, 1), + ), + DELIMITER, + "{{magenta}}"+DENOTER+"! [PANICKED] [1.000 seconds]{{/}}", + "Describe A", + "{{gray}}"+cl0.String()+"{{/}}", + " {{magenta}}{{bold}}Context B [JustBeforeEach]{{/}}", + " {{gray}}"+cl1.String()+"{{/}}", + " The Test", + " {{gray}}"+cl2.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{magenta}}FAILURE MESSAGE", + " WITH DETAILS{{/}}", + " {{magenta}}In {{bold}}[JustBeforeEach]{{/}}{{magenta}} at: {{bold}}"+cl3.String()+"{{/}}", + DELIMITER, + "", + ), + Entry("when a test has panicked and there is a forwarded panic", + C(), + S(CTS("Describe A", "Context B", "The Test"), CLS(cl0, cl1, cl2), + types.SpecStatePanicked, 2, + GW("GW-OUTPUT\nIS EMITTED"), STD("STD-OUTPUT\nIS EMITTED"), + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeJustBeforeEach, 1, ForwardedPanic("the panic\nthusly forwarded")), + ), + DELIMITER, + "{{magenta}}"+DENOTER+"! [PANICKED] [1.000 seconds]{{/}}", + "Describe A", + "{{gray}}"+cl0.String()+"{{/}}", + " {{magenta}}{{bold}}Context B [JustBeforeEach]{{/}}", + " {{gray}}"+cl1.String()+"{{/}}", + " The Test", + " {{gray}}"+cl2.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{magenta}}FAILURE MESSAGE", + " WITH DETAILS{{/}}", + " {{magenta}}In {{bold}}[JustBeforeEach]{{/}}{{magenta}} at: {{bold}}"+cl3.String()+"{{/}}", + "", + " {{magenta}}the panic", + " thusly forwarded{{/}}", + "", + " {{magenta}}Full Stack Trace{{/}}", + " full-trace", + " cl-3", + DELIMITER, + "", + ), + + Entry("when a test is interrupted", + C(), + S(CTS("Describe A", "Context B", "The Test"), CLS(cl0, cl1, cl2), + types.SpecStateInterrupted, 2, + GW("GW-OUTPUT\nIS EMITTED"), STD("STD-OUTPUT\nIS EMITTED"), + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeJustBeforeEach, 1), + ), + DELIMITER, + "{{orange}}"+DENOTER+"! [INTERRUPTED] [1.000 seconds]{{/}}", + "Describe A", + "{{gray}}"+cl0.String()+"{{/}}", + " {{orange}}{{bold}}Context B [JustBeforeEach]{{/}}", + " {{gray}}"+cl1.String()+"{{/}}", + " The Test", + " {{gray}}"+cl2.String()+"{{/}}", + "", + " {{gray}}Begin Captured StdOut/StdErr Output >>{{/}}", + " STD-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured StdOut/StdErr Output{{/}}", + "", + " {{gray}}Begin Captured GinkgoWriter Output >>{{/}}", + " GW-OUTPUT", + " IS EMITTED", + " {{gray}}<< End Captured GinkgoWriter Output{{/}}", + "", + " {{orange}}FAILURE MESSAGE", + " WITH DETAILS{{/}}", + " {{orange}}In {{bold}}[JustBeforeEach]{{/}}{{orange}} at: {{bold}}"+cl3.String()+"{{/}}", + DELIMITER, + "", + ), + ) - Context("When the spec failed", func() { - BeforeEach(func() { - spec.State = types.SpecStateFailed - }) + DescribeTable("Rendering SpecSuiteDidEnd", + func(conf config.DefaultReporterConfigType, summaries []types.Summary, summary types.SuiteSummary, expected ...string) { + reporter := reporters.NewDefaultReporterUnderTest(conf, buf) + for _, summary := range summaries { + reporter.WillRun(summary) + reporter.DidRun(summary) + } + buf.Clear() + reporter.SpecSuiteDidEnd(summary) + verifyExpectedOutput(expected) + }, + + Entry("when configured to be succinct", + C(Succinct), + []types.Summary{S()}, + types.SuiteSummary{ + SuiteSucceeded: true, + RunTime: time.Minute, + }, + " {{green}}SUCCESS!{{/}} 1m0s ", + ), + Entry("the suite passes", + C(), + []types.Summary{S()}, + types.SuiteSummary{ + SuiteSucceeded: true, + RunTime: time.Minute, + NumberOfSpecsThatWillBeRun: 20, + NumberOfTotalSpecs: 30, + NumberOfPassedSpecs: 18, + NumberOfSkippedSpecs: 12, + NumberOfPendingSpecs: 10, + NumberOfFailedSpecs: 0, + NumberOfFlakedSpecs: 0, + }, + "", + "", + "{{green}}{{bold}}Ran 18 of 30 Specs in 60.000 seconds{{/}}", + "{{green}}{{bold}}SUCCESS!{{/}} -- {{green}}{{bold}}18 Passed{{/}} | {{red}}{{bold}}0 Failed{{/}} | {{yellow}}{{bold}}10 Pending{{/}} | {{cyan}}{{bold}}12 Skipped{{/}}", + "", + ), + Entry("the suite passes and has flaky specs", + C(), + []types.Summary{S()}, + types.SuiteSummary{ + SuiteSucceeded: true, + RunTime: time.Minute, + NumberOfSpecsThatWillBeRun: 20, + NumberOfTotalSpecs: 30, + NumberOfPassedSpecs: 18, + NumberOfSkippedSpecs: 12, + NumberOfPendingSpecs: 10, + NumberOfFailedSpecs: 0, + NumberOfFlakedSpecs: 4, + }, + "", + "", + "{{green}}{{bold}}Ran 18 of 30 Specs in 60.000 seconds{{/}}", + "{{green}}{{bold}}SUCCESS!{{/}} -- {{green}}{{bold}}18 Passed{{/}} | {{red}}{{bold}}0 Failed{{/}} | {{light-yellow}}{{bold}}4 Flaked{{/}} | {{yellow}}{{bold}}10 Pending{{/}} | {{cyan}}{{bold}}12 Skipped{{/}}", + "", + ), + Entry("the suite fails with one failed test", + C(), + []types.Summary{S(CTS("Describe A", "Context B", "The Test"), CLS(cl0, cl1, cl2), + types.SpecStateFailed, 2, + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeJustBeforeEach, 1), + )}, + types.SuiteSummary{ + SuiteSucceeded: false, + RunTime: time.Minute, + NumberOfSpecsThatWillBeRun: 20, + NumberOfTotalSpecs: 30, + NumberOfPassedSpecs: 17, + NumberOfSkippedSpecs: 12, + NumberOfPendingSpecs: 10, + NumberOfFailedSpecs: 1, + NumberOfFlakedSpecs: 4, + }, + "", + "{{red}}{{bold}}Ran 18 of 30 Specs in 60.000 seconds{{/}}", + "{{red}}{{bold}}FAIL!{{/}} -- {{green}}{{bold}}17 Passed{{/}} | {{red}}{{bold}}1 Failed{{/}} | {{light-yellow}}{{bold}}4 Flaked{{/}} | {{yellow}}{{bold}}10 Pending{{/}} | {{cyan}}{{bold}}12 Skipped{{/}}", + "", + ), + Entry("the suite fails with multiple failed tests", + C(), + []types.Summary{ + S(CTS("Describe A", "Context B", "The Test"), CLS(cl0, cl1, cl2), + types.SpecStateFailed, 2, + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeJustBeforeEach, 1), + ), + S(CTS("Describe A", "The Test"), CLS(cl0, cl1), + types.SpecStatePanicked, 2, + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl2), types.NodeTypeIt, 1), + ), + S(CTS("The Test"), CLS(cl0), + types.SpecStateInterrupted, 2, + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl1), types.NodeTypeIt, 0), + ), + }, + types.SuiteSummary{ + SuiteSucceeded: false, + RunTime: time.Minute, + NumberOfSpecsThatWillBeRun: 20, + NumberOfTotalSpecs: 30, + NumberOfPassedSpecs: 15, + NumberOfSkippedSpecs: 12, + NumberOfPendingSpecs: 10, + NumberOfFailedSpecs: 3, + NumberOfFlakedSpecs: 4, + }, + "", + "", + "{{red}}{{bold}}Summarizing 3 Failures:{{/}}", + " {{red}}[FAIL]{{/}} {{/}}Describe A {{gray}}{{red}}{{bold}}[JustBeforeEach] Context B{{/}} {{/}}The Test{{/}}", + " {{gray}}"+cl3.String()+"{{/}}", + " {{magenta}}[PANICKED!]{{/}} {{/}}Describe A {{gray}}{{magenta}}{{bold}}[It] The Test{{/}}{{/}}", + " {{gray}}"+cl2.String()+"{{/}}", + " {{orange}}[INTERRUPTED]{{/}} {{/}}{{orange}}{{bold}}[It] The Test{{/}}{{/}}", + " {{gray}}"+cl1.String()+"{{/}}", + "", + "{{red}}{{bold}}Ran 18 of 30 Specs in 60.000 seconds{{/}}", + "{{red}}{{bold}}FAIL!{{/}} -- {{green}}{{bold}}15 Passed{{/}} | {{red}}{{bold}}3 Failed{{/}} | {{light-yellow}}{{bold}}4 Flaked{{/}} | {{yellow}}{{bold}}10 Pending{{/}} | {{cyan}}{{bold}}12 Skipped{{/}}", + "", + ), + Entry("the suite fails with failed suite setups", + C(), + []types.Summary{ + S(types.NodeTypeBeforeSuite, cl0, types.SpecStateFailed, 2, + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl1), types.NodeTypeBeforeSuite, 0), + ), + S(types.NodeTypeAfterSuite, cl2, types.SpecStateFailed, 2, + F("FAILURE MESSAGE\nWITH DETAILS", Location(cl3), types.NodeTypeAfterSuite, 0), + ), + }, + types.SuiteSummary{ + SuiteSucceeded: false, + RunTime: time.Minute, + NumberOfSpecsThatWillBeRun: 20, + NumberOfTotalSpecs: 30, + NumberOfPassedSpecs: 0, + NumberOfSkippedSpecs: 30, + NumberOfPendingSpecs: 10, + NumberOfFailedSpecs: 0, + NumberOfFlakedSpecs: 0, + }, + "", + "", + "{{red}}{{bold}}Summarizing 2 Failures:{{/}}", + " {{red}}[FAIL]{{/}} {{red}}{{bold}}[BeforeSuite]{{/}}", + " {{gray}}"+cl1.String()+"{{/}}", + " {{red}}[FAIL]{{/}} {{red}}{{bold}}[AfterSuite]{{/}}", + " {{gray}}"+cl3.String()+"{{/}}", + "", + "{{red}}{{bold}}Ran 0 of 30 Specs in 60.000 seconds{{/}}", + "{{red}}{{bold}}FAIL!{{/}} -- {{green}}{{bold}}0 Passed{{/}} | {{red}}{{bold}}0 Failed{{/}} | {{yellow}}{{bold}}10 Pending{{/}} | {{cyan}}{{bold}}30 Skipped{{/}}", + "", + ), + ) - It("should announce the failed spec", func() { - Ω(stenographer.Calls()[0]).Should(Equal(call("AnnounceSpecFailed", spec, true, true))) - }) + Describe("with failOnPending set to true", func() { + It("notifies the user when the suite failed due to pending tests", func() { + reporter := reporters.NewDefaultReporterUnderTest(C(), buf) + reporter.SpecSuiteWillBegin(config.GinkgoConfigType{ + FailOnPending: true, + }, types.SuiteSummary{SuiteDescription: "My Suite", NumberOfSpecsThatWillBeRun: 20, NumberOfTotalSpecs: 20}) + buf.Clear() + reporter.SpecSuiteDidEnd(types.SuiteSummary{ + SuiteSucceeded: false, + RunTime: time.Minute, + NumberOfSpecsThatWillBeRun: 20, + NumberOfTotalSpecs: 20, + NumberOfPassedSpecs: 19, + NumberOfSkippedSpecs: 0, + NumberOfPendingSpecs: 1, + NumberOfFailedSpecs: 0, + NumberOfFlakedSpecs: 0, + }) + verifyExpectedOutput([]string{ + "", + "{{yellow}}{{bold}}Ran 19 of 20 Specs in 60.000 seconds{{/}}", + "{{yellow}}{{bold}}FAIL! - Detected pending specs and --fail-on-pending is set{{/}} -- {{green}}{{bold}}19 Passed{{/}} | {{red}}{{bold}}0 Failed{{/}} | {{yellow}}{{bold}}1 Pending{{/}} | {{cyan}}{{bold}}0 Skipped{{/}}", + "", }) }) }) - - Describe("SpecSuiteDidEnd", func() { - BeforeEach(func() { - suite = &types.SuiteSummary{} - reporter.SpecSuiteDidEnd(suite) - }) - - It("should announce the spec run's completion", func() { - Ω(stenographer.Calls()[1]).Should(Equal(call("AnnounceSpecRunCompletion", suite, false))) - }) - }) }) diff --git a/reporters/fake_reporter.go b/reporters/fake_reporter.go deleted file mode 100644 index 27db479490..0000000000 --- a/reporters/fake_reporter.go +++ /dev/null @@ -1,59 +0,0 @@ -package reporters - -import ( - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/types" -) - -//FakeReporter is useful for testing purposes -type FakeReporter struct { - Config config.GinkgoConfigType - - BeginSummary *types.SuiteSummary - BeforeSuiteSummary *types.SetupSummary - SpecWillRunSummaries []*types.SpecSummary - SpecSummaries []*types.SpecSummary - AfterSuiteSummary *types.SetupSummary - EndSummary *types.SuiteSummary - - SpecWillRunStub func(specSummary *types.SpecSummary) - SpecDidCompleteStub func(specSummary *types.SpecSummary) -} - -func NewFakeReporter() *FakeReporter { - return &FakeReporter{ - SpecWillRunSummaries: make([]*types.SpecSummary, 0), - SpecSummaries: make([]*types.SpecSummary, 0), - } -} - -func (fakeR *FakeReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { - fakeR.Config = config - fakeR.BeginSummary = summary -} - -func (fakeR *FakeReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { - fakeR.BeforeSuiteSummary = setupSummary -} - -func (fakeR *FakeReporter) SpecWillRun(specSummary *types.SpecSummary) { - if fakeR.SpecWillRunStub != nil { - fakeR.SpecWillRunStub(specSummary) - } - fakeR.SpecWillRunSummaries = append(fakeR.SpecWillRunSummaries, specSummary) -} - -func (fakeR *FakeReporter) SpecDidComplete(specSummary *types.SpecSummary) { - if fakeR.SpecDidCompleteStub != nil { - fakeR.SpecDidCompleteStub(specSummary) - } - fakeR.SpecSummaries = append(fakeR.SpecSummaries, specSummary) -} - -func (fakeR *FakeReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { - fakeR.AfterSuiteSummary = setupSummary -} - -func (fakeR *FakeReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { - fakeR.EndSummary = summary -} diff --git a/reporters/junit_reporter.go b/reporters/junit_reporter.go index 01ddca6e1d..c2bcb4d36a 100644 --- a/reporters/junit_reporter.go +++ b/reporters/junit_reporter.go @@ -62,7 +62,7 @@ func NewJUnitReporter(filename string) *JUnitReporter { } } -func (reporter *JUnitReporter) SpecSuiteWillBegin(ginkgoConfig config.GinkgoConfigType, summary *types.SuiteSummary) { +func (reporter *JUnitReporter) SpecSuiteWillBegin(ginkgoConfig config.GinkgoConfigType, summary types.SuiteSummary) { reporter.suite = JUnitTestSuite{ Name: summary.SuiteDescription, TestCases: []JUnitTestCase{}, @@ -71,107 +71,80 @@ func (reporter *JUnitReporter) SpecSuiteWillBegin(ginkgoConfig config.GinkgoConf reporter.ReporterConfig = config.DefaultReporterConfig } -func (reporter *JUnitReporter) SpecWillRun(specSummary *types.SpecSummary) { +func (reporter *JUnitReporter) WillRun(_ types.Summary) { } -func (reporter *JUnitReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { - reporter.handleSetupSummary("BeforeSuite", setupSummary) -} - -func (reporter *JUnitReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { - reporter.handleSetupSummary("AfterSuite", setupSummary) -} - -func failureMessage(failure types.SpecFailure) string { - return fmt.Sprintf("%s\n%s\n%s", failure.ComponentCodeLocation.String(), failure.Message, failure.Location.String()) -} - -func (reporter *JUnitReporter) handleSetupSummary(name string, setupSummary *types.SetupSummary) { - if setupSummary.State != types.SpecStatePassed { - testCase := JUnitTestCase{ - Name: name, - ClassName: reporter.testSuiteName, - } - - testCase.FailureMessage = &JUnitFailureMessage{ - Type: reporter.failureTypeForState(setupSummary.State), - Message: failureMessage(setupSummary.Failure), - } - testCase.SystemOut = setupSummary.CapturedOutput - testCase.Time = setupSummary.RunTime.Seconds() - reporter.suite.TestCases = append(reporter.suite.TestCases, testCase) - } -} - -func (reporter *JUnitReporter) SpecDidComplete(specSummary *types.SpecSummary) { +func (reporter *JUnitReporter) DidRun(summary types.Summary) { testCase := JUnitTestCase{ - Name: strings.Join(specSummary.ComponentTexts[1:], " "), ClassName: reporter.testSuiteName, } - if reporter.ReporterConfig.ReportPassed && specSummary.State == types.SpecStatePassed { - testCase.SystemOut = specSummary.CapturedOutput + if summary.LeafNodeType.Is(types.NodeTypesForSuiteSetup...) { + if summary.State.Is(types.SpecStatePassed) { + return + } + testCase.Name = summary.LeafNodeType.String() + } else { + testCase.Name = strings.Join(summary.NodeTexts, " ") + } + if reporter.ReporterConfig.ReportPassed && summary.State == types.SpecStatePassed { + testCase.SystemOut = summary.CombinedOutput() } - if specSummary.State == types.SpecStateFailed || specSummary.State == types.SpecStateTimedOut || specSummary.State == types.SpecStatePanicked { + if summary.State.Is(types.SpecStateFailureStates...) { testCase.FailureMessage = &JUnitFailureMessage{ - Type: reporter.failureTypeForState(specSummary.State), - Message: failureMessage(specSummary.Failure), + Type: reporter.failureTypeForState(summary.State), + Message: reporter.failureMessage(summary.Failure), } - if specSummary.State == types.SpecStatePanicked { + if summary.State.Is(types.SpecStatePanicked) { testCase.FailureMessage.Message += fmt.Sprintf("\n\nPanic: %s\n\nFull stack:\n%s", - specSummary.Failure.ForwardedPanic, - specSummary.Failure.Location.FullStackTrace) + summary.Failure.ForwardedPanic, + summary.Failure.Location.FullStackTrace) } - testCase.SystemOut = specSummary.CapturedOutput + testCase.SystemOut = summary.CombinedOutput() } - if specSummary.State == types.SpecStateSkipped || specSummary.State == types.SpecStatePending { + if summary.State == types.SpecStateSkipped || summary.State == types.SpecStatePending { testCase.Skipped = &JUnitSkipped{} - if specSummary.Failure.Message != "" { - testCase.Skipped.Message = failureMessage(specSummary.Failure) + if summary.Failure.Message != "" { + testCase.Skipped.Message = reporter.failureMessage(summary.Failure) } } - testCase.Time = specSummary.RunTime.Seconds() + testCase.Time = summary.RunTime.Seconds() reporter.suite.TestCases = append(reporter.suite.TestCases, testCase) } -func (reporter *JUnitReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { +func (reporter *JUnitReporter) SpecSuiteDidEnd(summary types.SuiteSummary) { reporter.suite.Tests = summary.NumberOfSpecsThatWillBeRun reporter.suite.Time = math.Trunc(summary.RunTime.Seconds()*1000) / 1000 reporter.suite.Failures = summary.NumberOfFailedSpecs reporter.suite.Errors = 0 - if reporter.ReporterConfig.ReportFile != "" { - reporter.filename = reporter.ReporterConfig.ReportFile - fmt.Printf("\nJUnit path was configured: %s\n", reporter.filename) - } filePath, _ := filepath.Abs(reporter.filename) dirPath := filepath.Dir(filePath) err := os.MkdirAll(dirPath, os.ModePerm) if err != nil { - fmt.Printf("\nFailed to create JUnit directory: %s\n\t%s", filePath, err.Error()) + return } file, err := os.Create(filePath) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create JUnit report file: %s\n\t%s", filePath, err.Error()) + return } defer file.Close() file.WriteString(xml.Header) encoder := xml.NewEncoder(file) encoder.Indent(" ", " ") - err = encoder.Encode(reporter.suite) - if err == nil { - fmt.Fprintf(os.Stdout, "\nJUnit report was created: %s\n", filePath) - } else { - fmt.Fprintf(os.Stderr,"\nFailed to generate JUnit report data:\n\t%s", err.Error()) - } + encoder.Encode(reporter.suite) +} + +func (reporter *JUnitReporter) failureMessage(failure types.Failure) string { + return fmt.Sprintf("%s\n%s\n%s", failure.NodeType.String(), failure.Message, failure.Location.String()) } func (reporter *JUnitReporter) failureTypeForState(state types.SpecState) string { switch state { case types.SpecStateFailed: return "Failure" - case types.SpecStateTimedOut: - return "Timeout" case types.SpecStatePanicked: - return "Panic" + return "Panicked" + case types.SpecStateInterrupted: + return "Interrupted" default: return "" } diff --git a/reporters/junit_reporter_test.go b/reporters/junit_reporter_test.go index 8b07fedaf8..c67a01bb5d 100644 --- a/reporters/junit_reporter_test.go +++ b/reporters/junit_reporter_test.go @@ -8,7 +8,6 @@ import ( . "github.com/onsi/ginkgo" "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/internal/codelocation" "github.com/onsi/ginkgo/reporters" "github.com/onsi/ginkgo/types" . "github.com/onsi/gomega" @@ -39,7 +38,7 @@ var _ = Describe("JUnit Reporter", func() { reporter = reporters.NewJUnitReporter(outputFile) - reporter.SpecSuiteWillBegin(config.GinkgoConfigType{}, &types.SuiteSummary{ + reporter.SpecSuiteWillBegin(config.GinkgoConfigType{}, types.SuiteSummary{ SuiteDescription: "My test suite", NumberOfSpecsThatWillBeRun: 1, }) @@ -51,29 +50,18 @@ var _ = Describe("JUnit Reporter", func() { Describe("when configured with ReportPassed, and test has passed", func() { BeforeEach(func() { - beforeSuite := &types.SetupSummary{ - State: types.SpecStatePassed, - } - reporter.BeforeSuiteDidRun(beforeSuite) - - afterSuite := &types.SetupSummary{ - State: types.SpecStatePassed, - } - reporter.AfterSuiteDidRun(afterSuite) - - // Set the ReportPassed config flag, in order to show captured output when tests have passed. reporter.ReporterConfig.ReportPassed = true - spec := &types.SpecSummary{ - ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, - CapturedOutput: "Test scenario...", - State: types.SpecStatePassed, - RunTime: 5 * time.Second, + spec := types.Summary{ + NodeTexts: []string{"A", "B", "C"}, + CapturedGinkgoWriterOutput: "Test scenario...", + State: types.SpecStatePassed, + RunTime: 5 * time.Second, } - reporter.SpecWillRun(spec) - reporter.SpecDidComplete(spec) + reporter.WillRun(spec) + reporter.DidRun(spec) - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + reporter.SpecSuiteDidEnd(types.SuiteSummary{ NumberOfSpecsThatWillBeRun: 1, NumberOfFailedSpecs: 0, RunTime: testSuiteTime, @@ -97,83 +85,24 @@ var _ = Describe("JUnit Reporter", func() { }) }) - Describe("when configured with ReportFile ", func() { - BeforeEach(func() { - beforeSuite := &types.SetupSummary{ - State: types.SpecStatePassed, - } - reporter.BeforeSuiteDidRun(beforeSuite) - - afterSuite := &types.SetupSummary{ - State: types.SpecStatePassed, - } - reporter.AfterSuiteDidRun(afterSuite) - - reporter.SpecSuiteWillBegin(config.GinkgoConfigType{}, &types.SuiteSummary{ - SuiteDescription: "My test suite", - NumberOfSpecsThatWillBeRun: 1, - }) - - // Set the ReportFile config flag with a new directory and new file path to be created. - d, err := ioutil.TempDir("", "new-junit-dir") - Expect(err).ToNot(HaveOccurred()) - f, err := ioutil.TempFile(d, "output") - Expect(err).ToNot(HaveOccurred()) - f.Close() - outputFile = f.Name() - err = os.RemoveAll(d) - Expect(err).ToNot(HaveOccurred()) - reporter.ReporterConfig.ReportFile = outputFile - - spec := &types.SpecSummary{ - ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, - CapturedOutput: "Test scenario...", - State: types.SpecStatePassed, - RunTime: 5 * time.Second, - } - reporter.SpecWillRun(spec) - reporter.SpecDidComplete(spec) - - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ - NumberOfSpecsThatWillBeRun: 1, - NumberOfFailedSpecs: 0, - RunTime: testSuiteTime, - }) - - }) - - It("should create the report (and parent directories) as specified by ReportFile path", func() { - output := readOutputFile() - Expect(output.Name).To(Equal("My test suite")) - Expect(output.Tests).To(Equal(1)) - Expect(output.Failures).To(Equal(0)) - Expect(output.Time).To(Equal(reportedSuiteTime)) - Expect(output.Errors).To(Equal(0)) - Expect(output.TestCases).To(HaveLen(1)) - Expect(output.TestCases[0].Name).To(Equal("A B C")) - Expect(output.TestCases[0].ClassName).To(Equal("My test suite")) - Expect(output.TestCases[0].FailureMessage).To(BeNil()) - Expect(output.TestCases[0].Skipped).To(BeNil()) - Expect(output.TestCases[0].Time).To(Equal(5.0)) - }) - }) - - Describe("when the BeforeSuite fails", func() { - var beforeSuite *types.SetupSummary + Describe("when a BeforeSuite fails", func() { + var beforeSuite types.Summary BeforeEach(func() { - beforeSuite = &types.SetupSummary{ - State: types.SpecStateFailed, - RunTime: 3 * time.Second, - Failure: types.SpecFailure{ - Message: "failed to setup", - ComponentCodeLocation: codelocation.New(0), - Location: codelocation.New(2), + beforeSuite = types.Summary{ + LeafNodeType: types.NodeTypeBeforeSuite, + State: types.SpecStateFailed, + RunTime: 3 * time.Second, + Failure: types.Failure{ + Message: "failed to setup", + NodeType: types.NodeTypeJustBeforeEach, + Location: types.NewCodeLocation(2), }, } - reporter.BeforeSuiteDidRun(beforeSuite) + reporter.WillRun(beforeSuite) + reporter.DidRun(beforeSuite) - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + reporter.SpecSuiteDidEnd(types.SuiteSummary{ NumberOfSpecsThatWillBeRun: 1, NumberOfFailedSpecs: 1, RunTime: testSuiteTime, @@ -192,52 +121,12 @@ var _ = Describe("JUnit Reporter", func() { Expect(output.TestCases[0].ClassName).To(Equal("My test suite")) Expect(output.TestCases[0].FailureMessage.Type).To(Equal("Failure")) Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring("failed to setup")) - Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring(beforeSuite.Failure.ComponentCodeLocation.String())) + Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring("JustBeforeEach")) Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring(beforeSuite.Failure.Location.String())) Expect(output.TestCases[0].Skipped).To(BeNil()) }) }) - Describe("when the AfterSuite fails", func() { - var afterSuite *types.SetupSummary - - BeforeEach(func() { - afterSuite = &types.SetupSummary{ - State: types.SpecStateFailed, - RunTime: 3 * time.Second, - Failure: types.SpecFailure{ - Message: "failed to setup", - ComponentCodeLocation: codelocation.New(0), - Location: codelocation.New(2), - }, - } - reporter.AfterSuiteDidRun(afterSuite) - - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ - NumberOfSpecsThatWillBeRun: 1, - NumberOfFailedSpecs: 1, - RunTime: testSuiteTime, - }) - }) - - It("should record the test as having failed", func() { - output := readOutputFile() - Expect(output.Name).To(Equal("My test suite")) - Expect(output.Tests).To(Equal(1)) - Expect(output.Failures).To(Equal(1)) - Expect(output.Time).To(Equal(reportedSuiteTime)) - Expect(output.Errors).To(Equal(0)) - Expect(output.TestCases[0].Name).To(Equal("AfterSuite")) - Expect(output.TestCases[0].Time).To(Equal(3.0)) - Expect(output.TestCases[0].ClassName).To(Equal("My test suite")) - Expect(output.TestCases[0].FailureMessage.Type).To(Equal("Failure")) - Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring("failed to setup")) - Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring(afterSuite.Failure.ComponentCodeLocation.String())) - Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring(afterSuite.Failure.Location.String())) - Expect(output.TestCases[0].Skipped).To(BeNil()) - }) - }) - specStateCases := []struct { state types.SpecState message string @@ -246,30 +135,30 @@ var _ = Describe("JUnit Reporter", func() { forwardedPanic string }{ {types.SpecStateFailed, "Failure", ""}, - {types.SpecStateTimedOut, "Timeout", ""}, - {types.SpecStatePanicked, "Panic", "artifical panic"}, + {types.SpecStatePanicked, "Panicked", "artifical panic"}, + {types.SpecStateInterrupted, "Interrupted", ""}, } for _, specStateCase := range specStateCases { specStateCase := specStateCase Describe("a failing test", func() { - var spec *types.SpecSummary + var spec types.Summary BeforeEach(func() { - spec = &types.SpecSummary{ - ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, - State: specStateCase.state, - RunTime: 5 * time.Second, - Failure: types.SpecFailure{ - ComponentCodeLocation: codelocation.New(0), - Location: codelocation.New(2), - Message: "I failed", - ForwardedPanic: specStateCase.forwardedPanic, + spec = types.Summary{ + NodeTexts: []string{"A", "B", "C"}, + State: specStateCase.state, + RunTime: 5 * time.Second, + Failure: types.Failure{ + NodeType: types.NodeTypeJustBeforeEach, + Location: types.NewCodeLocation(2), + Message: "I failed", + ForwardedPanic: specStateCase.forwardedPanic, }, } - reporter.SpecWillRun(spec) - reporter.SpecDidComplete(spec) + reporter.WillRun(spec) + reporter.DidRun(spec) - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + reporter.SpecSuiteDidEnd(types.SuiteSummary{ NumberOfSpecsThatWillBeRun: 1, NumberOfFailedSpecs: 1, RunTime: testSuiteTime, @@ -287,7 +176,7 @@ var _ = Describe("JUnit Reporter", func() { Expect(output.TestCases[0].ClassName).To(Equal("My test suite")) Expect(output.TestCases[0].FailureMessage.Type).To(Equal(specStateCase.message)) Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring("I failed")) - Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring(spec.Failure.ComponentCodeLocation.String())) + Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring("JustBeforeEach")) Expect(output.TestCases[0].FailureMessage.Message).To(ContainSubstring(spec.Failure.Location.String())) Expect(output.TestCases[0].Skipped).To(BeNil()) if specStateCase.state == types.SpecStatePanicked { @@ -301,20 +190,20 @@ var _ = Describe("JUnit Reporter", func() { for _, specStateCase := range []types.SpecState{types.SpecStatePending, types.SpecStateSkipped} { specStateCase := specStateCase Describe("a skipped test", func() { - var spec *types.SpecSummary + var spec types.Summary BeforeEach(func() { - spec = &types.SpecSummary{ - ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, - State: specStateCase, - RunTime: 5 * time.Second, - Failure: types.SpecFailure{ + spec = types.Summary{ + NodeTexts: []string{"A", "B", "C"}, + State: specStateCase, + RunTime: 5 * time.Second, + Failure: types.Failure{ Message: "skipped reason", }, } - reporter.SpecWillRun(spec) - reporter.SpecDidComplete(spec) + reporter.WillRun(spec) + reporter.DidRun(spec) - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + reporter.SpecSuiteDidEnd(types.SuiteSummary{ NumberOfSpecsThatWillBeRun: 1, NumberOfFailedSpecs: 0, RunTime: testSuiteTime, diff --git a/reporters/multi_reporter.go b/reporters/multi_reporter.go new file mode 100644 index 0000000000..e3b58f4197 --- /dev/null +++ b/reporters/multi_reporter.go @@ -0,0 +1,40 @@ +package reporters + +import ( + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +type MultiReporter struct { + reporters []Reporter +} + +func NewMultiReporter(reporters ...Reporter) *MultiReporter { + return &MultiReporter{ + reporters: reporters, + } +} + +func (mr *MultiReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary types.SuiteSummary) { + for _, reporter := range mr.reporters { + reporter.SpecSuiteWillBegin(config, summary) + } +} + +func (mr *MultiReporter) WillRun(summary types.Summary) { + for _, reporter := range mr.reporters { + reporter.WillRun(summary) + } +} + +func (mr *MultiReporter) DidRun(summary types.Summary) { + for i := len(mr.reporters) - 1; i >= 0; i-- { + mr.reporters[i].DidRun(summary) + } +} + +func (mr *MultiReporter) SpecSuiteDidEnd(summary types.SuiteSummary) { + for _, reporter := range mr.reporters { + reporter.SpecSuiteDidEnd(summary) + } +} diff --git a/reporters/multi_reporter_test.go b/reporters/multi_reporter_test.go new file mode 100644 index 0000000000..2eb478fde4 --- /dev/null +++ b/reporters/multi_reporter_test.go @@ -0,0 +1,46 @@ +package reporters_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/internal/test_helpers" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("MultiReporter", func() { + var fake1, fake2 *FakeReporter + var multiReporter *reporters.MultiReporter + + BeforeEach(func() { + fake1 = &FakeReporter{} + fake2 = &FakeReporter{} + multiReporter = reporters.NewMultiReporter(fake1, fake2) + }) + + It("forwards all calls to the registered reporters", func() { + conf := config.GinkgoConfigType{ParallelNode: 3} + suiteSummary := types.SuiteSummary{SuiteDescription: "foo"} + + multiReporter.SpecSuiteWillBegin(conf, suiteSummary) + Ω(fake1.Config).Should(Equal(conf)) + Ω(fake1.Begin).Should(Equal(suiteSummary)) + Ω(fake2.Config).Should(Equal(conf)) + Ω(fake2.Begin).Should(Equal(suiteSummary)) + + summary := types.Summary{CapturedGinkgoWriterOutput: "spec"} + multiReporter.WillRun(summary) + Ω(fake1.Will[0]).Should(Equal(summary)) + Ω(fake2.Will[0]).Should(Equal(summary)) + multiReporter.DidRun(summary) + Ω(fake1.Did[0]).Should(Equal(summary)) + Ω(fake2.Did[0]).Should(Equal(summary)) + + multiReporter.SpecSuiteDidEnd(suiteSummary) + Ω(fake1.End).Should(Equal(suiteSummary)) + Ω(fake2.End).Should(Equal(suiteSummary)) + }) + +}) diff --git a/reporters/reporter.go b/reporters/reporter.go index 348b9dfce1..4a69d8ae32 100644 --- a/reporters/reporter.go +++ b/reporters/reporter.go @@ -6,6 +6,14 @@ import ( ) type Reporter interface { + SpecSuiteWillBegin(config config.GinkgoConfigType, summary types.SuiteSummary) + WillRun(specSummary types.Summary) + DidRun(specSummary types.Summary) + SpecSuiteDidEnd(summary types.SuiteSummary) +} + +// Old V1Reporter compatibility support +type V1Reporter interface { SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) BeforeSuiteDidRun(setupSummary *types.SetupSummary) SpecWillRun(specSummary *types.SpecSummary) @@ -13,3 +21,44 @@ type Reporter interface { AfterSuiteDidRun(setupSummary *types.SetupSummary) SpecSuiteDidEnd(summary *types.SuiteSummary) } + +type compatiblityShim struct { + reporter V1Reporter +} + +func (cs *compatiblityShim) IsDeprecatedReporter() {} + +func (cs *compatiblityShim) SpecSuiteWillBegin(config config.GinkgoConfigType, summary types.SuiteSummary) { + s := summary + cs.reporter.SpecSuiteWillBegin(config, &s) +} + +func (cs *compatiblityShim) WillRun(summary types.Summary) { + if summary.LeafNodeType.Is(types.NodeTypesForSuiteSetup...) { + return + } + cs.reporter.SpecWillRun(types.DeprecatedSpecSummaryFromSummary(summary)) +} + +func (cs *compatiblityShim) DidRun(summary types.Summary) { + if summary.LeafNodeType.Is(types.NodeTypesForSuiteSetup...) { + if summary.LeafNodeType.Is(types.NodeTypeBeforeSuite, types.NodeTypeSynchronizedBeforeSuite) { + cs.reporter.BeforeSuiteDidRun(types.DeprecatedSetupSummaryFromSummary(summary)) + } else { + cs.reporter.AfterSuiteDidRun(types.DeprecatedSetupSummaryFromSummary(summary)) + } + } else { + cs.reporter.SpecDidComplete(types.DeprecatedSpecSummaryFromSummary(summary)) + } +} + +func (cs *compatiblityShim) SpecSuiteDidEnd(summary types.SuiteSummary) { + s := summary + cs.reporter.SpecSuiteDidEnd(&s) +} + +func ReporterFromV1Reporter(reporter V1Reporter) Reporter { + return &compatiblityShim{ + reporter: reporter, + } +} diff --git a/reporters/stenographer/console_logging.go b/reporters/stenographer/console_logging.go deleted file mode 100644 index 45b8f88690..0000000000 --- a/reporters/stenographer/console_logging.go +++ /dev/null @@ -1,64 +0,0 @@ -package stenographer - -import ( - "fmt" - "strings" -) - -func (s *consoleStenographer) colorize(colorCode string, format string, args ...interface{}) string { - var out string - - if len(args) > 0 { - out = fmt.Sprintf(format, args...) - } else { - out = format - } - - if s.color { - return fmt.Sprintf("%s%s%s", colorCode, out, defaultStyle) - } else { - return out - } -} - -func (s *consoleStenographer) printBanner(text string, bannerCharacter string) { - fmt.Fprintln(s.w, text) - fmt.Fprintln(s.w, strings.Repeat(bannerCharacter, len(text))) -} - -func (s *consoleStenographer) printNewLine() { - fmt.Fprintln(s.w, "") -} - -func (s *consoleStenographer) printDelimiter() { - fmt.Fprintln(s.w, s.colorize(grayColor, "%s", strings.Repeat("-", 30))) -} - -func (s *consoleStenographer) print(indentation int, format string, args ...interface{}) { - fmt.Fprint(s.w, s.indent(indentation, format, args...)) -} - -func (s *consoleStenographer) println(indentation int, format string, args ...interface{}) { - fmt.Fprintln(s.w, s.indent(indentation, format, args...)) -} - -func (s *consoleStenographer) indent(indentation int, format string, args ...interface{}) string { - var text string - - if len(args) > 0 { - text = fmt.Sprintf(format, args...) - } else { - text = format - } - - stringArray := strings.Split(text, "\n") - padding := "" - if indentation >= 0 { - padding = strings.Repeat(" ", indentation) - } - for i, s := range stringArray { - stringArray[i] = fmt.Sprintf("%s%s", padding, s) - } - - return strings.Join(stringArray, "\n") -} diff --git a/reporters/stenographer/fake_stenographer.go b/reporters/stenographer/fake_stenographer.go deleted file mode 100644 index 1aa5b9db0f..0000000000 --- a/reporters/stenographer/fake_stenographer.go +++ /dev/null @@ -1,142 +0,0 @@ -package stenographer - -import ( - "sync" - - "github.com/onsi/ginkgo/types" -) - -func NewFakeStenographerCall(method string, args ...interface{}) FakeStenographerCall { - return FakeStenographerCall{ - Method: method, - Args: args, - } -} - -type FakeStenographer struct { - calls []FakeStenographerCall - lock *sync.Mutex -} - -type FakeStenographerCall struct { - Method string - Args []interface{} -} - -func NewFakeStenographer() *FakeStenographer { - stenographer := &FakeStenographer{ - lock: &sync.Mutex{}, - } - stenographer.Reset() - return stenographer -} - -func (stenographer *FakeStenographer) Calls() []FakeStenographerCall { - stenographer.lock.Lock() - defer stenographer.lock.Unlock() - - return stenographer.calls -} - -func (stenographer *FakeStenographer) Reset() { - stenographer.lock.Lock() - defer stenographer.lock.Unlock() - - stenographer.calls = make([]FakeStenographerCall, 0) -} - -func (stenographer *FakeStenographer) CallsTo(method string) []FakeStenographerCall { - stenographer.lock.Lock() - defer stenographer.lock.Unlock() - - results := make([]FakeStenographerCall, 0) - for _, call := range stenographer.calls { - if call.Method == method { - results = append(results, call) - } - } - - return results -} - -func (stenographer *FakeStenographer) registerCall(method string, args ...interface{}) { - stenographer.lock.Lock() - defer stenographer.lock.Unlock() - - stenographer.calls = append(stenographer.calls, NewFakeStenographerCall(method, args...)) -} - -func (stenographer *FakeStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) { - stenographer.registerCall("AnnounceSuite", description, randomSeed, randomizingAll, succinct) -} - -func (stenographer *FakeStenographer) AnnounceAggregatedParallelRun(nodes int, succinct bool) { - stenographer.registerCall("AnnounceAggregatedParallelRun", nodes, succinct) -} - -func (stenographer *FakeStenographer) AnnounceParallelRun(node int, nodes int, succinct bool) { - stenographer.registerCall("AnnounceParallelRun", node, nodes, succinct) -} - -func (stenographer *FakeStenographer) AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) { - stenographer.registerCall("AnnounceNumberOfSpecs", specsToRun, total, succinct) -} - -func (stenographer *FakeStenographer) AnnounceTotalNumberOfSpecs(total int, succinct bool) { - stenographer.registerCall("AnnounceTotalNumberOfSpecs", total, succinct) -} - -func (stenographer *FakeStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) { - stenographer.registerCall("AnnounceSpecRunCompletion", summary, succinct) -} - -func (stenographer *FakeStenographer) AnnounceSpecWillRun(spec *types.SpecSummary) { - stenographer.registerCall("AnnounceSpecWillRun", spec) -} - -func (stenographer *FakeStenographer) AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { - stenographer.registerCall("AnnounceBeforeSuiteFailure", summary, succinct, fullTrace) -} - -func (stenographer *FakeStenographer) AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { - stenographer.registerCall("AnnounceAfterSuiteFailure", summary, succinct, fullTrace) -} -func (stenographer *FakeStenographer) AnnounceCapturedOutput(output string) { - stenographer.registerCall("AnnounceCapturedOutput", output) -} - -func (stenographer *FakeStenographer) AnnounceSuccessfulSpec(spec *types.SpecSummary) { - stenographer.registerCall("AnnounceSuccessfulSpec", spec) -} - -func (stenographer *FakeStenographer) AnnounceSuccessfulSlowSpec(spec *types.SpecSummary, succinct bool) { - stenographer.registerCall("AnnounceSuccessfulSlowSpec", spec, succinct) -} - -func (stenographer *FakeStenographer) AnnounceSuccessfulMeasurement(spec *types.SpecSummary, succinct bool) { - stenographer.registerCall("AnnounceSuccessfulMeasurement", spec, succinct) -} - -func (stenographer *FakeStenographer) AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) { - stenographer.registerCall("AnnouncePendingSpec", spec, noisy) -} - -func (stenographer *FakeStenographer) AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool) { - stenographer.registerCall("AnnounceSkippedSpec", spec, succinct, fullTrace) -} - -func (stenographer *FakeStenographer) AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) { - stenographer.registerCall("AnnounceSpecTimedOut", spec, succinct, fullTrace) -} - -func (stenographer *FakeStenographer) AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) { - stenographer.registerCall("AnnounceSpecPanicked", spec, succinct, fullTrace) -} - -func (stenographer *FakeStenographer) AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) { - stenographer.registerCall("AnnounceSpecFailed", spec, succinct, fullTrace) -} - -func (stenographer *FakeStenographer) SummarizeFailures(summaries []*types.SpecSummary) { - stenographer.registerCall("SummarizeFailures", summaries) -} diff --git a/reporters/stenographer/stenographer.go b/reporters/stenographer/stenographer.go deleted file mode 100644 index 638d6fbb1a..0000000000 --- a/reporters/stenographer/stenographer.go +++ /dev/null @@ -1,572 +0,0 @@ -/* -The stenographer is used by Ginkgo's reporters to generate output. - -Move along, nothing to see here. -*/ - -package stenographer - -import ( - "fmt" - "io" - "runtime" - "strings" - - "github.com/onsi/ginkgo/types" -) - -const defaultStyle = "\x1b[0m" -const boldStyle = "\x1b[1m" -const redColor = "\x1b[91m" -const greenColor = "\x1b[32m" -const yellowColor = "\x1b[33m" -const cyanColor = "\x1b[36m" -const grayColor = "\x1b[90m" -const lightGrayColor = "\x1b[37m" - -type cursorStateType int - -const ( - cursorStateTop cursorStateType = iota - cursorStateStreaming - cursorStateMidBlock - cursorStateEndBlock -) - -type Stenographer interface { - AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) - AnnounceAggregatedParallelRun(nodes int, succinct bool) - AnnounceParallelRun(node int, nodes int, succinct bool) - AnnounceTotalNumberOfSpecs(total int, succinct bool) - AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) - AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) - - AnnounceSpecWillRun(spec *types.SpecSummary) - AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) - AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) - - AnnounceCapturedOutput(output string) - - AnnounceSuccessfulSpec(spec *types.SpecSummary) - AnnounceSuccessfulSlowSpec(spec *types.SpecSummary, succinct bool) - AnnounceSuccessfulMeasurement(spec *types.SpecSummary, succinct bool) - - AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) - AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool) - - AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) - AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) - AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) - - SummarizeFailures(summaries []*types.SpecSummary) -} - -func New(color bool, enableFlakes bool, writer io.Writer) Stenographer { - denoter := "•" - if runtime.GOOS == "windows" { - denoter = "+" - } - return &consoleStenographer{ - color: color, - denoter: denoter, - cursorState: cursorStateTop, - enableFlakes: enableFlakes, - w: writer, - } -} - -type consoleStenographer struct { - color bool - denoter string - cursorState cursorStateType - enableFlakes bool - w io.Writer -} - -var alternatingColors = []string{defaultStyle, grayColor} - -func (s *consoleStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) { - if succinct { - s.print(0, "[%d] %s ", randomSeed, s.colorize(boldStyle, description)) - return - } - s.printBanner(fmt.Sprintf("Running Suite: %s", description), "=") - s.print(0, "Random Seed: %s", s.colorize(boldStyle, "%d", randomSeed)) - if randomizingAll { - s.print(0, " - Will randomize all specs") - } - s.printNewLine() -} - -func (s *consoleStenographer) AnnounceParallelRun(node int, nodes int, succinct bool) { - if succinct { - s.print(0, "- node #%d ", node) - return - } - s.println(0, - "Parallel test node %s/%s.", - s.colorize(boldStyle, "%d", node), - s.colorize(boldStyle, "%d", nodes), - ) - s.printNewLine() -} - -func (s *consoleStenographer) AnnounceAggregatedParallelRun(nodes int, succinct bool) { - if succinct { - s.print(0, "- %d nodes ", nodes) - return - } - s.println(0, - "Running in parallel across %s nodes", - s.colorize(boldStyle, "%d", nodes), - ) - s.printNewLine() -} - -func (s *consoleStenographer) AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) { - if succinct { - s.print(0, "- %d/%d specs ", specsToRun, total) - s.stream() - return - } - s.println(0, - "Will run %s of %s specs", - s.colorize(boldStyle, "%d", specsToRun), - s.colorize(boldStyle, "%d", total), - ) - - s.printNewLine() -} - -func (s *consoleStenographer) AnnounceTotalNumberOfSpecs(total int, succinct bool) { - if succinct { - s.print(0, "- %d specs ", total) - s.stream() - return - } - s.println(0, - "Will run %s specs", - s.colorize(boldStyle, "%d", total), - ) - - s.printNewLine() -} - -func (s *consoleStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) { - if succinct && summary.SuiteSucceeded { - s.print(0, " %s %s ", s.colorize(greenColor, "SUCCESS!"), summary.RunTime) - return - } - s.printNewLine() - color := greenColor - if !summary.SuiteSucceeded { - color = redColor - } - s.println(0, s.colorize(boldStyle+color, "Ran %d of %d Specs in %.3f seconds", summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs, summary.RunTime.Seconds())) - - status := "" - if summary.SuiteSucceeded { - status = s.colorize(boldStyle+greenColor, "SUCCESS!") - } else { - status = s.colorize(boldStyle+redColor, "FAIL!") - } - - flakes := "" - if s.enableFlakes { - flakes = " | " + s.colorize(yellowColor+boldStyle, "%d Flaked", summary.NumberOfFlakedSpecs) - } - - s.print(0, - "%s -- %s | %s | %s | %s\n", - status, - s.colorize(greenColor+boldStyle, "%d Passed", summary.NumberOfPassedSpecs), - s.colorize(redColor+boldStyle, "%d Failed", summary.NumberOfFailedSpecs)+flakes, - s.colorize(yellowColor+boldStyle, "%d Pending", summary.NumberOfPendingSpecs), - s.colorize(cyanColor+boldStyle, "%d Skipped", summary.NumberOfSkippedSpecs), - ) -} - -func (s *consoleStenographer) AnnounceSpecWillRun(spec *types.SpecSummary) { - s.startBlock() - for i, text := range spec.ComponentTexts[1 : len(spec.ComponentTexts)-1] { - s.print(0, s.colorize(alternatingColors[i%2], text)+" ") - } - - indentation := 0 - if len(spec.ComponentTexts) > 2 { - indentation = 1 - s.printNewLine() - } - index := len(spec.ComponentTexts) - 1 - s.print(indentation, s.colorize(boldStyle, spec.ComponentTexts[index])) - s.printNewLine() - s.print(indentation, s.colorize(lightGrayColor, spec.ComponentCodeLocations[index].String())) - s.printNewLine() - s.midBlock() -} - -func (s *consoleStenographer) AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { - s.announceSetupFailure("BeforeSuite", summary, succinct, fullTrace) -} - -func (s *consoleStenographer) AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { - s.announceSetupFailure("AfterSuite", summary, succinct, fullTrace) -} - -func (s *consoleStenographer) announceSetupFailure(name string, summary *types.SetupSummary, succinct bool, fullTrace bool) { - s.startBlock() - var message string - switch summary.State { - case types.SpecStateFailed: - message = "Failure" - case types.SpecStatePanicked: - message = "Panic" - case types.SpecStateTimedOut: - message = "Timeout" - } - - s.println(0, s.colorize(redColor+boldStyle, "%s [%.3f seconds]", message, summary.RunTime.Seconds())) - - indentation := s.printCodeLocationBlock([]string{name}, []types.CodeLocation{summary.CodeLocation}, summary.ComponentType, 0, summary.State, true) - - s.printNewLine() - s.printFailure(indentation, summary.State, summary.Failure, fullTrace) - - s.endBlock() -} - -func (s *consoleStenographer) AnnounceCapturedOutput(output string) { - if output == "" { - return - } - - s.startBlock() - s.println(0, output) - s.midBlock() -} - -func (s *consoleStenographer) AnnounceSuccessfulSpec(spec *types.SpecSummary) { - s.print(0, s.colorize(greenColor, s.denoter)) - s.stream() -} - -func (s *consoleStenographer) AnnounceSuccessfulSlowSpec(spec *types.SpecSummary, succinct bool) { - s.printBlockWithMessage( - s.colorize(greenColor, "%s [SLOW TEST:%.3f seconds]", s.denoter, spec.RunTime.Seconds()), - "", - spec, - succinct, - ) -} - -func (s *consoleStenographer) AnnounceSuccessfulMeasurement(spec *types.SpecSummary, succinct bool) { - s.printBlockWithMessage( - s.colorize(greenColor, "%s [MEASUREMENT]", s.denoter), - s.measurementReport(spec, succinct), - spec, - succinct, - ) -} - -func (s *consoleStenographer) AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) { - if noisy { - s.printBlockWithMessage( - s.colorize(yellowColor, "P [PENDING]"), - "", - spec, - false, - ) - } else { - s.print(0, s.colorize(yellowColor, "P")) - s.stream() - } -} - -func (s *consoleStenographer) AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool) { - // Skips at runtime will have a non-empty spec.Failure. All others should be succinct. - if succinct || spec.Failure == (types.SpecFailure{}) { - s.print(0, s.colorize(cyanColor, "S")) - s.stream() - } else { - s.startBlock() - s.println(0, s.colorize(cyanColor+boldStyle, "S [SKIPPING]%s [%.3f seconds]", s.failureContext(spec.Failure.ComponentType), spec.RunTime.Seconds())) - - indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, spec.Failure.ComponentType, spec.Failure.ComponentIndex, spec.State, succinct) - - s.printNewLine() - s.printSkip(indentation, spec.Failure) - s.endBlock() - } -} - -func (s *consoleStenographer) AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) { - s.printSpecFailure(fmt.Sprintf("%s... Timeout", s.denoter), spec, succinct, fullTrace) -} - -func (s *consoleStenographer) AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) { - s.printSpecFailure(fmt.Sprintf("%s! Panic", s.denoter), spec, succinct, fullTrace) -} - -func (s *consoleStenographer) AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) { - s.printSpecFailure(fmt.Sprintf("%s Failure", s.denoter), spec, succinct, fullTrace) -} - -func (s *consoleStenographer) SummarizeFailures(summaries []*types.SpecSummary) { - failingSpecs := []*types.SpecSummary{} - - for _, summary := range summaries { - if summary.HasFailureState() { - failingSpecs = append(failingSpecs, summary) - } - } - - if len(failingSpecs) == 0 { - return - } - - s.printNewLine() - s.printNewLine() - plural := "s" - if len(failingSpecs) == 1 { - plural = "" - } - s.println(0, s.colorize(redColor+boldStyle, "Summarizing %d Failure%s:", len(failingSpecs), plural)) - for _, summary := range failingSpecs { - s.printNewLine() - if summary.HasFailureState() { - if summary.TimedOut() { - s.print(0, s.colorize(redColor+boldStyle, "[Timeout...] ")) - } else if summary.Panicked() { - s.print(0, s.colorize(redColor+boldStyle, "[Panic!] ")) - } else if summary.Failed() { - s.print(0, s.colorize(redColor+boldStyle, "[Fail] ")) - } - s.printSpecContext(summary.ComponentTexts, summary.ComponentCodeLocations, summary.Failure.ComponentType, summary.Failure.ComponentIndex, summary.State, true) - s.printNewLine() - s.println(0, s.colorize(lightGrayColor, summary.Failure.Location.String())) - } - } -} - -func (s *consoleStenographer) startBlock() { - if s.cursorState == cursorStateStreaming { - s.printNewLine() - s.printDelimiter() - } else if s.cursorState == cursorStateMidBlock { - s.printNewLine() - } -} - -func (s *consoleStenographer) midBlock() { - s.cursorState = cursorStateMidBlock -} - -func (s *consoleStenographer) endBlock() { - s.printDelimiter() - s.cursorState = cursorStateEndBlock -} - -func (s *consoleStenographer) stream() { - s.cursorState = cursorStateStreaming -} - -func (s *consoleStenographer) printBlockWithMessage(header string, message string, spec *types.SpecSummary, succinct bool) { - s.startBlock() - s.println(0, header) - - indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, types.SpecComponentTypeInvalid, 0, spec.State, succinct) - - if message != "" { - s.printNewLine() - s.println(indentation, message) - } - - s.endBlock() -} - -func (s *consoleStenographer) printSpecFailure(message string, spec *types.SpecSummary, succinct bool, fullTrace bool) { - s.startBlock() - s.println(0, s.colorize(redColor+boldStyle, "%s%s [%.3f seconds]", message, s.failureContext(spec.Failure.ComponentType), spec.RunTime.Seconds())) - - indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, spec.Failure.ComponentType, spec.Failure.ComponentIndex, spec.State, succinct) - - s.printNewLine() - s.printFailure(indentation, spec.State, spec.Failure, fullTrace) - s.endBlock() -} - -func (s *consoleStenographer) failureContext(failedComponentType types.SpecComponentType) string { - switch failedComponentType { - case types.SpecComponentTypeBeforeSuite: - return " in Suite Setup (BeforeSuite)" - case types.SpecComponentTypeAfterSuite: - return " in Suite Teardown (AfterSuite)" - case types.SpecComponentTypeBeforeEach: - return " in Spec Setup (BeforeEach)" - case types.SpecComponentTypeJustBeforeEach: - return " in Spec Setup (JustBeforeEach)" - case types.SpecComponentTypeAfterEach: - return " in Spec Teardown (AfterEach)" - } - - return "" -} - -func (s *consoleStenographer) printSkip(indentation int, spec types.SpecFailure) { - s.println(indentation, s.colorize(cyanColor, spec.Message)) - s.printNewLine() - s.println(indentation, spec.Location.String()) -} - -func (s *consoleStenographer) printFailure(indentation int, state types.SpecState, failure types.SpecFailure, fullTrace bool) { - if state == types.SpecStatePanicked { - s.println(indentation, s.colorize(redColor+boldStyle, failure.Message)) - s.println(indentation, s.colorize(redColor, failure.ForwardedPanic)) - s.println(indentation, failure.Location.String()) - s.printNewLine() - s.println(indentation, s.colorize(redColor, "Full Stack Trace")) - s.println(indentation, failure.Location.FullStackTrace) - } else { - s.println(indentation, s.colorize(redColor, failure.Message)) - s.printNewLine() - s.println(indentation, failure.Location.String()) - if fullTrace { - s.printNewLine() - s.println(indentation, s.colorize(redColor, "Full Stack Trace")) - s.println(indentation, failure.Location.FullStackTrace) - } - } -} - -func (s *consoleStenographer) printSpecContext(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, state types.SpecState, succinct bool) int { - startIndex := 1 - indentation := 0 - - if len(componentTexts) == 1 { - startIndex = 0 - } - - for i := startIndex; i < len(componentTexts); i++ { - if (state.IsFailure() || state == types.SpecStateSkipped) && i == failedComponentIndex { - color := redColor - if state == types.SpecStateSkipped { - color = cyanColor - } - blockType := "" - switch failedComponentType { - case types.SpecComponentTypeBeforeSuite: - blockType = "BeforeSuite" - case types.SpecComponentTypeAfterSuite: - blockType = "AfterSuite" - case types.SpecComponentTypeBeforeEach: - blockType = "BeforeEach" - case types.SpecComponentTypeJustBeforeEach: - blockType = "JustBeforeEach" - case types.SpecComponentTypeAfterEach: - blockType = "AfterEach" - case types.SpecComponentTypeIt: - blockType = "It" - case types.SpecComponentTypeMeasure: - blockType = "Measurement" - } - if succinct { - s.print(0, s.colorize(color+boldStyle, "[%s] %s ", blockType, componentTexts[i])) - } else { - s.println(indentation, s.colorize(color+boldStyle, "%s [%s]", componentTexts[i], blockType)) - s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i])) - } - } else { - if succinct { - s.print(0, s.colorize(alternatingColors[i%2], "%s ", componentTexts[i])) - } else { - s.println(indentation, componentTexts[i]) - s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i])) - } - } - indentation++ - } - - return indentation -} - -func (s *consoleStenographer) printCodeLocationBlock(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, state types.SpecState, succinct bool) int { - indentation := s.printSpecContext(componentTexts, componentCodeLocations, failedComponentType, failedComponentIndex, state, succinct) - - if succinct { - if len(componentTexts) > 0 { - s.printNewLine() - s.print(0, s.colorize(lightGrayColor, "%s", componentCodeLocations[len(componentCodeLocations)-1])) - } - s.printNewLine() - indentation = 1 - } else { - indentation-- - } - - return indentation -} - -func (s *consoleStenographer) orderedMeasurementKeys(measurements map[string]*types.SpecMeasurement) []string { - orderedKeys := make([]string, len(measurements)) - for key, measurement := range measurements { - orderedKeys[measurement.Order] = key - } - return orderedKeys -} - -func (s *consoleStenographer) measurementReport(spec *types.SpecSummary, succinct bool) string { - if len(spec.Measurements) == 0 { - return "Found no measurements" - } - - message := []string{} - orderedKeys := s.orderedMeasurementKeys(spec.Measurements) - - if succinct { - message = append(message, fmt.Sprintf("%s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples))) - for _, key := range orderedKeys { - measurement := spec.Measurements[key] - message = append(message, fmt.Sprintf(" %s - %s: %s%s, %s: %s%s ± %s%s, %s: %s%s", - s.colorize(boldStyle, "%s", measurement.Name), - measurement.SmallestLabel, - s.colorize(greenColor, measurement.PrecisionFmt(), measurement.Smallest), - measurement.Units, - measurement.AverageLabel, - s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.Average), - measurement.Units, - s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.StdDeviation), - measurement.Units, - measurement.LargestLabel, - s.colorize(redColor, measurement.PrecisionFmt(), measurement.Largest), - measurement.Units, - )) - } - } else { - message = append(message, fmt.Sprintf("Ran %s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples))) - for _, key := range orderedKeys { - measurement := spec.Measurements[key] - info := "" - if measurement.Info != nil { - message = append(message, fmt.Sprintf("%v", measurement.Info)) - } - - message = append(message, fmt.Sprintf("%s:\n%s %s: %s%s\n %s: %s%s\n %s: %s%s ± %s%s", - s.colorize(boldStyle, "%s", measurement.Name), - info, - measurement.SmallestLabel, - s.colorize(greenColor, measurement.PrecisionFmt(), measurement.Smallest), - measurement.Units, - measurement.LargestLabel, - s.colorize(redColor, measurement.PrecisionFmt(), measurement.Largest), - measurement.Units, - measurement.AverageLabel, - s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.Average), - measurement.Units, - s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.StdDeviation), - measurement.Units, - )) - } - } - - return strings.Join(message, "\n") -} diff --git a/reporters/stenographer/support/README.md b/reporters/stenographer/support/README.md deleted file mode 100644 index 37de454f41..0000000000 --- a/reporters/stenographer/support/README.md +++ /dev/null @@ -1,6 +0,0 @@ -## Colorize Windows - -These packages are used for colorize on Windows and contributed by mattn.jp@gmail.com - - * go-colorable: - * go-isatty: diff --git a/reporters/stenographer/support/go-colorable/README.md b/reporters/stenographer/support/go-colorable/README.md deleted file mode 100644 index e84226a735..0000000000 --- a/reporters/stenographer/support/go-colorable/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# go-colorable - -Colorable writer for windows. - -For example, most of logger packages doesn't show colors on windows. (I know we can do it with ansicon. But I don't want.) -This package is possible to handle escape sequence for ansi color on windows. - -## Too Bad! - -![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/bad.png) - - -## So Good! - -![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/good.png) - -## Usage - -```go -logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true}) -logrus.SetOutput(colorable.NewColorableStdout()) - -logrus.Info("succeeded") -logrus.Warn("not correct") -logrus.Error("something error") -logrus.Fatal("panic") -``` - -You can compile above code on non-windows OSs. - -## Installation - -``` -$ go get github.com/mattn/go-colorable -``` - -# License - -MIT - -# Author - -Yasuhiro Matsumoto (a.k.a mattn) diff --git a/reporters/stenographer/support/go-colorable/colorable_others.go b/reporters/stenographer/support/go-colorable/colorable_others.go deleted file mode 100644 index 52d6653b34..0000000000 --- a/reporters/stenographer/support/go-colorable/colorable_others.go +++ /dev/null @@ -1,24 +0,0 @@ -// +build !windows - -package colorable - -import ( - "io" - "os" -) - -func NewColorable(file *os.File) io.Writer { - if file == nil { - panic("nil passed instead of *os.File to NewColorable()") - } - - return file -} - -func NewColorableStdout() io.Writer { - return os.Stdout -} - -func NewColorableStderr() io.Writer { - return os.Stderr -} diff --git a/reporters/stenographer/support/go-colorable/noncolorable.go b/reporters/stenographer/support/go-colorable/noncolorable.go deleted file mode 100644 index fb976dbd8b..0000000000 --- a/reporters/stenographer/support/go-colorable/noncolorable.go +++ /dev/null @@ -1,57 +0,0 @@ -package colorable - -import ( - "bytes" - "fmt" - "io" -) - -type NonColorable struct { - out io.Writer - lastbuf bytes.Buffer -} - -func NewNonColorable(w io.Writer) io.Writer { - return &NonColorable{out: w} -} - -func (w *NonColorable) Write(data []byte) (n int, err error) { - er := bytes.NewBuffer(data) -loop: - for { - c1, _, err := er.ReadRune() - if err != nil { - break loop - } - if c1 != 0x1b { - fmt.Fprint(w.out, string(c1)) - continue - } - c2, _, err := er.ReadRune() - if err != nil { - w.lastbuf.WriteRune(c1) - break loop - } - if c2 != 0x5b { - w.lastbuf.WriteRune(c1) - w.lastbuf.WriteRune(c2) - continue - } - - var buf bytes.Buffer - for { - c, _, err := er.ReadRune() - if err != nil { - w.lastbuf.WriteRune(c1) - w.lastbuf.WriteRune(c2) - w.lastbuf.Write(buf.Bytes()) - break loop - } - if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { - break - } - buf.Write([]byte(string(c))) - } - } - return len(data) - w.lastbuf.Len(), nil -} diff --git a/reporters/stenographer/support/go-isatty/LICENSE b/reporters/stenographer/support/go-isatty/LICENSE deleted file mode 100644 index 65dc692b6b..0000000000 --- a/reporters/stenographer/support/go-isatty/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) Yasuhiro MATSUMOTO - -MIT License (Expat) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/reporters/stenographer/support/go-isatty/README.md b/reporters/stenographer/support/go-isatty/README.md deleted file mode 100644 index 74845de4a2..0000000000 --- a/reporters/stenographer/support/go-isatty/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# go-isatty - -isatty for golang - -## Usage - -```go -package main - -import ( - "fmt" - "github.com/mattn/go-isatty" - "os" -) - -func main() { - if isatty.IsTerminal(os.Stdout.Fd()) { - fmt.Println("Is Terminal") - } else { - fmt.Println("Is Not Terminal") - } -} -``` - -## Installation - -``` -$ go get github.com/mattn/go-isatty -``` - -# License - -MIT - -# Author - -Yasuhiro Matsumoto (a.k.a mattn) diff --git a/reporters/stenographer/support/go-isatty/doc.go b/reporters/stenographer/support/go-isatty/doc.go deleted file mode 100644 index 17d4f90ebc..0000000000 --- a/reporters/stenographer/support/go-isatty/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package isatty implements interface to isatty -package isatty diff --git a/reporters/stenographer/support/go-isatty/isatty_appengine.go b/reporters/stenographer/support/go-isatty/isatty_appengine.go deleted file mode 100644 index 83c588773c..0000000000 --- a/reporters/stenographer/support/go-isatty/isatty_appengine.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build appengine - -package isatty - -// IsTerminal returns true if the file descriptor is terminal which -// is always false on on appengine classic which is a sandboxed PaaS. -func IsTerminal(fd uintptr) bool { - return false -} diff --git a/reporters/stenographer/support/go-isatty/isatty_bsd.go b/reporters/stenographer/support/go-isatty/isatty_bsd.go deleted file mode 100644 index 98ffe86a4b..0000000000 --- a/reporters/stenographer/support/go-isatty/isatty_bsd.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build darwin freebsd openbsd netbsd -// +build !appengine - -package isatty - -import ( - "syscall" - "unsafe" -) - -const ioctlReadTermios = syscall.TIOCGETA - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} diff --git a/reporters/stenographer/support/go-isatty/isatty_linux.go b/reporters/stenographer/support/go-isatty/isatty_linux.go deleted file mode 100644 index 9d24bac1db..0000000000 --- a/reporters/stenographer/support/go-isatty/isatty_linux.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build linux -// +build !appengine - -package isatty - -import ( - "syscall" - "unsafe" -) - -const ioctlReadTermios = syscall.TCGETS - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} diff --git a/reporters/stenographer/support/go-isatty/isatty_solaris.go b/reporters/stenographer/support/go-isatty/isatty_solaris.go deleted file mode 100644 index 1f0c6bf53d..0000000000 --- a/reporters/stenographer/support/go-isatty/isatty_solaris.go +++ /dev/null @@ -1,16 +0,0 @@ -// +build solaris -// +build !appengine - -package isatty - -import ( - "golang.org/x/sys/unix" -) - -// IsTerminal returns true if the given file descriptor is a terminal. -// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c -func IsTerminal(fd uintptr) bool { - var termio unix.Termio - err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio) - return err == nil -} diff --git a/reporters/stenographer/support/go-isatty/isatty_windows.go b/reporters/stenographer/support/go-isatty/isatty_windows.go deleted file mode 100644 index 83c398b16d..0000000000 --- a/reporters/stenographer/support/go-isatty/isatty_windows.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build windows -// +build !appengine - -package isatty - -import ( - "syscall" - "unsafe" -) - -var kernel32 = syscall.NewLazyDLL("kernel32.dll") -var procGetConsoleMode = kernel32.NewProc("GetConsoleMode") - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var st uint32 - r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) - return r != 0 && e == 0 -} diff --git a/reporters/teamcity_reporter.go b/reporters/teamcity_reporter.go index 84fd8aff87..8789e75f99 100644 --- a/reporters/teamcity_reporter.go +++ b/reporters/teamcity_reporter.go @@ -18,7 +18,7 @@ import ( ) const ( - messageId = "##teamcity" + teamcityMessageId = "##teamcity" ) type TeamCityReporter struct { @@ -33,69 +33,58 @@ func NewTeamCityReporter(writer io.Writer) *TeamCityReporter { } } -func (reporter *TeamCityReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { - reporter.testSuiteName = escape(summary.SuiteDescription) - fmt.Fprintf(reporter.writer, "%s[testSuiteStarted name='%s']\n", messageId, reporter.testSuiteName) -} - -func (reporter *TeamCityReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) { - reporter.handleSetupSummary("BeforeSuite", setupSummary) -} +func (reporter *TeamCityReporter) SpecSuiteWillBegin(conf config.GinkgoConfigType, summary types.SuiteSummary) { + reporter.testSuiteName = reporter.escape(summary.SuiteDescription) + reporter.ReporterConfig = config.DefaultReporterConfig -func (reporter *TeamCityReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) { - reporter.handleSetupSummary("AfterSuite", setupSummary) + fmt.Fprintf(reporter.writer, "%s[testSuiteStarted name='%s']\n", teamcityMessageId, reporter.testSuiteName) } -func (reporter *TeamCityReporter) handleSetupSummary(name string, setupSummary *types.SetupSummary) { - if setupSummary.State != types.SpecStatePassed { - testName := escape(name) - fmt.Fprintf(reporter.writer, "%s[testStarted name='%s']\n", messageId, testName) - message := reporter.failureMessage(setupSummary.Failure) - details := reporter.failureDetails(setupSummary.Failure) - fmt.Fprintf(reporter.writer, "%s[testFailed name='%s' message='%s' details='%s']\n", messageId, testName, message, details) - durationInMilliseconds := setupSummary.RunTime.Seconds() * 1000 - fmt.Fprintf(reporter.writer, "%s[testFinished name='%s' duration='%v']\n", messageId, testName, durationInMilliseconds) +func (reporter *TeamCityReporter) testNameFor(summary types.Summary) string { + if summary.LeafNodeType.Is(types.NodeTypesForSuiteSetup...) { + return reporter.escape(summary.LeafNodeType.String()) + } else { + return reporter.escape(strings.Join(summary.NodeTexts, " ")) } } -func (reporter *TeamCityReporter) SpecWillRun(specSummary *types.SpecSummary) { - testName := escape(strings.Join(specSummary.ComponentTexts[1:], " ")) - fmt.Fprintf(reporter.writer, "%s[testStarted name='%s']\n", messageId, testName) +func (reporter *TeamCityReporter) WillRun(summary types.Summary) { + fmt.Fprintf(reporter.writer, "%s[testStarted name='%s']\n", teamcityMessageId, reporter.testNameFor(summary)) } -func (reporter *TeamCityReporter) SpecDidComplete(specSummary *types.SpecSummary) { - testName := escape(strings.Join(specSummary.ComponentTexts[1:], " ")) +func (reporter *TeamCityReporter) DidRun(summary types.Summary) { + testName := reporter.testNameFor(summary) - if reporter.ReporterConfig.ReportPassed && specSummary.State == types.SpecStatePassed { - details := escape(specSummary.CapturedOutput) - fmt.Fprintf(reporter.writer, "%s[testPassed name='%s' details='%s']\n", messageId, testName, details) + if reporter.ReporterConfig.ReportPassed && summary.State == types.SpecStatePassed { + details := reporter.escape(summary.CombinedOutput()) + fmt.Fprintf(reporter.writer, "%s[testPassed name='%s' details='%s']\n", teamcityMessageId, testName, details) } - if specSummary.State == types.SpecStateFailed || specSummary.State == types.SpecStateTimedOut || specSummary.State == types.SpecStatePanicked { - message := reporter.failureMessage(specSummary.Failure) - details := reporter.failureDetails(specSummary.Failure) - fmt.Fprintf(reporter.writer, "%s[testFailed name='%s' message='%s' details='%s']\n", messageId, testName, message, details) + if summary.State.Is(types.SpecStateFailureStates...) { + message := reporter.failureMessage(summary.Failure) + details := reporter.failureDetails(summary.Failure) + fmt.Fprintf(reporter.writer, "%s[testFailed name='%s' message='%s' details='%s']\n", teamcityMessageId, testName, message, details) } - if specSummary.State == types.SpecStateSkipped || specSummary.State == types.SpecStatePending { - fmt.Fprintf(reporter.writer, "%s[testIgnored name='%s']\n", messageId, testName) + if summary.State == types.SpecStateSkipped || summary.State == types.SpecStatePending { + fmt.Fprintf(reporter.writer, "%s[testIgnored name='%s']\n", teamcityMessageId, testName) } - durationInMilliseconds := specSummary.RunTime.Seconds() * 1000 - fmt.Fprintf(reporter.writer, "%s[testFinished name='%s' duration='%v']\n", messageId, testName, durationInMilliseconds) + durationInMilliseconds := summary.RunTime.Seconds() * 1000 + fmt.Fprintf(reporter.writer, "%s[testFinished name='%s' duration='%v']\n", teamcityMessageId, testName, durationInMilliseconds) } -func (reporter *TeamCityReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { - fmt.Fprintf(reporter.writer, "%s[testSuiteFinished name='%s']\n", messageId, reporter.testSuiteName) +func (reporter *TeamCityReporter) SpecSuiteDidEnd(summary types.SuiteSummary) { + fmt.Fprintf(reporter.writer, "%s[testSuiteFinished name='%s']\n", teamcityMessageId, reporter.testSuiteName) } -func (reporter *TeamCityReporter) failureMessage(failure types.SpecFailure) string { - return escape(failure.ComponentCodeLocation.String()) +func (reporter *TeamCityReporter) failureMessage(failure types.Failure) string { + return reporter.escape(failure.NodeType.String()) } -func (reporter *TeamCityReporter) failureDetails(failure types.SpecFailure) string { - return escape(fmt.Sprintf("%s\n%s", failure.Message, failure.Location.String())) +func (reporter *TeamCityReporter) failureDetails(failure types.Failure) string { + return reporter.escape(fmt.Sprintf("%s\n%s", failure.Message, failure.Location.String())) } -func escape(output string) string { +func (reporter *TeamCityReporter) escape(output string) string { output = strings.Replace(output, "|", "||", -1) output = strings.Replace(output, "'", "|'", -1) output = strings.Replace(output, "\n", "|n", -1) diff --git a/reporters/teamcity_reporter_test.go b/reporters/teamcity_reporter_test.go index f7c4729fd8..ebf1637878 100644 --- a/reporters/teamcity_reporter_test.go +++ b/reporters/teamcity_reporter_test.go @@ -7,7 +7,6 @@ import ( . "github.com/onsi/ginkgo" "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/internal/codelocation" "github.com/onsi/ginkgo/reporters" "github.com/onsi/ginkgo/types" . "github.com/onsi/gomega" @@ -22,7 +21,7 @@ var _ = Describe("TeamCity Reporter", func() { BeforeEach(func() { buffer.Truncate(0) reporter = reporters.NewTeamCityReporter(&buffer) - reporter.SpecSuiteWillBegin(config.GinkgoConfigType{}, &types.SuiteSummary{ + reporter.SpecSuiteWillBegin(config.GinkgoConfigType{}, types.SuiteSummary{ SuiteDescription: "Foo's test suite", NumberOfSpecsThatWillBeRun: 1, }) @@ -30,29 +29,19 @@ var _ = Describe("TeamCity Reporter", func() { Describe("a passing test", func() { BeforeEach(func() { - beforeSuite := &types.SetupSummary{ - State: types.SpecStatePassed, - } - reporter.BeforeSuiteDidRun(beforeSuite) - - afterSuite := &types.SetupSummary{ - State: types.SpecStatePassed, - } - reporter.AfterSuiteDidRun(afterSuite) - // Set the ReportPassed config flag, in order to show captured output when tests have passed. reporter.ReporterConfig.ReportPassed = true - spec := &types.SpecSummary{ - ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, - CapturedOutput: "Test scenario...", - State: types.SpecStatePassed, - RunTime: 5 * time.Second, + spec := types.Summary{ + NodeTexts: []string{"A", "B", "C"}, + CapturedGinkgoWriterOutput: "Test scenario...", + State: types.SpecStatePassed, + RunTime: 5 * time.Second, } - reporter.SpecWillRun(spec) - reporter.SpecDidComplete(spec) + reporter.WillRun(spec) + reporter.DidRun(spec) - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + reporter.SpecSuiteDidEnd(types.SuiteSummary{ NumberOfSpecsThatWillBeRun: 1, NumberOfFailedSpecs: 0, RunTime: 10 * time.Second, @@ -72,21 +61,23 @@ var _ = Describe("TeamCity Reporter", func() { }) Describe("when the BeforeSuite fails", func() { - var beforeSuite *types.SetupSummary + var beforeSuite types.Summary BeforeEach(func() { - beforeSuite = &types.SetupSummary{ - State: types.SpecStateFailed, - RunTime: 3 * time.Second, - Failure: types.SpecFailure{ - Message: "failed to setup\n", - ComponentCodeLocation: codelocation.New(0), - Location: codelocation.New(2), + beforeSuite = types.Summary{ + LeafNodeType: types.NodeTypeBeforeSuite, + State: types.SpecStateFailed, + RunTime: 3 * time.Second, + Failure: types.Failure{ + Message: "failed to setup\n", + NodeType: types.NodeTypeBeforeSuite, + Location: types.NewCodeLocation(2), }, } - reporter.BeforeSuiteDidRun(beforeSuite) + reporter.WillRun(beforeSuite) + reporter.DidRun(beforeSuite) - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + reporter.SpecSuiteDidEnd(types.SuiteSummary{ NumberOfSpecsThatWillBeRun: 1, NumberOfFailedSpecs: 1, RunTime: 10 * time.Second, @@ -98,80 +89,43 @@ var _ = Describe("TeamCity Reporter", func() { expected := fmt.Sprintf( "##teamcity[testSuiteStarted name='Foo|'s test suite']\n"+ "##teamcity[testStarted name='BeforeSuite']\n"+ - "##teamcity[testFailed name='BeforeSuite' message='%s' details='failed to setup|n|n%s']\n"+ + "##teamcity[testFailed name='BeforeSuite' message='BeforeSuite' details='failed to setup|n|n%s']\n"+ "##teamcity[testFinished name='BeforeSuite' duration='3000']\n"+ "##teamcity[testSuiteFinished name='Foo|'s test suite']\n", - beforeSuite.Failure.ComponentCodeLocation.String(), beforeSuite.Failure.Location.String(), ) Ω(actual).Should(Equal(expected)) }) }) - Describe("when the AfterSuite fails", func() { - var afterSuite *types.SetupSummary - - BeforeEach(func() { - afterSuite = &types.SetupSummary{ - State: types.SpecStateFailed, - RunTime: 3 * time.Second, - Failure: types.SpecFailure{ - Message: "failed to setup\n", - ComponentCodeLocation: codelocation.New(0), - Location: codelocation.New(2), - }, - } - reporter.AfterSuiteDidRun(afterSuite) - - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ - NumberOfSpecsThatWillBeRun: 1, - NumberOfFailedSpecs: 1, - RunTime: 10 * time.Second, - }) - }) - - It("should record the test as having failed", func() { - actual := buffer.String() - expected := fmt.Sprintf( - "##teamcity[testSuiteStarted name='Foo|'s test suite']\n"+ - "##teamcity[testStarted name='AfterSuite']\n"+ - "##teamcity[testFailed name='AfterSuite' message='%s' details='failed to setup|n|n%s']\n"+ - "##teamcity[testFinished name='AfterSuite' duration='3000']\n"+ - "##teamcity[testSuiteFinished name='Foo|'s test suite']\n", - afterSuite.Failure.ComponentCodeLocation.String(), - afterSuite.Failure.Location.String(), - ) - Ω(actual).Should(Equal(expected)) - }) - }) specStateCases := []struct { state types.SpecState message string }{ {types.SpecStateFailed, "Failure"}, - {types.SpecStateTimedOut, "Timeout"}, - {types.SpecStatePanicked, "Panic"}, + {types.SpecStatePanicked, "Panicked"}, + {types.SpecStateInterrupted, "interrupted"}, } for _, specStateCase := range specStateCases { specStateCase := specStateCase Describe("a failing test", func() { - var spec *types.SpecSummary + var spec types.Summary BeforeEach(func() { - spec = &types.SpecSummary{ - ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, - State: specStateCase.state, - RunTime: 5 * time.Second, - Failure: types.SpecFailure{ - ComponentCodeLocation: codelocation.New(0), - Location: codelocation.New(2), - Message: "I failed", + spec = types.Summary{ + NodeTexts: []string{"A", "B", "C"}, + State: specStateCase.state, + RunTime: 5 * time.Second, + Failure: types.Failure{ + NodeType: types.NodeTypeJustBeforeEach, + Location: types.NewCodeLocation(2), + Message: "I failed", }, } - reporter.SpecWillRun(spec) - reporter.SpecDidComplete(spec) + reporter.WillRun(spec) + reporter.DidRun(spec) - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + reporter.SpecSuiteDidEnd(types.SuiteSummary{ NumberOfSpecsThatWillBeRun: 1, NumberOfFailedSpecs: 1, RunTime: 10 * time.Second, @@ -183,10 +137,9 @@ var _ = Describe("TeamCity Reporter", func() { expected := fmt.Sprintf("##teamcity[testSuiteStarted name='Foo|'s test suite']\n"+ "##teamcity[testStarted name='A B C']\n"+ - "##teamcity[testFailed name='A B C' message='%s' details='I failed|n%s']\n"+ + "##teamcity[testFailed name='A B C' message='JustBeforeEach' details='I failed|n%s']\n"+ "##teamcity[testFinished name='A B C' duration='5000']\n"+ "##teamcity[testSuiteFinished name='Foo|'s test suite']\n", - spec.Failure.ComponentCodeLocation.String(), spec.Failure.Location.String(), ) Ω(actual).Should(Equal(expected)) @@ -197,17 +150,17 @@ var _ = Describe("TeamCity Reporter", func() { for _, specStateCase := range []types.SpecState{types.SpecStatePending, types.SpecStateSkipped} { specStateCase := specStateCase Describe("a skipped test", func() { - var spec *types.SpecSummary + var spec types.Summary BeforeEach(func() { - spec = &types.SpecSummary{ - ComponentTexts: []string{"[Top Level]", "A", "B", "C"}, - State: specStateCase, - RunTime: 5 * time.Second, + spec = types.Summary{ + NodeTexts: []string{"A", "B", "C"}, + State: specStateCase, + RunTime: 5 * time.Second, } - reporter.SpecWillRun(spec) - reporter.SpecDidComplete(spec) + reporter.WillRun(spec) + reporter.DidRun(spec) - reporter.SpecSuiteDidEnd(&types.SuiteSummary{ + reporter.SpecSuiteDidEnd(types.SuiteSummary{ NumberOfSpecsThatWillBeRun: 1, NumberOfFailedSpecs: 0, RunTime: 10 * time.Second, diff --git a/types/code_location.go b/types/code_location.go index 935a89e136..c5f33b3a5a 100644 --- a/types/code_location.go +++ b/types/code_location.go @@ -2,6 +2,10 @@ package types import ( "fmt" + "regexp" + "runtime" + "runtime/debug" + "strings" ) type CodeLocation struct { @@ -13,3 +17,41 @@ type CodeLocation struct { func (codeLocation CodeLocation) String() string { return fmt.Sprintf("%s:%d", codeLocation.FileName, codeLocation.LineNumber) } + +func NewCodeLocation(skip int) CodeLocation { + _, file, line, _ := runtime.Caller(skip + 1) + stackTrace := PruneStack(string(debug.Stack()), skip+1) + return CodeLocation{FileName: file, LineNumber: line, FullStackTrace: stackTrace} +} + +// PruneStack removes references to functions that are internal to Ginkgo +// and the Go runtime from a stack string and a certain number of stack entries +// at the beginning of the stack. The stack string has the format +// as returned by runtime/debug.Stack. The leading goroutine information is +// optional and always removed if present. Beware that runtime/debug.Stack +// adds itself as first entry, so typically skip must be >= 1 to remove that +// entry. +func PruneStack(fullStackTrace string, skip int) string { + stack := strings.Split(fullStackTrace, "\n") + // Ensure that the even entries are the method names and the + // the odd entries the source code information. + if len(stack) > 0 && strings.HasPrefix(stack[0], "goroutine ") { + // Ignore "goroutine 29 [running]:" line. + stack = stack[1:] + } + // The "+1" is for skipping over the initial entry, which is + // runtime/debug.Stack() itself. + if len(stack) > 2*(skip+1) { + stack = stack[2*(skip+1):] + } + prunedStack := []string{} + re := regexp.MustCompile(`\/ginkgo\/|\/pkg\/testing\/|\/pkg\/runtime\/`) + for i := 0; i < len(stack)/2; i++ { + // We filter out based on the source code file name. + if !re.Match([]byte(stack[i*2+1])) { + prunedStack = append(prunedStack, stack[i*2]) + prunedStack = append(prunedStack, stack[i*2+1]) + } + } + return strings.Join(prunedStack, "\n") +} diff --git a/types/code_location_test.go b/types/code_location_test.go new file mode 100644 index 0000000000..9d5d0af2ad --- /dev/null +++ b/types/code_location_test.go @@ -0,0 +1,76 @@ +package types_test + +import ( + "runtime" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("CodeLocation", func() { + var codeLocation types.CodeLocation + var expectedFileName string + var expectedLineNumber int + + caller0 := func() { + codeLocation = types.NewCodeLocation(1) + } + + caller1 := func() { + _, expectedFileName, expectedLineNumber, _ = runtime.Caller(0) + expectedLineNumber += 2 + caller0() + } + + BeforeEach(func() { + caller1() + }) + + It("should use the passed in skip parameter to pick out the correct file & line number", func() { + Ω(codeLocation.FileName).Should(Equal(expectedFileName)) + Ω(codeLocation.LineNumber).Should(Equal(expectedLineNumber)) + }) + + Describe("stringer behavior", func() { + It("should stringify nicely", func() { + Ω(codeLocation.String()).Should(ContainSubstring("code_location_test.go:%d", expectedLineNumber)) + }) + }) + + Describe("PruneStack", func() { + It("should remove any references to ginkgo and pkg/testing and pkg/runtime", func() { + // Hard-coded string, loosely based on what debug.Stack() produces. + input := `Skip: skip() +/Skip/me +Skip: skip() +/Skip/me +Something: Func() +/Users/whoever/gospace/src/github.com/onsi/ginkgo/whatever.go:10 (0x12314) +SomethingInternalToGinkgo: Func() +/Users/whoever/gospace/src/github.com/onsi/ginkgo/whatever_else.go:10 (0x12314) +Oops: BlowUp() +/usr/goroot/pkg/strings/oops.go:10 (0x12341) +MyCode: Func() +/Users/whoever/gospace/src/mycode/code.go:10 (0x12341) +MyCodeTest: Func() +/Users/whoever/gospace/src/mycode/code_test.go:10 (0x12341) +TestFoo: RunSpecs(t, "Foo Suite") +/Users/whoever/gospace/src/mycode/code_suite_test.go:12 (0x37f08) +TestingT: Blah() +/usr/goroot/pkg/testing/testing.go:12 (0x37f08) +Something: Func() +/usr/goroot/pkg/runtime/runtime.go:12 (0x37f08) +` + prunedStack := types.PruneStack(input, 1) + Ω(prunedStack).Should(Equal(`Oops: BlowUp() +/usr/goroot/pkg/strings/oops.go:10 (0x12341) +MyCode: Func() +/Users/whoever/gospace/src/mycode/code.go:10 (0x12341) +MyCodeTest: Func() +/Users/whoever/gospace/src/mycode/code_test.go:10 (0x12341) +TestFoo: RunSpecs(t, "Foo Suite") +/Users/whoever/gospace/src/mycode/code_suite_test.go:12 (0x37f08)`)) + }) + }) +}) diff --git a/types/deprecated_support_test.go b/types/deprecated_support_test.go new file mode 100644 index 0000000000..c854124161 --- /dev/null +++ b/types/deprecated_support_test.go @@ -0,0 +1,172 @@ +package types_test + +import ( + "reflect" + "strings" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/types" +) + +var _ = Describe("Deprecation Support", func() { + Describe("Tracking Deprecations", func() { + var tracker *types.DeprecationTracker + + BeforeEach(func() { + tracker = types.NewDeprecationTracker() + formatter.SingletonFormatter.ColorMode = formatter.ColorModePassthrough + }) + + AfterEach(func() { + formatter.SingletonFormatter.ColorMode = formatter.ColorModeTerminal + }) + + Context("with no tracked deprecations", func() { + It("reports no tracked deprecations", func() { + Ω(tracker.DidTrackDeprecations()).Should(BeFalse()) + }) + }) + + Context("with tracked dependencies", func() { + BeforeEach(func() { + tracker.TrackDeprecation(types.Deprecation{ + Message: "Deprecation 1", + DocLink: "doclink-1", + }, types.CodeLocation{FileName: "foo.go", LineNumber: 17}) + tracker.TrackDeprecation(types.Deprecation{ + Message: "Deprecation 1", + DocLink: "doclink-1", + }, types.CodeLocation{FileName: "bar.go", LineNumber: 30}) + tracker.TrackDeprecation(types.Deprecation{ + Message: "Deprecation 2", + DocLink: "doclink-2", + }) + tracker.TrackDeprecation(types.Deprecation{ + Message: "Deprecation 3", + }, types.CodeLocation{FileName: "baz.go", LineNumber: 72}) + }) + + It("reports tracked deprecations", func() { + Ω(tracker.DidTrackDeprecations()).Should(BeTrue()) + }) + + It("generates a nicely formatted report", func() { + report := tracker.DeprecationsReport() + Ω(report).Should(HavePrefix("{{light-yellow}}You're using deprecated Ginkgo functionality:{{/}}\n{{light-yellow}}============================================={{/}}\n")) + Ω(report).Should(ContainSubstring(strings.Join([]string{ + " {{yellow}}Deprecation 1{{/}}", + " {{bold}}Learn more at:{{/}} {{cyan}}{{underline}}https://github.com/onsi/ginkgo/blob/v2/docs/MIGRATING_TO_V2.md#doclink-1{{/}}", + " {{gray}}foo.go:17{{/}}", + " {{gray}}bar.go:30{{/}}", + "", + }, "\n"))) + Ω(report).Should(ContainSubstring(strings.Join([]string{ + " {{yellow}}Deprecation 2{{/}}", + " {{bold}}Learn more at:{{/}} {{cyan}}{{underline}}https://github.com/onsi/ginkgo/blob/v2/docs/MIGRATING_TO_V2.md#doclink-2{{/}}", + "", + }, "\n"))) + Ω(report).Should(ContainSubstring(strings.Join([]string{ + " {{yellow}}Deprecation 3{{/}}", + " {{gray}}baz.go:72{{/}}", + }, "\n"))) + }) + + It("validates that all deprecations point to working documentation", func() { + v := reflect.ValueOf(types.Deprecations) + Ω(v.NumMethod()).Should(BeNumerically(">", 0)) + for i := 0; i < v.NumMethod(); i += 1 { + m := v.Method(i) + deprecation := m.Call([]reflect.Value{})[0].Interface().(types.Deprecation) + + if deprecation.DocLink != "" { + Ω(deprecation.DocLink).Should(BeElementOf(DEPRECATION_ANCHORS)) + } + } + }) + }) + }) + + Describe("DeprecatedSetupSummaryFromSummary", func() { + It("converts to the v1 summary format", func() { + cl1 := types.CodeLocation{FileName: "foo.go", LineNumber: 3} + cl2 := types.CodeLocation{FileName: "bar.go", LineNumber: 5} + Ω(types.DeprecatedSetupSummaryFromSummary(types.Summary{ + LeafNodeType: types.NodeTypeBeforeSuite, + LeafNodeLocation: cl1, + State: types.SpecStateFailed, + RunTime: time.Hour, + CapturedGinkgoWriterOutput: "ginkgo-writer-output", + CapturedStdOutErr: "std-output", + Failure: types.Failure{ + Message: "failure message", + Location: cl2, + ForwardedPanic: "forwarded panic", + NodeIndex: 2, + NodeType: types.NodeTypeBeforeSuite, + }, + })).Should(Equal( + &types.SetupSummary{ + ComponentType: types.SpecComponentTypeBeforeSuite, + CodeLocation: cl1, + State: types.SpecStateFailed, + RunTime: time.Hour, + CapturedOutput: "std-output\nginkgo-writer-output", + Failure: types.SpecFailure{ + Message: "failure message", + Location: cl2, + ForwardedPanic: "forwarded panic", + ComponentIndex: 2, + ComponentType: types.SpecComponentTypeBeforeSuite, + ComponentCodeLocation: cl2, + }, + }, + )) + }) + }) + + Describe("DeprecatedSpecSummaryFromSummary", func() { + It("converts to the v1 summary format", func() { + cl1 := types.CodeLocation{FileName: "foo.go", LineNumber: 3} + cl2 := types.CodeLocation{FileName: "bar.go", LineNumber: 5} + Ω(types.DeprecatedSpecSummaryFromSummary(types.Summary{ + NodeTexts: []string{"A", "B"}, + NodeLocations: []types.CodeLocation{cl1, cl2}, + LeafNodeType: types.NodeTypeBeforeSuite, + LeafNodeLocation: cl1, + State: types.SpecStateFailed, + RunTime: time.Hour, + CapturedGinkgoWriterOutput: "ginkgo-writer-output", + CapturedStdOutErr: "std-output", + Failure: types.Failure{ + Message: "failure message", + Location: cl2, + ForwardedPanic: "forwarded panic", + NodeIndex: 2, + NodeType: types.NodeTypeBeforeSuite, + }, + })).Should(Equal( + &types.SpecSummary{ + ComponentTexts: []string{"A", "B"}, + ComponentCodeLocations: []types.CodeLocation{cl1, cl2}, + State: types.SpecStateFailed, + RunTime: time.Hour, + CapturedOutput: "std-output\nginkgo-writer-output", + IsMeasurement: false, + Measurements: map[string]*types.SpecMeasurement{}, + Failure: types.SpecFailure{ + Message: "failure message", + Location: cl2, + ForwardedPanic: "forwarded panic", + ComponentIndex: 2, + ComponentType: types.SpecComponentTypeBeforeSuite, + ComponentCodeLocation: cl2, + }, + }, + )) + }) + }) +}) diff --git a/types/deprecation_support.go b/types/deprecation_support.go index c7bbfbf414..bf1f221644 100644 --- a/types/deprecation_support.go +++ b/types/deprecation_support.go @@ -1,6 +1,9 @@ package types import ( + "strconv" + "time" + "github.com/onsi/ginkgo/formatter" ) @@ -13,16 +16,9 @@ type deprecations struct{} var Deprecations = deprecations{} -func (d deprecations) CustomReporter() Deprecation { +func (d deprecations) V1Report() Deprecation { return Deprecation{ - Message: "You are using a custom reporter. Support for custom reporters will likely be removed in V2. Most users were using them to generate junit or teamcity reports and this functionality will be merged into the core reporter. In addition, Ginkgo 2.0 will support emitting a JSON-formatted report that users can then manipulate to generate custom reports.\n\n{{red}}{{bold}}If this change will be impactful to you please leave a comment on {{cyan}}{{underline}}https://github.com/onsi/ginkgo/issues/711{{/}}", - DocLink: "removed-custom-reporters", - } -} - -func (d deprecations) V1Reporter() Deprecation { - return Deprecation{ - Message: "You are using a V1 Ginkgo Reporter. Please update your custom reporter to the new V2 Reporter interface.", + Message: "You are susing a V1 Ginkgo Reporter. Please update your custom reporter to the new V2 Reporter interface.", DocLink: "changed-reporter-interface", } } @@ -48,11 +44,6 @@ func (d deprecations) Convert() Deprecation { } } -func (d deprecations) Blur() Deprecation { - return Deprecation{ - Message: "The blur command is deprecated in Ginkgo V2. Use 'ginkgo unfocus' instead.", - } -} type DeprecationTracker struct { deprecations map[Deprecation][]CodeLocation @@ -79,10 +70,6 @@ func (d *DeprecationTracker) DidTrackDeprecations() bool { func (d *DeprecationTracker) DeprecationsReport() string { out := formatter.F("{{light-yellow}}You're using deprecated Ginkgo functionality:{{/}}\n") out += formatter.F("{{light-yellow}}============================================={{/}}\n") - out += formatter.F("Ginkgo 2.0 is under active development and will introduce (a small number of) breaking changes.\n") - out += formatter.F("To learn more, view the migration guide at {{cyan}}{{underline}}https://github.com/onsi/ginkgo/blob/v2/docs/MIGRATING_TO_V2.md{{/}}\n") - out += formatter.F("To comment, chime in at {{cyan}}{{underline}}https://github.com/onsi/ginkgo/issues/711{{/}}\n") - for deprecation, locations := range d.deprecations { out += formatter.Fi(1, "{{yellow}}"+deprecation.Message+"{{/}}\n") if deprecation.DocLink != "" { @@ -94,3 +81,157 @@ func (d *DeprecationTracker) DeprecationsReport() string { } return out } + +/* + A set of deprecations to make the transition from v1 to v2 easier for users who have written custom reporters. +*/ + +type SetupSummary = DeprecatedSetupSummary +type SpecSummary = DeprecatedSpecSummary +type SpecMeasurement = DeprecatedSpecMeasurement +type SpecComponentType = NodeType +type SpecFailure = DeprecatedSpecFailure + +var ( + SpecComponentTypeInvalid = NodeTypeInvalid + SpecComponentTypeContainer = NodeTypeContainer + SpecComponentTypeIt = NodeTypeIt + SpecComponentTypeBeforeEach = NodeTypeBeforeEach + SpecComponentTypeJustBeforeEach = NodeTypeJustBeforeEach + SpecComponentTypeAfterEach = NodeTypeAfterEach + SpecComponentTypeJustAfterEach = NodeTypeJustAfterEach + SpecComponentTypeBeforeSuite = NodeTypeBeforeSuite + SpecComponentTypeSynchronizedBeforeSuite = NodeTypeSynchronizedBeforeSuite + SpecComponentTypeAfterSuite = NodeTypeAfterSuite + SpecComponentTypeSynchronizedAfterSuite = NodeTypeSynchronizedAfterSuite +) + +type DeprecatedSetupSummary struct { + ComponentType SpecComponentType + CodeLocation CodeLocation + + State SpecState + RunTime time.Duration + Failure SpecFailure + + CapturedOutput string + SuiteID string +} + +func DeprecatedSetupSummaryFromSummary(summary Summary) *DeprecatedSetupSummary { + return &DeprecatedSetupSummary{ + ComponentType: summary.LeafNodeType, + CodeLocation: summary.LeafNodeLocation, + State: summary.State, + RunTime: summary.RunTime, + Failure: deprecatedSpecFailureFromFailure(summary.Failure), + CapturedOutput: summary.CombinedOutput(), + } +} + +type DeprecatedSpecSummary struct { + ComponentTexts []string + ComponentCodeLocations []CodeLocation + + State SpecState + RunTime time.Duration + Failure SpecFailure + IsMeasurement bool + NumberOfSamples int + Measurements map[string]*DeprecatedSpecMeasurement + + CapturedOutput string + SuiteID string +} + +func DeprecatedSpecSummaryFromSummary(summary Summary) *DeprecatedSpecSummary { + return &DeprecatedSpecSummary{ + ComponentTexts: summary.NodeTexts, + ComponentCodeLocations: summary.NodeLocations, + State: summary.State, + RunTime: summary.RunTime, + Failure: deprecatedSpecFailureFromFailure(summary.Failure), + IsMeasurement: false, + NumberOfSamples: 0, + Measurements: map[string]*DeprecatedSpecMeasurement{}, + CapturedOutput: summary.CombinedOutput(), + } +} + +func (s DeprecatedSpecSummary) HasFailureState() bool { + return s.State.Is(SpecStateFailureStates...) +} + +func (s DeprecatedSpecSummary) TimedOut() bool { + return false +} + +func (s DeprecatedSpecSummary) Panicked() bool { + return s.State == SpecStatePanicked +} + +func (s DeprecatedSpecSummary) Failed() bool { + return s.State == SpecStateFailed +} + +func (s DeprecatedSpecSummary) Passed() bool { + return s.State == SpecStatePassed +} + +func (s DeprecatedSpecSummary) Skipped() bool { + return s.State == SpecStateSkipped +} + +func (s DeprecatedSpecSummary) Pending() bool { + return s.State == SpecStatePending +} + +type DeprecatedSpecFailure struct { + Message string + Location CodeLocation + ForwardedPanic string + + ComponentIndex int + ComponentType SpecComponentType + ComponentCodeLocation CodeLocation +} + +func deprecatedSpecFailureFromFailure(failure Failure) SpecFailure { + return SpecFailure{ + Message: failure.Message, + Location: failure.Location, + ForwardedPanic: failure.ForwardedPanic, + ComponentIndex: failure.NodeIndex, + ComponentType: failure.NodeType, + ComponentCodeLocation: failure.Location, + } +} + +type DeprecatedSpecMeasurement struct { + Name string + Info interface{} + Order int + + Results []float64 + + Smallest float64 + Largest float64 + Average float64 + StdDeviation float64 + + SmallestLabel string + LargestLabel string + AverageLabel string + Units string + Precision int +} + +func (s DeprecatedSpecMeasurement) PrecisionFmt() string { + if s.Precision == 0 { + return "%f" + } + + str := strconv.Itoa(s.Precision) + + return "%." + str + "f" +} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 0000000000..4e91dd9427 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,204 @@ +package types + +import ( + "reflect" + + "github.com/onsi/ginkgo/formatter" +) + +type GinkgoError struct { + Heading string + Message string + DocLink string + CodeLocation CodeLocation +} + +func (g GinkgoError) Error() string { + out := formatter.F("{{bold}}{{red}}%s{{/}}\n", g.Heading) + if (g.CodeLocation != CodeLocation{}) { + out += formatter.F("{{gray}}%s{{/}}\n", g.CodeLocation) + } + if g.Message != "" { + out += formatter.Fiw(1, formatter.COLS, g.Message) + out += "\n\n" + } + if g.DocLink != "" { + out += formatter.Fiw(1, formatter.COLS, "{{bold}}Learn more at:{{/}} {{cyan}}{{underline}}http://onsi.github.io/ginkgo/#%s{{/}}\n", g.DocLink) + } + + return out +} + +type ginkgoErrors struct{} + +var GinkgoErrors = ginkgoErrors{} + +func (g ginkgoErrors) UncaughtGinkgoPanic(cl CodeLocation) error { + return GinkgoError{ + Heading: "Your Test Panicked", + Message: `When you, or your assertion library, calls Ginkgo's Fail(), +Ginkgo panics to prevent subsequent assertions from running. + +Normally Ginkgo rescues this panic so you shouldn't see it. + +However, if you make an assertion in a goroutine, Ginkgo can't capture the panic. +To circumvent this, you should call + + defer GinkgoRecover() + +at the top of the goroutine that caused this panic. + +Alternatively, you may have made an assertion outside of the Ginkgo +testing tree (e.g. in a func init()) - please move your assertion to +an appropriate Ginkgo node (e.g. a BeforeSuite or as part of a specific test).`, + DocLink: "marking-specs-as-failed", + CodeLocation: cl, + } +} + +func (g ginkgoErrors) PushingNodeInRunPhase(nodeType NodeType, cl CodeLocation) error { + return GinkgoError{ + Heading: "Ginkgo detected an issue with your test structure", + Message: formatter.F( + `It looks like you are trying to add a {{bold}}[%s]{{/}} node +to the Ginkgo test tree in a leaf node {{bold}}after{{/}} the tests started running. + +To enable randomization and parallelization Ginkgo requires the test tree +to be fully construted up front. In practice, this means that you can +only create nodes like {{bold}}[%s]{{/}} at the top-level or within the +body of a {{bold}}Describe{{/}}, {{bold}}Context{{/}}, or {{bold}}When{{/}}.`, nodeType, nodeType), + CodeLocation: cl, + DocLink: "understanding-ginkgos-lifecycle", + } +} + +func (g ginkgoErrors) CaughtPanicDuringABuildPhase(cl CodeLocation) error { + return GinkgoError{ + Heading: "Assertion or Panic detected during tree construction", + Message: formatter.F( + `Ginkgo detected a panic while constructing the test tree. +You may be trying to make an assertion in the body of a container node +(i.e. {{bold}}Describe{{/}}, {{bold}}Context{{/}}, or {{bold}}When{{/}}). + +Please ensure all assertions are inside leaf nodes such as {{bold}}BeforeEach{{/}}, +{{bold}}It{{/}}, etc.`), + CodeLocation: cl, + DocLink: "do-not-make-assertions-in-container-node-functions", + } +} + +func (g ginkgoErrors) SetupNodeInNestedContext(nodeType NodeType, cl CodeLocation) error { + return GinkgoError{ + Heading: "Ginkgo detected an issue with your test structure", + Message: formatter.F( + `It looks like you are trying to add a {{bold}}[%s]{{/}} node within a container node. + +{{bold}}%s{{/}} can only be called at the top level.`, nodeType, nodeType), + CodeLocation: cl, + DocLink: "global-setup-and-teardown-beforesuite-and-aftersuite", + } +} + +func (g ginkgoErrors) SetupNodeDuringRunPhase(nodeType NodeType, cl CodeLocation) error { + return GinkgoError{ + Heading: "Ginkgo detected an issue with your test structure", + Message: formatter.F( + `It looks like you are trying to add a {{bold}}[%s]{{/}} node within a leaf node after the test started running. + +{{bold}}%s{{/}} can only be called at the top level.`, nodeType, nodeType), + CodeLocation: cl, + DocLink: "global-setup-and-teardown-beforesuite-and-aftersuite", + } +} + +func (g ginkgoErrors) MultipleBeforeSuiteNodes(nodeType NodeType, cl CodeLocation, earlierNodeType NodeType, earlierCodeLocation CodeLocation) error { + return ginkgoErrorMultipleSuiteNodes("setup", nodeType, cl, earlierNodeType, earlierCodeLocation) +} + +func (g ginkgoErrors) MultipleAfterSuiteNodes(nodeType NodeType, cl CodeLocation, earlierNodeType NodeType, earlierCodeLocation CodeLocation) error { + return ginkgoErrorMultipleSuiteNodes("teardown", nodeType, cl, earlierNodeType, earlierCodeLocation) +} + +func ginkgoErrorMultipleSuiteNodes(setupOrTeardown string, nodeType NodeType, cl CodeLocation, earlierNodeType NodeType, earlierCodeLocation CodeLocation) error { + return GinkgoError{ + Heading: "Ginkgo detected an issue with your test structure", + Message: formatter.F( + `It looks like you are trying to add a {{bold}}[%s]{{/}} node but +you already have a {{bold}}[%s]{{/}} node defined at: {{gray}}%s{{/}}. + +Ginkgo only allows you to define one suite %s node.`, nodeType, earlierNodeType, earlierCodeLocation, setupOrTeardown), + CodeLocation: cl, + DocLink: "global-setup-and-teardown-beforesuite-and-aftersuite", + } +} + +func (g ginkgoErrors) InvalidBodyType(t reflect.Type, cl CodeLocation) error { + return GinkgoError{ + Heading: "Invalid Parameter", + Message: formatter.F(`Ginkgo nodes must be passed {{bold}}func(){{/}} - i.e. functions that take nothing and return nothing. +You passed {{bold}}%s{{/}} instead.`, t), + CodeLocation: cl, + } +} + +var sharedParallelErrorMessage = "It looks like you are trying to run tests in parallel with go test.\nThis is unsupported and you should use the ginkgo CLI instead." + +func (g ginkgoErrors) InvalidParallelTotalConfiguration() error { + return GinkgoError{ + Heading: "-ginkgo.parallel.total must be >= 1", + Message: sharedParallelErrorMessage, + DocLink: "parallel-specs", + } +} + +func (g ginkgoErrors) InvalidParallelNodeConfiguration() error { + return GinkgoError{ + Heading: "-ginkgo.parallel.node is one-indexed and must be <= ginkgo.parallel.total", + Message: sharedParallelErrorMessage, + DocLink: "parallel-specs", + } +} + +func (g ginkgoErrors) MissingParallelHostConfiguration() error { + return GinkgoError{ + Heading: "-ginkgo.parallel.host is missing", + Message: sharedParallelErrorMessage, + DocLink: "parallel-specs", + } +} + +func (g ginkgoErrors) UnreachableParallelHost(host string) error { + return GinkgoError{ + Heading: "Could not reach ginkgo.parallel.host:" + host, + Message: sharedParallelErrorMessage, + DocLink: "parallel-specs", + } +} + +func (g ginkgoErrors) DryRunInParallelConfiguration() error { + return GinkgoError{ + Heading: "Ginkgo only performs -dryRun in serial mode.", + Message: "Please try running ginkgo -dryRun again, but without -p or -nodes to ensure the test is running in series.", + } +} + +func (g ginkgoErrors) ConflictingVerboseSuccinctConfiguration() error { + return GinkgoError{ + Heading: "Conflicting reporter verbosity settings -v and --succinct.", + Message: "You can't set both -v and --succinct. Please pick one!", + } +} + +func (g ginkgoErrors) InvalidGoFlagCount() error { + return GinkgoError{ + Heading: "Use of go test -count", + Message: "Ginkgo does not support using go test -count to rerun test suites. Please use the ginkgo cli and `ginkgo -until-it-fails` or `ginkgo -repeat`.", + } +} + +func (g ginkgoErrors) InvalidGoFlagParallel() error { + return GinkgoError{ + Heading: "Use of go test -parallel", + Message: "Go test's implementation of parallelization does not actually parallelize Ginkgo tests. Please use the ginkgo cli and `ginkgo -p` or `ginkgo -nodes=N` instad.", + } +} diff --git a/types/errors_test.go b/types/errors_test.go new file mode 100644 index 0000000000..ceadde85f3 --- /dev/null +++ b/types/errors_test.go @@ -0,0 +1,67 @@ +package types_test + +import ( + "reflect" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + "github.com/onsi/ginkgo/formatter" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("GinkgoErrors", func() { + BeforeEach(func() { + formatter.SingletonFormatter.ColorMode = formatter.ColorModePassthrough + }) + + AfterEach(func() { + formatter.SingletonFormatter.ColorMode = formatter.ColorModeTerminal + }) + + DescribeTable("error render cases", func(err error, expected ...string) { + Expect(err.Error()).To(HavePrefix(strings.Join(expected, "\n"))) + }, + Entry("an error with only a heading", + types.GinkgoError{ + Heading: "Error! Error!", + }, + "{{bold}}{{red}}Error! Error!{{/}}", + "", + ), + Entry("an error with all the things", + types.GinkgoError{ + Heading: "Error! Error!", + CodeLocation: types.CodeLocation{FileName: "foo.go", LineNumber: 17}, + Message: "An error occured.\nWelp!", + DocLink: "the-doc-section", + }, + "{{bold}}{{red}}Error! Error!{{/}}", + "{{gray}}foo.go:17{{/}}", + " An error occured.", + " Welp!", + "", + " {{bold}}Learn more at:{{/}}", + " {{cyan}}{{underline}}http://onsi.github.io/ginkgo/#the-doc-section{{/}}", + ), + ) + + It("validates that all errors point to working documentation", func() { + v := reflect.ValueOf(types.GinkgoErrors) + Ω(v.NumMethod()).Should(BeNumerically(">", 0)) + for i := 0; i < v.NumMethod(); i += 1 { + m := v.Method(i) + args := []reflect.Value{} + for j := 0; j < m.Type().NumIn(); j += 1 { + args = append(args, reflect.Zero(m.Type().In(j))) + } + + ginkgoError := m.Call(args)[0].Interface().(types.GinkgoError) + + if ginkgoError.DocLink != "" { + Ω(ginkgoError.DocLink).Should(BeElementOf(DOC_ANCHORS)) + } + } + }) +}) diff --git a/types/synchronization.go b/types/synchronization.go deleted file mode 100644 index fdd6ed5bdf..0000000000 --- a/types/synchronization.go +++ /dev/null @@ -1,30 +0,0 @@ -package types - -import ( - "encoding/json" -) - -type RemoteBeforeSuiteState int - -const ( - RemoteBeforeSuiteStateInvalid RemoteBeforeSuiteState = iota - - RemoteBeforeSuiteStatePending - RemoteBeforeSuiteStatePassed - RemoteBeforeSuiteStateFailed - RemoteBeforeSuiteStateDisappeared -) - -type RemoteBeforeSuiteData struct { - Data []byte - State RemoteBeforeSuiteState -} - -func (r RemoteBeforeSuiteData) ToJSON() []byte { - data, _ := json.Marshal(r) - return data -} - -type RemoteAfterSuiteData struct { - CanRun bool -} diff --git a/types/types.go b/types/types.go index c143e02d84..f40b4547b8 100644 --- a/types/types.go +++ b/types/types.go @@ -1,135 +1,89 @@ package types import ( - "strconv" + "encoding/json" "time" ) const GINKGO_FOCUS_EXIT_CODE = 197 -/* -SuiteSummary represents the a summary of the test suite and is passed to both -Reporter.SpecSuiteWillBegin -Reporter.SpecSuiteDidEnd - -this is unfortunate as these two methods should receive different objects. When running in parallel -each node does not deterministically know how many specs it will end up running. - -Unfortunately making such a change would break backward compatibility. - -Until Ginkgo 2.0 comes out we will continue to reuse this struct but populate unknown fields -with -1. -*/ type SuiteSummary struct { SuiteDescription string SuiteSucceeded bool - SuiteID string - - NumberOfSpecsBeforeParallelization int - NumberOfTotalSpecs int - NumberOfSpecsThatWillBeRun int - NumberOfPendingSpecs int - NumberOfSkippedSpecs int - NumberOfPassedSpecs int - NumberOfFailedSpecs int - // Flaked specs are those that failed initially, but then passed on a - // subsequent try. - NumberOfFlakedSpecs int - RunTime time.Duration -} - -type SpecSummary struct { - ComponentTexts []string - ComponentCodeLocations []CodeLocation - State SpecState - RunTime time.Duration - Failure SpecFailure - IsMeasurement bool - NumberOfSamples int - Measurements map[string]*SpecMeasurement + NumberOfTotalSpecs int + NumberOfSpecsThatWillBeRun int - CapturedOutput string - SuiteID string + NumberOfSkippedSpecs int + NumberOfPassedSpecs int + NumberOfFailedSpecs int + NumberOfPendingSpecs int + NumberOfFlakedSpecs int + RunTime time.Duration } -func (s SpecSummary) HasFailureState() bool { - return s.State.IsFailure() +func (summary SuiteSummary) NumberOfSpecsThatRan() int { + return summary.NumberOfPassedSpecs + summary.NumberOfFailedSpecs } -func (s SpecSummary) TimedOut() bool { - return s.State == SpecStateTimedOut -} +func (summary SuiteSummary) Add(other SuiteSummary) SuiteSummary { + out := SuiteSummary{} + out.SuiteDescription = summary.SuiteDescription + out.SuiteSucceeded = summary.SuiteSucceeded && other.SuiteSucceeded + out.NumberOfTotalSpecs = summary.NumberOfTotalSpecs + out.NumberOfSpecsThatWillBeRun = summary.NumberOfSpecsThatWillBeRun + + out.NumberOfSkippedSpecs = summary.NumberOfSkippedSpecs + other.NumberOfSkippedSpecs + out.NumberOfPassedSpecs = summary.NumberOfPassedSpecs + other.NumberOfPassedSpecs + out.NumberOfFailedSpecs = summary.NumberOfFailedSpecs + other.NumberOfFailedSpecs + out.NumberOfPendingSpecs = summary.NumberOfPendingSpecs + other.NumberOfPendingSpecs + out.NumberOfFlakedSpecs = summary.NumberOfFlakedSpecs + other.NumberOfFlakedSpecs + if summary.RunTime > other.RunTime { + out.RunTime = summary.RunTime + } else { + out.RunTime = other.RunTime + } -func (s SpecSummary) Panicked() bool { - return s.State == SpecStatePanicked + return out } -func (s SpecSummary) Failed() bool { - return s.State == SpecStateFailed -} +type Summary struct { + NodeTexts []string + NodeLocations []CodeLocation -func (s SpecSummary) Passed() bool { - return s.State == SpecStatePassed -} + LeafNodeType NodeType + LeafNodeLocation CodeLocation -func (s SpecSummary) Skipped() bool { - return s.State == SpecStateSkipped -} + State SpecState + RunTime time.Duration + Failure Failure + NumAttempts int -func (s SpecSummary) Pending() bool { - return s.State == SpecStatePending + CapturedStdOutErr string + CapturedGinkgoWriterOutput string } -type SetupSummary struct { - ComponentType SpecComponentType - CodeLocation CodeLocation - - State SpecState - RunTime time.Duration - Failure SpecFailure - - CapturedOutput string - SuiteID string +func (summary Summary) CombinedOutput() string { + output := summary.CapturedStdOutErr + if output == "" { + output = summary.CapturedGinkgoWriterOutput + } else { + output = output + "\n" + summary.CapturedGinkgoWriterOutput + } + return output } -type SpecFailure struct { +type Failure struct { Message string Location CodeLocation ForwardedPanic string - ComponentIndex int - ComponentType SpecComponentType - ComponentCodeLocation CodeLocation + NodeIndex int + NodeType NodeType } -type SpecMeasurement struct { - Name string - Info interface{} - Order int - - Results []float64 - - Smallest float64 - Largest float64 - Average float64 - StdDeviation float64 - - SmallestLabel string - LargestLabel string - AverageLabel string - Units string - Precision int -} - -func (s SpecMeasurement) PrecisionFmt() string { - if s.Precision == 0 { - return "%f" - } - - str := strconv.Itoa(s.Precision) - - return "%." + str + "f" +func (f Failure) IsZero() bool { + return f == Failure{} } type SpecState uint @@ -142,33 +96,101 @@ const ( SpecStatePassed SpecStateFailed SpecStatePanicked - SpecStateTimedOut + SpecStateInterrupted ) -func (state SpecState) IsFailure() bool { - return state == SpecStateTimedOut || state == SpecStatePanicked || state == SpecStateFailed +var SpecStateFailureStates = []SpecState{SpecStateFailed, SpecStatePanicked, SpecStateInterrupted} + +func (state SpecState) Is(states ...SpecState) bool { + for _, testState := range states { + if testState == state { + return true + } + } + + return false } -type SpecComponentType uint +type NodeType uint const ( - SpecComponentTypeInvalid SpecComponentType = iota - - SpecComponentTypeContainer - SpecComponentTypeBeforeSuite - SpecComponentTypeAfterSuite - SpecComponentTypeBeforeEach - SpecComponentTypeJustBeforeEach - SpecComponentTypeJustAfterEach - SpecComponentTypeAfterEach - SpecComponentTypeIt - SpecComponentTypeMeasure + NodeTypeInvalid NodeType = iota + + NodeTypeContainer + NodeTypeIt + + NodeTypeBeforeEach + NodeTypeJustBeforeEach + NodeTypeAfterEach + NodeTypeJustAfterEach + + NodeTypeBeforeSuite + NodeTypeSynchronizedBeforeSuite + NodeTypeAfterSuite + NodeTypeSynchronizedAfterSuite ) -type FlagType uint +var NodeTypesForContainerAndIt = []NodeType{NodeTypeContainer, NodeTypeIt} +var NodeTypesForSuiteSetup = []NodeType{NodeTypeBeforeSuite, NodeTypeSynchronizedBeforeSuite, NodeTypeAfterSuite, NodeTypeSynchronizedAfterSuite} + +func (nt NodeType) Is(nodeTypes ...NodeType) bool { + for _, nodeType := range nodeTypes { + if nt == nodeType { + return true + } + } + + return false +} + +func (nt NodeType) String() string { + switch nt { + case NodeTypeContainer: + return "Container" + case NodeTypeIt: + return "It" + case NodeTypeBeforeEach: + return "BeforeEach" + case NodeTypeJustBeforeEach: + return "JustBeforeEach" + case NodeTypeAfterEach: + return "AfterEach" + case NodeTypeJustAfterEach: + return "JustAfterEach" + case NodeTypeBeforeSuite: + return "BeforeSuite" + case NodeTypeSynchronizedBeforeSuite: + return "SynchronizedBeforeSuite" + case NodeTypeAfterSuite: + return "AfterSuite" + case NodeTypeSynchronizedAfterSuite: + return "SynchronizedAfterSuite" + } + + return "INVALID NODE TYPE" +} + +type RemoteBeforeSuiteState int const ( - FlagTypeNone FlagType = iota - FlagTypeFocused - FlagTypePending + RemoteBeforeSuiteStateInvalid RemoteBeforeSuiteState = iota + + RemoteBeforeSuiteStatePending + RemoteBeforeSuiteStatePassed + RemoteBeforeSuiteStateFailed + RemoteBeforeSuiteStateDisappeared ) + +type RemoteBeforeSuiteData struct { + Data []byte + State RemoteBeforeSuiteState +} + +func (r RemoteBeforeSuiteData) ToJSON() []byte { + data, _ := json.Marshal(r) + return data +} + +type RemoteAfterSuiteData struct { + CanRun bool +} diff --git a/types/types_suite_test.go b/types/types_suite_test.go index b026169c12..186c3825cb 100644 --- a/types/types_suite_test.go +++ b/types/types_suite_test.go @@ -1,13 +1,17 @@ package types_test import ( + "testing" + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/internal/test_helpers" . "github.com/onsi/gomega" - - "testing" ) func TestTypes(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Types Suite") } + +var DOC_ANCHORS = test_helpers.LoadMarkdownHeadingAnchors("../docs/index.md") +var DEPRECATION_ANCHORS = test_helpers.LoadMarkdownHeadingAnchors("../docs/MIGRATING_TO_V2.md") diff --git a/types/types_test.go b/types/types_test.go index a0e161c888..293f1f4d9d 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -1,99 +1,39 @@ package types_test import ( - . "github.com/onsi/ginkgo/types" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" -) - -var specStates = []SpecState{ - SpecStatePassed, - SpecStateTimedOut, - SpecStatePanicked, - SpecStateFailed, - SpecStatePending, - SpecStateSkipped, -} - -func verifySpecSummary(caller func(SpecSummary) bool, trueStates ...SpecState) { - summary := SpecSummary{} - trueStateLookup := map[SpecState]bool{} - for _, state := range trueStates { - trueStateLookup[state] = true - summary.State = state - Ω(caller(summary)).Should(BeTrue()) - } - for _, state := range specStates { - if trueStateLookup[state] { - continue - } - summary.State = state - Ω(caller(summary)).Should(BeFalse()) - } -} + . "github.com/onsi/ginkgo/extensions/table" + "github.com/onsi/ginkgo/types" +) var _ = Describe("Types", func() { - Describe("IsFailureState", func() { - It("knows when it is in a failure-like state", func() { - verifySpecSummary(func(summary SpecSummary) bool { - return summary.State.IsFailure() - }, SpecStateTimedOut, SpecStatePanicked, SpecStateFailed) - }) - }) - - Describe("SpecSummary", func() { - It("knows when it is in a failure-like state", func() { - verifySpecSummary(func(summary SpecSummary) bool { - return summary.HasFailureState() - }, SpecStateTimedOut, SpecStatePanicked, SpecStateFailed) - }) - - It("knows when it passed", func() { - verifySpecSummary(func(summary SpecSummary) bool { - return summary.Passed() - }, SpecStatePassed) - }) - - It("knows when it has failed", func() { - verifySpecSummary(func(summary SpecSummary) bool { - return summary.Failed() - }, SpecStateFailed) - }) - - It("knows when it has panicked", func() { - verifySpecSummary(func(summary SpecSummary) bool { - return summary.Panicked() - }, SpecStatePanicked) - }) - - It("knows when it has timed out", func() { - verifySpecSummary(func(summary SpecSummary) bool { - return summary.TimedOut() - }, SpecStateTimedOut) - }) - - It("knows when it is pending", func() { - verifySpecSummary(func(summary SpecSummary) bool { - return summary.Pending() - }, SpecStatePending) - }) - - It("knows when it is skipped", func() { - verifySpecSummary(func(summary SpecSummary) bool { - return summary.Skipped() - }, SpecStateSkipped) - }) - }) - - Describe("SpecMeasurement", func() { - It("knows how to format values when the precision is 0", func() { - Ω(SpecMeasurement{}.PrecisionFmt()).Should(Equal("%f")) - }) - - It("knows how to format the values when the precision is 3", func() { - Ω(SpecMeasurement{Precision: 3}.PrecisionFmt()).Should(Equal("%.3f")) - }) + var _ = Describe("NodeType", func() { + Describe("Is", func() { + It("returns true when the NodeType is in the passed-in list", func() { + Ω(types.NodeTypeContainer.Is(types.NodeTypeIt, types.NodeTypeContainer)).Should(BeTrue()) + }) + + It("returns false when the NodeType is not in the passed-in list", func() { + Ω(types.NodeTypeContainer.Is(types.NodeTypeIt, types.NodeTypeBeforeEach)).Should(BeFalse()) + }) + }) + + DescribeTable("String and AsComponentType", func(nodeType types.NodeType, expectedString string) { + Ω(nodeType.String()).Should(Equal(expectedString)) + }, + Entry("Container", types.NodeTypeContainer, "Container"), + Entry("It", types.NodeTypeIt, "It"), + Entry("BeforeEach", types.NodeTypeBeforeEach, "BeforeEach"), + Entry("JustBeforeEach", types.NodeTypeJustBeforeEach, "JustBeforeEach"), + Entry("AfterEach", types.NodeTypeAfterEach, "AfterEach"), + Entry("JustAfterEach", types.NodeTypeJustAfterEach, "JustAfterEach"), + Entry("BeforeSuite", types.NodeTypeBeforeSuite, "BeforeSuite"), + Entry("SynchronizedBeforeSuite", types.NodeTypeSynchronizedBeforeSuite, "SynchronizedBeforeSuite"), + Entry("AfterSuite", types.NodeTypeAfterSuite, "AfterSuite"), + Entry("SynchronizedAfterSuite", types.NodeTypeSynchronizedAfterSuite, "SynchronizedAfterSuite"), + Entry("Invalid", types.NodeTypeInvalid, "INVALID NODE TYPE"), + ) }) })