diff --git a/event-protocol/Makefile b/event-protocol/Makefile index cd384ba9c0..e3a0c4a31c 100644 --- a/event-protocol/Makefile +++ b/event-protocol/Makefile @@ -1,25 +1,13 @@ -EXAMPLES = $(wildcard examples/events/*.json) -SCHEMAS = $(wildcard schemas/*.json) -SRC = $(wildcard bin/* lib/*) +MAKEFILES=validator/Makefile ruby/Makefile -default: .valid - yarn link +default: $(patsubst %/Makefile,default-%,$(MAKEFILES)) .PHONY: default -.built: yarn.lock $(SRC) - yarn test - touch $@ +default-%: % + cd $< && make default -yarn.lock: package.json - yarn install - -clean: - rm -rf yarn.lock node_modules coverage dist .built .valid examples/events.ndjson +clean: $(patsubst %/Makefile,clean-%,$(MAKEFILES)) .PHONY: clean -.valid: examples/events.ndjson yarn.lock ./bin/cucumber-event-validator.js $(SCHEMAS) $(SRC) - @cat $< | node ./bin/cucumber-event-validator.js - @touch $@ - -examples/events.ndjson: $(EXAMPLES) - @cat $(EXAMPLES) | jq --compact-output "." > $@ +clean-%: % + cd $< && make clean diff --git a/event-protocol/examples/events.ndjson b/event-protocol/examples/events.ndjson deleted file mode 100644 index 934edc315a..0000000000 --- a/event-protocol/examples/events.ndjson +++ /dev/null @@ -1,5 +0,0 @@ -{"data":"Feature: Hello\n Scenario: World\n Given a step\n\n","media":{"encoding":"utf-8","type":"text/vnd.cucumber.gherkin+plain"},"type":"source","uri":"features/hello.feature"} -{"document":{"comments":[],"feature":{"children":[{"keyword":"Scenario","location":{"column":3,"line":2},"name":"World","steps":[{"keyword":"Given ","location":{"column":5,"line":3},"text":"a step","type":"Step"}],"tags":[],"type":"Scenario"}],"keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Hello","tags":[],"type":"Feature"},"type":"GherkinDocument"},"type":"gherkin-document","uri":"features/hello.feature"} -{"pickle":{"language":"en","locations":[{"column":3,"line":2}],"name":"World","steps":[{"arguments":[],"locations":[{"column":11,"line":3}],"text":"a step"}],"tags":[]},"type":"pickle","uri":"features/hello.feature"} -{"type":"attachment","source":{"uri":"features/hello.feature","start":{"line":3,"column":7}},"data":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAELUExURUdwTACoGAClFACqHACoFwCnGQCnFQCnGACoGQCoGQCoGACuGgCnFwCiFwCoGACoGACnFwClFgCpGACoFwD/AACnGAC2JACoGACnFwCqKgCoGACoGACqGQCoFwCnFwCoFwB/AACnGACoGAC/AACoGACoFgCmFwCnFwCnFwCqFgCmFgCZAACoGC64Ql3IbA2sJBuxMBiwLhewLR2yMgmrIOn36/z+/BGuJ9/04rHkuIDUjGHJcA6tJeD04wGoGQusIpnco+T257PlugSpHN704XDOfQeqHiCzNc7u0+z57t3z4HvSh7fmvj69UJzdpSi2PNjy3AKpGlrHahqxMBCtJpHZm83u0jS6R0vCXEWczDEAAAAsdFJOUwDIJQlYPSP7cFCqE1cW+mf4ImiBAXQHPu8GfP0zzoyOAp2wBOg4a2PqOU4FxgPjdgAAAK9JREFUGNNNj1UCwkAMRBcotLi7uw1S3N3d4f4nYSvIfO28bCYJIaI0OSabJ1+pFaDKpD4+zUJS1CHVWSwer+esDNhEQP+fV/f5uAd4XEIesNldl5WK0KSlQAtcDkc5xUcBA+y3tL/UKQIhCpLA+sTzw1K7UQVHQTyB23QyKraaNSAgTOGAQb9bqBeAoDg2EpMT4XdLm3rD8qrO7zFWO7UW8+86oodJp/zzRGUwyq83Mjcb8VXl0ZMAAAAASUVORK5CYII=","media":{"encoding":"base64","type":"image/png"}} -{"type":"attachment","source":{"uri":"features/hello.feature","start":{"line":3,"column":7}},"data":"Exception in thread \"main\" java.lang.NullPointerException\n at com.example.myproject.Book.getTitle(Book.java:16)\n at com.example.myproject.Author.getBookTitles(Author.java:25)\n at com.example.myproject.Bootstrap.main(Bootstrap.java:14)\n","media":{"encoding":"utf-8","type":"text/vnd.cucumber.stacktrace.java+plain"}} diff --git a/event-protocol/ruby/.gitignore b/event-protocol/ruby/.gitignore new file mode 100644 index 0000000000..b955b40565 --- /dev/null +++ b/event-protocol/ruby/.gitignore @@ -0,0 +1,10 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +/Gemfile.local diff --git a/event-protocol/ruby/.rspec b/event-protocol/ruby/.rspec new file mode 100644 index 0000000000..8c18f1abdd --- /dev/null +++ b/event-protocol/ruby/.rspec @@ -0,0 +1,2 @@ +--format documentation +--color diff --git a/event-protocol/ruby/.travis.yml b/event-protocol/ruby/.travis.yml new file mode 100644 index 0000000000..29903da90e --- /dev/null +++ b/event-protocol/ruby/.travis.yml @@ -0,0 +1,5 @@ +sudo: false +language: ruby +rvm: + - 2.1.5 +before_install: gem install bundler -v 1.13.1 diff --git a/event-protocol/ruby/Gemfile b/event-protocol/ruby/Gemfile new file mode 100644 index 0000000000..74a23a1e5d --- /dev/null +++ b/event-protocol/ruby/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' +gemspec + +gem 'cucumber', git: 'https://github.com/cucumber/cucumber-ruby.git', branch: 'event-stream-3' diff --git a/event-protocol/ruby/LICENSE.txt b/event-protocol/ruby/LICENSE.txt new file mode 100644 index 0000000000..babf209137 --- /dev/null +++ b/event-protocol/ruby/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Cucumber Limited and contributors + +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/event-protocol/ruby/Makefile b/event-protocol/ruby/Makefile new file mode 100644 index 0000000000..058bffebfb --- /dev/null +++ b/event-protocol/ruby/Makefile @@ -0,0 +1,11 @@ +default: ../validator/yarn.lock Gemfile.lock test + +Gemfile.lock: Gemfile + bundle install + +test: + bundle exec rake +.PHONY: test + +clean: + rm Gemfile.lock diff --git a/event-protocol/ruby/README.md b/event-protocol/ruby/README.md new file mode 100644 index 0000000000..060b136920 --- /dev/null +++ b/event-protocol/ruby/README.md @@ -0,0 +1,25 @@ +# Cucumber EventStream Formatter + +This is a plugin for Cucumber-Ruby that emits the Cucumber [event protocol](https://github.com/cucumber/cucumber/blob/master/event-protocol). + +## Installation + +This gem is not designed to be installed stand-alone. It is a component used within the `cucumber` gem. + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/cucumber/cucumber + +## Code of Conduct + +Everyone interacting in this codebase and issue tracker is expected to follow the Cucumber [code of conduct](https://github.com/cucumber/cucumber/blob/master/CODE_OF_CONDUCT.md). + +## License + +The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). diff --git a/event-protocol/ruby/Rakefile b/event-protocol/ruby/Rakefile new file mode 100644 index 0000000000..b7e9ed549b --- /dev/null +++ b/event-protocol/ruby/Rakefile @@ -0,0 +1,6 @@ +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec diff --git a/event-protocol/ruby/bin/setup b/event-protocol/ruby/bin/setup new file mode 100755 index 0000000000..dce67d860a --- /dev/null +++ b/event-protocol/ruby/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/event-protocol/ruby/cucumber-events.gemspec b/event-protocol/ruby/cucumber-events.gemspec new file mode 100644 index 0000000000..66c44561e1 --- /dev/null +++ b/event-protocol/ruby/cucumber-events.gemspec @@ -0,0 +1,30 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'cucumber/events/version' + +Gem::Specification.new do |spec| + spec.name = "cucumber-events" + spec.version = Cucumber::Events::VERSION + spec.authors = ["Matt Wynne"] + spec.email = ["matt@cucumber.io"] + + spec.summary = %q{Library for events emitted by cucumber-ruby} + spec.description = %q{Streams events that describe your test run in real-time.} + spec.homepage = "https://cucumber.io" + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.13" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency "cucumber", "~> 3.0.0.pre.2" + spec.add_development_dependency "json", "~> 1.8" + spec.add_development_dependency "io-console", "~> 0.4.2" +end diff --git a/event-protocol/ruby/examples/a-failing-scenario/expected-events.ndjson b/event-protocol/ruby/examples/a-failing-scenario/expected-events.ndjson new file mode 100644 index 0000000000..f8b9e2a9cd --- /dev/null +++ b/event-protocol/ruby/examples/a-failing-scenario/expected-events.ndjson @@ -0,0 +1,11 @@ +{"type":"source","uri":"features/failing.feature","data":"Feature: Failing feature\n Scenario: failling\n Given this step fails\n Then this step will be skipped\n","media":{"encoding":"utf-8","type":"text/vnd.cucumber.gherkin+plain"}} +{"type":"test-run-started","workingDirectory":"{{Dir.pwd}}/examples/a-failing-scenario","timestamp":1490450321} +{"type":"test-case-prepared","sourceLocation":{"uri":"features/failing.feature","line":2},"steps":[{"actionLocation":{"uri":"{{Cucumber::LIBDIR}}/cucumber/filters/prepare_world.rb","line":28}},{"actionLocation":{"uri":"step_definitions/steps.rb","line":1},"sourceLocation":{"uri":"features/failing.feature","line":3}},{"actionLocation":{"uri":"step_definitions/steps.rb","line":5},"sourceLocation":{"uri":"features/failing.feature","line":4}}]} +{"type":"test-case-started","sourceLocation":{"uri":"features/failing.feature","line":2}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/failing.feature","line":2}},"index":0} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/failing.feature","line":2}},"index":0,"result":{"status":"passed","duration":3000}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/failing.feature","line":2}},"index":1} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/failing.feature","line":2}},"index":1,"result":{"status":"failed","duration":140000,"exception":{"message":"Failing step","type":"RuntimeError","stackTrace":["{{Dir.pwd}}/examples/a-failing-scenario/step_definitions/steps.rb:2:in `/^this step fails$/'","features/failing.feature:3:in `Given this step fails'"]}}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/failing.feature","line":2}},"index":2} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/failing.feature","line":2}},"index":2,"result":{"status":"skipped","exception":{"message":"","type":"Cucumber::Core::Test::Result::Skipped","stackTrace":[]}}} +{"type":"test-case-finished","sourceLocation":{"uri":"features/failing.feature","line":2},"result":{"status":"failed","duration":12344000,"exception":{"message":"Failing step","type":"RuntimeError","stackTrace":["{{Dir.pwd}}/examples/a-failing-scenario/step_definitions/steps.rb:2:in `/^this step fails$/'","features/failing.feature:3:in `Given this step fails'"]}}} diff --git a/event-protocol/ruby/examples/a-failing-scenario/features/failing.feature b/event-protocol/ruby/examples/a-failing-scenario/features/failing.feature new file mode 100644 index 0000000000..e4f4486bf1 --- /dev/null +++ b/event-protocol/ruby/examples/a-failing-scenario/features/failing.feature @@ -0,0 +1,4 @@ +Feature: Failing feature + Scenario: failling + Given this step fails + Then this step will be skipped diff --git a/event-protocol/ruby/examples/a-failing-scenario/step_definitions/steps.rb b/event-protocol/ruby/examples/a-failing-scenario/step_definitions/steps.rb new file mode 100644 index 0000000000..64fd8ee115 --- /dev/null +++ b/event-protocol/ruby/examples/a-failing-scenario/step_definitions/steps.rb @@ -0,0 +1,7 @@ +Given(/^this step fails$/) do + raise "Failing step" +end + +Then(/^this step will be skipped$/) do + # noop +end diff --git a/event-protocol/ruby/examples/a-single-passing-scenario/expected-events.ndjson b/event-protocol/ruby/examples/a-single-passing-scenario/expected-events.ndjson new file mode 100644 index 0000000000..b09941823b --- /dev/null +++ b/event-protocol/ruby/examples/a-single-passing-scenario/expected-events.ndjson @@ -0,0 +1,9 @@ +{"type":"source","uri":"features/passing.feature","data":"Feature: Passing feature\n Scenario: Passing scenario\n Given this step passes\n","media":{"encoding":"utf-8","type":"text/vnd.cucumber.gherkin+plain"}} +{"type":"test-run-started","workingDirectory":"{{Dir.pwd}}/examples/a-single-passing-scenario","timestamp":1490444711} +{"type":"test-case-prepared","sourceLocation":{"uri":"features/passing.feature","line":2},"steps":[{"actionLocation":{"uri":"{{Cucumber::LIBDIR}}/cucumber/filters/prepare_world.rb","line":28}},{"actionLocation":{"uri":"step_definitions/steps.rb","line":1},"sourceLocation":{"uri":"features/passing.feature","line":3}}]} +{"type":"test-case-started","sourceLocation":{"uri":"features/passing.feature","line":2}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/passing.feature","line":2}},"index":0} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/passing.feature","line":2}},"index":0,"result":{"status":"passed","duration":3000}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/passing.feature","line":2}},"index":1} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/passing.feature","line":2}},"index":1,"result":{"status":"passed","duration":381000}} +{"type":"test-case-finished","sourceLocation":{"uri":"features/passing.feature","line":2},"result":{"status":"passed","duration":24008000}} diff --git a/event-protocol/ruby/examples/a-single-passing-scenario/features/passing.feature b/event-protocol/ruby/examples/a-single-passing-scenario/features/passing.feature new file mode 100644 index 0000000000..7e6b79f571 --- /dev/null +++ b/event-protocol/ruby/examples/a-single-passing-scenario/features/passing.feature @@ -0,0 +1,3 @@ +Feature: Passing feature + Scenario: Passing scenario + Given this step passes diff --git a/event-protocol/ruby/examples/a-single-passing-scenario/step_definitions/steps.rb b/event-protocol/ruby/examples/a-single-passing-scenario/step_definitions/steps.rb new file mode 100644 index 0000000000..57a3312f6e --- /dev/null +++ b/event-protocol/ruby/examples/a-single-passing-scenario/step_definitions/steps.rb @@ -0,0 +1,3 @@ +Given(/^this step passes$/) do + # noop +end diff --git a/event-protocol/ruby/examples/an-undefined-step/expected-events.ndjson b/event-protocol/ruby/examples/an-undefined-step/expected-events.ndjson new file mode 100644 index 0000000000..2e22e82e0b --- /dev/null +++ b/event-protocol/ruby/examples/an-undefined-step/expected-events.ndjson @@ -0,0 +1,9 @@ +{"type":"source","uri":"features/undefined.feature","data":"Feature: Undefined feature\n Scenario: Undefined scenario\n Given this step is undefined\n","media":{"encoding":"utf-8","type":"text/vnd.cucumber.gherkin+plain"}} +{"type":"test-run-started","workingDirectory":"{{Dir.pwd}}/examples/an-undefined-step","timestamp":1490444711} +{"type":"test-case-prepared","sourceLocation":{"uri":"features/undefined.feature","line":2},"steps":[{"actionLocation":{"uri":"{{Cucumber::LIBDIR}}/cucumber/filters/prepare_world.rb","line":28}},{"sourceLocation":{"uri":"features/undefined.feature","line":3}}]} +{"type":"test-case-started","sourceLocation":{"uri":"features/undefined.feature","line":2}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/undefined.feature","line":2}},"index":0} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/undefined.feature","line":2}},"index":0,"result":{"status":"passed","duration":3000}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/undefined.feature","line":2}},"index":1} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/undefined.feature","line":2}},"index":1,"result":{"status":"undefined","exception":{"message":"Undefined step: \"this step is undefined\"","type":"Cucumber::Core::Test::Result::Undefined","stackTrace":["features/undefined.feature:3:in `Given this step is undefined'"]}}} +{"type":"test-case-finished","sourceLocation":{"uri":"features/undefined.feature","line":2},"result":{"status":"undefined","duration":12811000,"exception":{"message":"Undefined step: \"this step is undefined\"","type":"Cucumber::Core::Test::Result::Undefined","stackTrace":["features/undefined.feature:3:in `Given this step is undefined'"]}}} diff --git a/event-protocol/ruby/examples/an-undefined-step/features/undefined.feature b/event-protocol/ruby/examples/an-undefined-step/features/undefined.feature new file mode 100644 index 0000000000..68427357e2 --- /dev/null +++ b/event-protocol/ruby/examples/an-undefined-step/features/undefined.feature @@ -0,0 +1,3 @@ +Feature: Undefined feature + Scenario: Undefined scenario + Given this step is undefined diff --git a/event-protocol/ruby/examples/readme.md b/event-protocol/ruby/examples/readme.md new file mode 100644 index 0000000000..e7f7cebad2 --- /dev/null +++ b/event-protocol/ruby/examples/readme.md @@ -0,0 +1,11 @@ +Each of these examples isolates a simple case of using Cucumber with the events plugin, showing +the expected events output in the `expected-events.ndjson`. + +These are used in the acceptance tests in `spec/cucumber/formatter/event_stream/event_stream_spec.rb` and the real output from running cucumber is comapred with this expected output. +To run a test manually, `cd` into the directory of the example, then run: + +``` +bundle exec cucumber -r . --format Cucumber::Formatter::EventStream::Plugin +``` + +Note that a certain amount of normalisation (for timestamps and directory paths) is neccesary before comparing the output for testing. diff --git a/event-protocol/ruby/examples/scenario-outline/expected-events.ndjson b/event-protocol/ruby/examples/scenario-outline/expected-events.ndjson new file mode 100644 index 0000000000..dd0c7f58e3 --- /dev/null +++ b/event-protocol/ruby/examples/scenario-outline/expected-events.ndjson @@ -0,0 +1,9 @@ +{"type":"source","uri":"features/passing_outline.feature","data":"Feature:\n Scenario Outline:\n Given this step is \n\n Examples:\n | status |\n | passing |\n","media":{"encoding":"utf-8","type":"text/vnd.cucumber.gherkin+plain"}} +{"type":"test-run-started","workingDirectory":"{{Dir.pwd}}/examples/scenario-outline","timestamp":1494880725} +{"type":"test-case-prepared","sourceLocation":{"uri":"features/passing_outline.feature","line":7},"steps":[{"actionLocation":{"uri":"{{Cucumber::LIBDIR}}/cucumber/filters/prepare_world.rb","line":28}},{"actionLocation":{"uri":"features/step_definitions/steps.rb","line":1},"sourceLocation":{"uri":"features/passing_outline.feature","line":7}}]} +{"type":"test-case-started","sourceLocation":{"uri":"features/passing_outline.feature","line":7}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/passing_outline.feature","line":7}},"index":0} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/passing_outline.feature","line":7}},"index":0,"result":{"status":"passed","duration":5000}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/passing_outline.feature","line":7}},"index":1} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/passing_outline.feature","line":7}},"index":1,"result":{"status":"passed","duration":200000}} +{"type":"test-case-finished","sourceLocation":{"uri":"features/passing_outline.feature","line":7},"result":{"status":"passed","duration":38645000}} diff --git a/event-protocol/ruby/examples/scenario-outline/features/passing_outline.feature b/event-protocol/ruby/examples/scenario-outline/features/passing_outline.feature new file mode 100644 index 0000000000..0bd262899a --- /dev/null +++ b/event-protocol/ruby/examples/scenario-outline/features/passing_outline.feature @@ -0,0 +1,7 @@ +Feature: + Scenario Outline: + Given this step is + + Examples: + | status | + | passing | diff --git a/event-protocol/ruby/examples/scenario-outline/features/step_definitions/steps.rb b/event-protocol/ruby/examples/scenario-outline/features/step_definitions/steps.rb new file mode 100644 index 0000000000..c45ff3fb7d --- /dev/null +++ b/event-protocol/ruby/examples/scenario-outline/features/step_definitions/steps.rb @@ -0,0 +1,3 @@ +Given(/pass/) do + # noop +end diff --git a/event-protocol/ruby/lib/cucumber/events/plugin.rb b/event-protocol/ruby/lib/cucumber/events/plugin.rb new file mode 100644 index 0000000000..16c62fa45f --- /dev/null +++ b/event-protocol/ruby/lib/cucumber/events/plugin.rb @@ -0,0 +1,138 @@ +require 'socket' + +module Cucumber + module Events + + class Plugin + def initialize(config, options) + io = config.out_stream + + EventEmitter.new(config).call do |event| + io.puts event.to_json + end + end + end + + class EventEmitter + attr_reader :config + + def initialize(config) + @config = config + @working_dir = Pathname.new(Dir.pwd) + end + + def call + current_test_case = nil + + config.on_event :gherkin_source_read, -> (event) { + yield \ + type: "source", + uri: uri(event.path), + data: event.body, + media: { + encoding: 'utf-8', + type: 'text/vnd.cucumber.gherkin+plain' + } + } + + config.on_event :test_run_starting, -> (event) { + yield \ + type: "test-run-started", + workingDirectory: working_dir, + timestamp: Time.now.to_i + + event.test_cases.each { |test_case| + yield \ + type: "test-case-prepared", + sourceLocation: location_to_json(test_case.location), + steps: test_case.test_steps.map { |test_step| + test_step_to_json(test_case, test_step) + } + } + } + + config.on_event :test_case_starting, -> (event) { + current_test_case = event.test_case # TODO: add this to the core step events so we don't have to cache it here + yield \ + type: "test-case-started", + sourceLocation: location_to_json(event.test_case.location) + } + + config.on_event :test_step_starting, -> (event) { + yield \ + type: "test-step-started", + testCase: { sourceLocation: location_to_json(current_test_case.location) }, + index: current_test_case.test_steps.index(event.test_step) + } + + config.on_event :test_step_finished, -> (event) { + yield \ + type: "test-step-finished", + testCase: { sourceLocation: location_to_json(current_test_case.location) }, + index: current_test_case.test_steps.index(event.test_step), + result: result_to_json(event.result) + } + + config.on_event :test_case_finished, -> (event) { + yield \ + type: "test-case-finished", + sourceLocation: location_to_json(event.test_case.location), + result: result_to_json(event.result) + } + + end + + private + + attr_reader :working_dir + + def result_to_json(result) + data = { status: result.to_sym.to_s } + result.duration.tap do |duration| + data["duration"] = duration.nanoseconds + end + if result.respond_to?(:exception) + data[:exception] = { + message: result.exception.message, + type: result.exception.class, + stackTrace: result.exception.backtrace || [] + } + end + data + end + + def test_step_to_json(test_case, test_step) + result = {} + unless undefined?(test_step) + result['actionLocation'] = location_to_json(test_step.action_location) + end + unless hook?(test_step) + result['sourceLocation'] = location_to_json(test_step.source.last.location) + end + result + end + + def undefined?(test_step) + test_step.action_location.file.match(/\.feature$/) + end + + def hook?(test_step) + test_step.action_location.file.match(/\.rb$/) && + test_step.source.last.location == test_step.action_location + end + + def location_to_json(location) + { + uri: uri(location.file), + line: location.line + } + end + + def uri(path) + Pathname.new(File.expand_path(path)).relative_path_from(working_dir) + end + end + + end +end + diff --git a/event-protocol/ruby/lib/cucumber/events/version.rb b/event-protocol/ruby/lib/cucumber/events/version.rb new file mode 100644 index 0000000000..a236696e87 --- /dev/null +++ b/event-protocol/ruby/lib/cucumber/events/version.rb @@ -0,0 +1,5 @@ +module Cucumber + module Events + VERSION = "0.1.0" + end +end diff --git a/event-protocol/ruby/spec/cucumber/events/plugin_acceptance_spec.rb b/event-protocol/ruby/spec/cucumber/events/plugin_acceptance_spec.rb new file mode 100644 index 0000000000..eb1720857e --- /dev/null +++ b/event-protocol/ruby/spec/cucumber/events/plugin_acceptance_spec.rb @@ -0,0 +1,104 @@ +require "json" +require "open3" +require "cucumber/platform" +require "cucumber/core/ast/location" + +cucumber = "bundle exec cucumber -r ../../lib --format Cucumber::Events::Plugin" +validator = "../validator/bin/cucumber-event-validator.js" + +## Acceptance Tests +# +# These tests work through each of the examples, running the command above, +# and comparing the actual output to that found in `expected-events.ndjson` +# +# Because some of the values in the output are timing-related, we normalize +# those values using a helper function: +# + +normalize_event = lambda do |event, index| + case event["type"] + when "test-run-started" + event["timestamp"] = "1000#{index}".to_i + when "test-step-finished", "test-case-finished" + event["result"]["duration"] = "10#{index}".to_i + when "test-case-prepared" + event["steps"].each_with_index do |step, index| + if step["sourceLocation"] + step["sourceLocation"]["uri"] = Cucumber::Core::Ast::Location.from_source_location( + step["sourceLocation"]["uri"], + step["sourceLocation"]["line"] + ).file + end + if step["actionLocation"] + step["actionLocation"]["uri"] = Cucumber::Core::Ast::Location.from_source_location( + step["actionLocation"]["uri"], + step["actionLocation"]["line"] + ).file + end + event["steps"][index] = step + end + end + event +end + +# Because some of the output depends on the specific environment where the tests +# are run, we use a lightweight mustache template to dynamically build the right +# expected output: + +# Expand templates for environment-specific values in expected events +mustache = /{{([^}]+)}}/ +normalize_line = lambda do |line| + line.gsub(mustache) { |rb| eval(rb.match(mustache)[1]) } +end + +# Now we dynamically build a set of RSpec example groups for each of the examples, +# and an RSpec example for each of the expected events + +describe "cucumber-events" do + describe "examples" do + + path = File.dirname(__FILE__) + "/../../../examples/*" + examples = Dir[path]. + map { |path| File.expand_path(path) }. + select { |path| File.directory?(path) } + + examples.each do |example_dir| + + describe File.basename(example_dir).gsub("-", " ") do + + expected = File.read(example_dir + "/expected-events.ndjson"). + lines. + map { |line| normalize_line[line] }. + map { |line| JSON.parse(line) }. + map { |event, index| normalize_event[event, index] } + + actual = Dir.chdir(example_dir) { `#{cucumber}` }. + lines. + map { |line| JSON.parse(line) }. + map { |event, index| normalize_event[event, index] } + + it "emits #{expected.length} event(s) as expected" do + expect(actual.map { |event| event["type"] }).to eq expected.map { |event| event["type"] } + end + + expected.each_with_index do |expected_event, index| + it "emits a `#{expected_event["type"]}` event as expected" do + expect(actual[index]).to eq(expected_event) + end + + it "emits a valid event" do + Open3.popen3(validator) do |stdin, stdout, stderr| + input = actual[index].to_json + stdin.puts input + stdin.close + errors = stderr.read + expect(errors).to be_empty, input + "\n" + errors + end + end + end + + end + + end + end +end diff --git a/event-protocol/ruby/spec/cucumber/events/plugin_spec.rb b/event-protocol/ruby/spec/cucumber/events/plugin_spec.rb new file mode 100644 index 0000000000..3240bc833a --- /dev/null +++ b/event-protocol/ruby/spec/cucumber/events/plugin_spec.rb @@ -0,0 +1,143 @@ +require 'cucumber/events/plugin' +require 'cucumber/configuration' +require 'cucumber/core/test/case' +require 'cucumber/hooks' +require 'cucumber/core/gherkin/document' + +describe Cucumber::Events::Plugin do + describe "when the test run starts" do + + it "emits one `test-case-prepared` event per test case" do + source = [ + source('features/test.feature:3'), + source('features/test.feature:1') + ] + test_steps = [ + Cucumber::Core::Test::Step.new( + [ source('features/test.feature:4') ] + ).with_action( + location('features/step_definitions/steps.rb:1') + ) { + # passing step + } + ] + output = run_test_cases([ Cucumber::Core::Test::Case.new(test_steps, source) ]) + expect(output.count { |message| message['type'] == 'test-case-prepared' }).to be 1 + end + end + + describe "test-case-prepared event" do + it "emits only the actionLocation for a hook" do + source = [ + source('features/test.feature:3'), + source('features/test.feature:1') + ] + test_steps = [ + Cucumber::Hooks.before_hook( + source, + location('features/support/hooks.rb:1') + ) { + # passing hook + }, + ] + + output = run_test_cases([ Cucumber::Core::Test::Case.new(test_steps, source) ]) + message = output.find { |message| message['type'] == 'test-case-prepared' } + expect(message['steps'][0].keys).to_not include('sourceLocation') + expect(message['steps'][0]['actionLocation']).to eq({ 'uri' => 'features/support/hooks.rb', 'line' => 1 }) + end + + it "emits only the sourceLocation for an undefined Gherkin step" do + source = [ + source('features/test.feature:3'), + source('features/test.feature:1') + ] + test_steps = [ + Cucumber::Core::Test::Step.new( + [ source('features/test.feature:4') ] + ) + ] + output = run_test_cases([ Cucumber::Core::Test::Case.new(test_steps, source) ]) + message = output.find { |message| message['type'] == 'test-case-prepared' } + expect(message['steps'][0].keys).to_not include('actionLocation') + expect(message['steps'][0]['sourceLocation']).to eq({ 'uri' => 'features/test.feature', 'line' => 4 }) + end + + it "emits both actionLocation and sourceLocation for a Gherkin step with an action" do + source = [ + source('features/test.feature:3'), + source('features/test.feature:1') + ] + test_steps = [ + Cucumber::Core::Test::Step.new( + [ source('features/test.feature:4') ] + ).with_action( + location('features/step_definitions/steps.rb:1') + ) { + # passing step + } + ] + output = run_test_cases([ Cucumber::Core::Test::Case.new(test_steps, source) ]) + message = output.find { |message| message['type'] == 'test-case-prepared' } + expect(message['steps'][0]['actionLocation']).to eq({ 'uri' => 'features/step_definitions/steps.rb', 'line' => 1 }) + expect(message['steps'][0]['sourceLocation']).to eq({ 'uri' => 'features/test.feature', 'line' => 4 }) + end + + it "emits a source location with two lines for a Gherkin Scenario Outline step" do + test_cases = compile('features/test.feature', %{Feature: + Scenario Outline: + Given this step is + + Examples: + | status | + | passing | + }) + + passing_test_case = test_cases[0]. + with_steps(test_cases[0].test_steps.map { |test_step| test_step.with_action { } }) + output = run_test_cases([ passing_test_case ]) + message = output.find { |message| message['type'] == 'test-case-prepared' } + expect(message['steps'][0]['sourceLocation']).to eq({ 'uri' => 'features/test.feature', 'lines' => [ 7, 3 ]}) + end + + def compile(file, gherkin) + core = Object.new.extend(Cucumber::Core) + receiver = Receiver.new + core.compile([Cucumber::Core::Gherkin::Document.new(file, gherkin)], receiver) + receiver.test_cases + end + + class Receiver + attr_reader :test_cases + + def initialize + @test_cases = [] + end + + def test_case(test_case) + @test_cases << test_case + end + + def done + end + end + end + + def location(file_colon_line) + Cucumber::Core::Ast::Location.from_file_colon_line(file_colon_line) + end + + def source(file_colon_line) + double(location: location(file_colon_line)) + end + + def run_test_cases(test_cases) + io = StringIO.new + config = Cucumber::Configuration.new(out_stream: io) + options = {} + plugin = Cucumber::Events::Plugin.new(config, options) + config.notify :test_run_starting, test_cases + io.string.lines.map { |line| JSON.parse(line) } + end + +end diff --git a/event-protocol/.gitignore b/event-protocol/validator/.gitignore similarity index 100% rename from event-protocol/.gitignore rename to event-protocol/validator/.gitignore diff --git a/event-protocol/validator/Makefile b/event-protocol/validator/Makefile new file mode 100644 index 0000000000..cd384ba9c0 --- /dev/null +++ b/event-protocol/validator/Makefile @@ -0,0 +1,25 @@ +EXAMPLES = $(wildcard examples/events/*.json) +SCHEMAS = $(wildcard schemas/*.json) +SRC = $(wildcard bin/* lib/*) + +default: .valid + yarn link +.PHONY: default + +.built: yarn.lock $(SRC) + yarn test + touch $@ + +yarn.lock: package.json + yarn install + +clean: + rm -rf yarn.lock node_modules coverage dist .built .valid examples/events.ndjson +.PHONY: clean + +.valid: examples/events.ndjson yarn.lock ./bin/cucumber-event-validator.js $(SCHEMAS) $(SRC) + @cat $< | node ./bin/cucumber-event-validator.js + @touch $@ + +examples/events.ndjson: $(EXAMPLES) + @cat $(EXAMPLES) | jq --compact-output "." > $@ diff --git a/event-protocol/README.md b/event-protocol/validator/README.md similarity index 74% rename from event-protocol/README.md rename to event-protocol/validator/README.md index 9bd64f99e4..17fc7fe289 100644 --- a/event-protocol/README.md +++ b/event-protocol/validator/README.md @@ -85,6 +85,74 @@ Example (Java stack trace): [snippet](examples/events/004_attachment-stacktrace.json) ``` +### test-run-started {#test-run-started} + +Signals the start of a test run. Contains details of the context of the run, like the working directory, start time etc. + +Example: + +```json +[snippet](examples/events/005_test-run-started.json) +``` + +### test-case-prepared {#test-case-prepared} + +Contains the details of a test case that's ready to be executed. The `sourceLocation` should match the `uri` of preceeding `source` and `gherkin-document` events. + +Each `step` has an `actionLocation` pointing to the source of the glue code that will be invoked when the step is executed. The `sourceLocation` points to the source of the step in a Gherkin document. + +Example: + +```json +[snippet](examples/events/006_test-case-prepared.json) +``` + +### test-case-started + +Signals the start of executing a test case + +Example: + +```json +[snippet](examples/events/007_test-case-started.json) +``` + +### test-step-started + +Signals the start of executing a test step within a test case. The `index` locates the step within the array of steps sent in the preceeding `test-case-prepared` event. + +Example: + +```json +[snippet](examples/events/008_test-step-started.json) +``` + +### test-step-finished + +Signals the end of executing a test step. The result + +Example of a passing step: + +```json +[snippet](examples/events/009_test-step-finished.json) +``` + +Example of failing step: + +```json +[snippet](examples/events/011_test-step-finished.json) +``` + +Note that undefined is just a special case of failure. The `result` will always have an `exception` when the step has not passed. + +### test-case-finished + +Signals the end of executing a whole test case (scenario or example row). + +```json +[snippet](examples/events/012_test-case-finished.json) +``` + ## Implementation * Cucumber events are formatted as [Newline Delimited JSON](http://ndjson.org) @@ -128,7 +196,9 @@ We'll manage this by adding a version to a specific event, where needed. For now There are a few constraints about the order of events: * A [source](#event-source) event must be received before any - [attachment](#event-attachment) events referring to it + [attachment](#event-attachment), `test-case-*` or `test-step-*` events referring to it +* [test-case-started](#test-case-started), [test-step-started](#test-step-started), [test-step-finished](#test-step-finished), [test-case-finished](#test-case-finished) events are expected to be received in that order. They should be preceeded by the relevant [test-case-prepared](#test-case-prepared) event describing the test case and its steps. +* `test-*-started` events are optional ## Event validation {#validation} diff --git a/event-protocol/bin/cucumber-event-validator.js b/event-protocol/validator/bin/cucumber-event-validator.js similarity index 100% rename from event-protocol/bin/cucumber-event-validator.js rename to event-protocol/validator/bin/cucumber-event-validator.js diff --git a/event-protocol/validator/examples/events.ndjson b/event-protocol/validator/examples/events.ndjson new file mode 100644 index 0000000000..f8fc3c6d3a --- /dev/null +++ b/event-protocol/validator/examples/events.ndjson @@ -0,0 +1,13 @@ +{"data":"Feature: Hello\n Scenario: World\n Given a step\n\n","media":{"encoding":"utf-8","type":"text/vnd.cucumber.gherkin+plain"},"type":"source","uri":"features/hello.feature"} +{"document":{"comments":[],"feature":{"children":[{"keyword":"Scenario","location":{"column":3,"line":2},"name":"World","steps":[{"keyword":"Given ","location":{"column":5,"line":3},"text":"a step","type":"Step"}],"tags":[],"type":"Scenario"}],"keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Hello","tags":[],"type":"Feature"},"type":"GherkinDocument"},"type":"gherkin-document","uri":"features/hello.feature"} +{"pickle":{"language":"en","locations":[{"column":3,"line":2}],"name":"World","steps":[{"arguments":[],"locations":[{"column":11,"line":3}],"text":"a step"}],"tags":[]},"type":"pickle","uri":"features/hello.feature"} +{"type":"attachment","source":{"uri":"features/hello.feature","start":{"line":3,"column":7}},"data":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAELUExURUdwTACoGAClFACqHACoFwCnGQCnFQCnGACoGQCoGQCoGACuGgCnFwCiFwCoGACoGACnFwClFgCpGACoFwD/AACnGAC2JACoGACnFwCqKgCoGACoGACqGQCoFwCnFwCoFwB/AACnGACoGAC/AACoGACoFgCmFwCnFwCnFwCqFgCmFgCZAACoGC64Ql3IbA2sJBuxMBiwLhewLR2yMgmrIOn36/z+/BGuJ9/04rHkuIDUjGHJcA6tJeD04wGoGQusIpnco+T257PlugSpHN704XDOfQeqHiCzNc7u0+z57t3z4HvSh7fmvj69UJzdpSi2PNjy3AKpGlrHahqxMBCtJpHZm83u0jS6R0vCXEWczDEAAAAsdFJOUwDIJQlYPSP7cFCqE1cW+mf4ImiBAXQHPu8GfP0zzoyOAp2wBOg4a2PqOU4FxgPjdgAAAK9JREFUGNNNj1UCwkAMRBcotLi7uw1S3N3d4f4nYSvIfO28bCYJIaI0OSabJ1+pFaDKpD4+zUJS1CHVWSwer+esDNhEQP+fV/f5uAd4XEIesNldl5WK0KSlQAtcDkc5xUcBA+y3tL/UKQIhCpLA+sTzw1K7UQVHQTyB23QyKraaNSAgTOGAQb9bqBeAoDg2EpMT4XdLm3rD8qrO7zFWO7UW8+86oodJp/zzRGUwyq83Mjcb8VXl0ZMAAAAASUVORK5CYII=","media":{"encoding":"base64","type":"image/png"}} +{"type":"attachment","source":{"uri":"features/hello.feature","start":{"line":3,"column":7}},"data":"Exception in thread \"main\" java.lang.NullPointerException\n at com.example.myproject.Book.getTitle(Book.java:16)\n at com.example.myproject.Author.getBookTitles(Author.java:25)\n at com.example.myproject.Bootstrap.main(Bootstrap.java:14)\n","media":{"encoding":"utf-8","type":"text/vnd.cucumber.stacktrace.java+plain"}} +{"type":"test-run-started","workingDirectory":"/Users/matt/projects/cucumber-ruby","timestamp":1489683458,"vcs":{"git":{"head":"be6683b9004a5785ca9b51776b98694dfe406fb6","diff":"diff --git a/event-protocol/schemas/test-run-started.json b/event-protocol/schemas/test-run-started.json\nindex 6fa0e39..48efe9a 100644\n--- a/event-protocol/schemas/test-run-started.json\n+++ b/event-protocol/schemas/test-run-started.json\n@@ -12,7 +12,27 @@\n },\n \"timestamp\": {\n \"type\": \"number\"\n+ },\n+ \"vcs\": {\n+ \"type\": \"object\",\n+ \"properties\": {\n+ \"git\": {\n+ \"type\": \"object\",\n+ \"properties\": {\n+ \"head\": {\n+ \"type\": \"string\"\n+ },\n+ \"diff\": {\n+ \"type\": \"string\"\n+ }\n+ }\n+ }\n+ }\n }\n },\n+ \"required\": [\n+ \"type\",\n+ \"timestamp\"\n+ ],\n \"additionalProperties\": false\n }\""}}} +{"type":"test-case-prepared","sourceLocation":{"uri":"features/passing.feature","line":2},"steps":[{"actionLocation":{"uri":"/Users/matt/projects/cucumber-ruby/lib/cucumber/filters/prepare_world.rb","line":28},"sourceLocation":{"uri":"/Users/matt/projects/cucumber-ruby/lib/cucumber/filters/prepare_world.rb","line":28}},{"actionLocation":{"uri":"features/passing.feature","line":3},"sourceLocation":{"uri":"features/passing.feature","line":3}}]} +{"type":"test-case-started","sourceLocation":{"uri":"features/passing.feature","line":2}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/passing.feature","line":2}},"index":0} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/passing.feature","line":2}},"index":0,"result":{"status":"passed","duration":3000}} +{"type":"test-step-started","testCase":{"sourceLocation":{"uri":"features/passing.feature","line":2}},"index":1} +{"type":"test-step-finished","testCase":{"sourceLocation":{"uri":"features/passing.feature","line":2}},"index":1,"result":{"status":"undefined","exception":{"message":"Undefined step: \"this step passes\"","type":"Cucumber::Core::Test::Result::Undefined","stackTrace":["features/passing.feature:3:in `Given this step passes'"]}}} +{"type":"test-case-finished","sourceLocation":{"uri":"features/passing.feature","line":2},"result":{"status":"undefined","duration":13163000,"exception":{"message":"Undefined step: \"this step passes\"","type":"Cucumber::Core::Test::Result::Undefined","stackTrace":["features/passing.feature:3:in `Given this step passes'"]}}} diff --git a/event-protocol/examples/events/001_source.json b/event-protocol/validator/examples/events/001_source.json similarity index 100% rename from event-protocol/examples/events/001_source.json rename to event-protocol/validator/examples/events/001_source.json diff --git a/event-protocol/examples/events/002_gherkin-document.json b/event-protocol/validator/examples/events/002_gherkin-document.json similarity index 100% rename from event-protocol/examples/events/002_gherkin-document.json rename to event-protocol/validator/examples/events/002_gherkin-document.json diff --git a/event-protocol/examples/events/003_pickle.json b/event-protocol/validator/examples/events/003_pickle.json similarity index 100% rename from event-protocol/examples/events/003_pickle.json rename to event-protocol/validator/examples/events/003_pickle.json diff --git a/event-protocol/examples/events/004_attachment-png-embedded.json b/event-protocol/validator/examples/events/004_attachment-png-embedded.json similarity index 100% rename from event-protocol/examples/events/004_attachment-png-embedded.json rename to event-protocol/validator/examples/events/004_attachment-png-embedded.json diff --git a/event-protocol/examples/events/004_attachment-stacktrace.json b/event-protocol/validator/examples/events/004_attachment-stacktrace.json similarity index 100% rename from event-protocol/examples/events/004_attachment-stacktrace.json rename to event-protocol/validator/examples/events/004_attachment-stacktrace.json diff --git a/event-protocol/validator/examples/events/005_test-run-started.json b/event-protocol/validator/examples/events/005_test-run-started.json new file mode 100644 index 0000000000..e1b5681a99 --- /dev/null +++ b/event-protocol/validator/examples/events/005_test-run-started.json @@ -0,0 +1,11 @@ +{ + "type":"test-run-started", + "workingDirectory":"/Users/matt/projects/cucumber-ruby", + "timestamp":1489683458, + "vcs": { + "git": { + "head": "be6683b9004a5785ca9b51776b98694dfe406fb6", + "diff": "diff --git a/event-protocol/schemas/test-run-started.json b/event-protocol/schemas/test-run-started.json\nindex 6fa0e39..48efe9a 100644\n--- a/event-protocol/schemas/test-run-started.json\n+++ b/event-protocol/schemas/test-run-started.json\n@@ -12,7 +12,27 @@\n },\n \"timestamp\": {\n \"type\": \"number\"\n+ },\n+ \"vcs\": {\n+ \"type\": \"object\",\n+ \"properties\": {\n+ \"git\": {\n+ \"type\": \"object\",\n+ \"properties\": {\n+ \"head\": {\n+ \"type\": \"string\"\n+ },\n+ \"diff\": {\n+ \"type\": \"string\"\n+ }\n+ }\n+ }\n+ }\n }\n },\n+ \"required\": [\n+ \"type\",\n+ \"timestamp\"\n+ ],\n \"additionalProperties\": false\n }\"" + } + } +} diff --git a/event-protocol/validator/examples/events/006_test-case-prepared.json b/event-protocol/validator/examples/events/006_test-case-prepared.json new file mode 100644 index 0000000000..75b0b7fe80 --- /dev/null +++ b/event-protocol/validator/examples/events/006_test-case-prepared.json @@ -0,0 +1,29 @@ +{ + "type": "test-case-prepared", + "sourceLocation": { + "uri": "features/passing.feature", + "line": 2 + }, + "steps": [ + { + "actionLocation": { + "uri": "/Users/matt/projects/cucumber-ruby/lib/cucumber/filters/prepare_world.rb", + "line": 28 + }, + "sourceLocation": { + "uri": "/Users/matt/projects/cucumber-ruby/lib/cucumber/filters/prepare_world.rb", + "line": 28 + } + }, + { + "actionLocation": { + "uri": "features/passing.feature", + "line": 3 + }, + "sourceLocation": { + "uri": "features/passing.feature", + "line": 3 + } + } + ] +} diff --git a/event-protocol/validator/examples/events/007_test-case-started.json b/event-protocol/validator/examples/events/007_test-case-started.json new file mode 100644 index 0000000000..88ced740b8 --- /dev/null +++ b/event-protocol/validator/examples/events/007_test-case-started.json @@ -0,0 +1,7 @@ +{ + "type": "test-case-started", + "sourceLocation": { + "uri": "features/passing.feature", + "line": 2 + } +} diff --git a/event-protocol/validator/examples/events/008_test-step-started.json b/event-protocol/validator/examples/events/008_test-step-started.json new file mode 100644 index 0000000000..e68b88c3fb --- /dev/null +++ b/event-protocol/validator/examples/events/008_test-step-started.json @@ -0,0 +1,10 @@ +{ + "type": "test-step-started", + "testCase": { + "sourceLocation": { + "uri": "features/passing.feature", + "line": 2 + } + }, + "index": 0 +} diff --git a/event-protocol/validator/examples/events/009_test-step-finished.json b/event-protocol/validator/examples/events/009_test-step-finished.json new file mode 100644 index 0000000000..1084ee8097 --- /dev/null +++ b/event-protocol/validator/examples/events/009_test-step-finished.json @@ -0,0 +1,14 @@ +{ + "type": "test-step-finished", + "testCase": { + "sourceLocation": { + "uri": "features/passing.feature", + "line": 2 + } + }, + "index": 0, + "result": { + "status": "passed", + "duration": 3000 + } +} diff --git a/event-protocol/validator/examples/events/010_test-step-started.json b/event-protocol/validator/examples/events/010_test-step-started.json new file mode 100644 index 0000000000..6d4069f101 --- /dev/null +++ b/event-protocol/validator/examples/events/010_test-step-started.json @@ -0,0 +1,10 @@ +{ + "type": "test-step-started", + "testCase": { + "sourceLocation": { + "uri": "features/passing.feature", + "line": 2 + } + }, + "index": 1 +} diff --git a/event-protocol/validator/examples/events/011_test-step-finished.json b/event-protocol/validator/examples/events/011_test-step-finished.json new file mode 100644 index 0000000000..b6554f09b7 --- /dev/null +++ b/event-protocol/validator/examples/events/011_test-step-finished.json @@ -0,0 +1,20 @@ +{ + "type": "test-step-finished", + "testCase": { + "sourceLocation": { + "uri": "features/passing.feature", + "line": 2 + } + }, + "index": 1, + "result": { + "status": "undefined", + "exception": { + "message": "Undefined step: \"this step passes\"", + "type": "Cucumber::Core::Test::Result::Undefined", + "stackTrace": [ + "features/passing.feature:3:in `Given this step passes'" + ] + } + } +} diff --git a/event-protocol/validator/examples/events/012_test-case-finished.json b/event-protocol/validator/examples/events/012_test-case-finished.json new file mode 100644 index 0000000000..eb6e744e0a --- /dev/null +++ b/event-protocol/validator/examples/events/012_test-case-finished.json @@ -0,0 +1,18 @@ +{ + "type": "test-case-finished", + "sourceLocation": { + "uri": "features/passing.feature", + "line": 2 + }, + "result": { + "status": "undefined", + "duration": 13163000, + "exception": { + "message": "Undefined step: \"this step passes\"", + "type": "Cucumber::Core::Test::Result::Undefined", + "stackTrace": [ + "features/passing.feature:3:in `Given this step passes'" + ] + } + } +} diff --git a/event-protocol/lib/validateEvent.js b/event-protocol/validator/lib/validateEvent.js similarity index 100% rename from event-protocol/lib/validateEvent.js rename to event-protocol/validator/lib/validateEvent.js diff --git a/event-protocol/package.json b/event-protocol/validator/package.json similarity index 100% rename from event-protocol/package.json rename to event-protocol/validator/package.json diff --git a/event-protocol/schemas/attachment.json b/event-protocol/validator/schemas/attachment.json similarity index 94% rename from event-protocol/schemas/attachment.json rename to event-protocol/validator/schemas/attachment.json index d3adb162f6..39fcefdea6 100644 --- a/event-protocol/schemas/attachment.json +++ b/event-protocol/validator/schemas/attachment.json @@ -13,7 +13,7 @@ "uri": { "type": "string" }, - "start": { "$ref": "defs.json#/location" } + "start": { "$ref": "defs.json#/ast-location" } }, "required": [ "uri", diff --git a/event-protocol/schemas/defs.json b/event-protocol/validator/schemas/defs.json similarity index 58% rename from event-protocol/schemas/defs.json rename to event-protocol/validator/schemas/defs.json index efa65351aa..c4c7cd3665 100644 --- a/event-protocol/schemas/defs.json +++ b/event-protocol/validator/schemas/defs.json @@ -3,6 +3,23 @@ "title": "definitions", "description": "Re-usable parts of the schema, referenced by others", "location": { + "type": "object", + "properties": { + "line": { + "type": "integer", + "minimum": 1 + }, + "uri": { + "type": "string" + } + }, + "required": [ + "line", + "uri" + ], + "additionalProperties": false + }, + "gherkin-node-location": { "type": "object", "properties": { "line": { @@ -12,9 +29,6 @@ "column": { "type": "integer", "minimum": 0 - }, - "uri": { - "type": "string" } }, "required": [ @@ -73,5 +87,44 @@ "location" ], "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "status": { + "enum": [ + "passed", + "failed", + "pending", + "skipped", + "undefined" + ] + }, + "duration": { + "type": "integer" + }, + "exception": { "$ref": "defs.json#/exception" } + }, + "required": [ "status" ], + "additionalProperties": false + }, + "exception": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "stackTrace": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + } + }, + "required": [ "message", "stackTrace" ], + "additionalProperties": false } } diff --git a/event-protocol/schemas/gherkin-document.json b/event-protocol/validator/schemas/gherkin-document.json similarity index 100% rename from event-protocol/schemas/gherkin-document.json rename to event-protocol/validator/schemas/gherkin-document.json diff --git a/event-protocol/schemas/pickle.json b/event-protocol/validator/schemas/pickle.json similarity index 91% rename from event-protocol/schemas/pickle.json rename to event-protocol/validator/schemas/pickle.json index 239e2027a7..76bd42f2e1 100644 --- a/event-protocol/schemas/pickle.json +++ b/event-protocol/validator/schemas/pickle.json @@ -26,7 +26,7 @@ "locations": { "type": "array", "minLength": 1, - "items": { "$ref": "defs.json#/location" } + "items": { "$ref": "defs.json#/gherkin-node-location" } }, "steps": { "type": "array", @@ -43,7 +43,7 @@ "locations": { "type": "array", "minLength": 1, - "items": { "$ref": "defs.json#/location" } + "items": { "$ref": "defs.json#/gherkin-node-location" } } }, "required": [ diff --git a/event-protocol/schemas/source.json b/event-protocol/validator/schemas/source.json similarity index 100% rename from event-protocol/schemas/source.json rename to event-protocol/validator/schemas/source.json diff --git a/event-protocol/validator/schemas/test-case-finished.json b/event-protocol/validator/schemas/test-case-finished.json new file mode 100644 index 0000000000..4d778fc652 --- /dev/null +++ b/event-protocol/validator/schemas/test-case-finished.json @@ -0,0 +1,19 @@ +{ + "id": "https://raw.github.com/cucumber/cucumber/master/event-protocol/schemas/test-case-finished.json#", + "title": "test-case-finished", + "description": "A test case has finished executing", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "sourceLocation": { + "$ref": "defs.json#/location" + }, + "result": { + "$ref": "defs.json#/result" + } + }, + "required": [ "sourceLocation", "result" ], + "additionalProperties": false +} diff --git a/event-protocol/validator/schemas/test-case-prepared.json b/event-protocol/validator/schemas/test-case-prepared.json new file mode 100644 index 0000000000..00d4cdd418 --- /dev/null +++ b/event-protocol/validator/schemas/test-case-prepared.json @@ -0,0 +1,32 @@ +{ + "id": "https://raw.github.com/cucumber/cucumber/master/event-protocol/schemas/test-case-prepared.json#", + "title": "test-case-prepared", + "description": "Describes a test case that is about to be run", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "sourceLocation": { + "$ref": "defs.json#/location" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sourceLocation": { + "$ref": "defs.json#/location" + }, + "actionLocation": { + "$ref": "defs.json#/location" + } + } + }, + "required": [ "sourceLocation", "actionLocation" ], + "additionalProperties": false + } + }, + "required": [ "sourceLocation", "steps" ], + "additionalProperties": false +} diff --git a/event-protocol/validator/schemas/test-case-started.json b/event-protocol/validator/schemas/test-case-started.json new file mode 100644 index 0000000000..efe815b119 --- /dev/null +++ b/event-protocol/validator/schemas/test-case-started.json @@ -0,0 +1,16 @@ +{ + "id": "https://raw.github.com/cucumber/cucumber/master/event-protocol/schemas/test-case-started.json#", + "title": "test-case-started", + "description": "Signals the start of executing a test case", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "sourceLocation": { + "$ref": "defs.json#/location" + } + }, + "required": [ "sourceLocation" ], + "additionalProperties": false +} diff --git a/event-protocol/validator/schemas/test-run-started.json b/event-protocol/validator/schemas/test-run-started.json new file mode 100644 index 0000000000..48efe9a82a --- /dev/null +++ b/event-protocol/validator/schemas/test-run-started.json @@ -0,0 +1,38 @@ +{ + "id": "https://raw.github.com/cucumber/cucumber/master/event-protocol/schemas/test-run-started.json#", + "title": "test-run-started", + "description": "Signals the start of a test run", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "workingDirectory": { + "type": "string" + }, + "timestamp": { + "type": "number" + }, + "vcs": { + "type": "object", + "properties": { + "git": { + "type": "object", + "properties": { + "head": { + "type": "string" + }, + "diff": { + "type": "string" + } + } + } + } + } + }, + "required": [ + "type", + "timestamp" + ], + "additionalProperties": false +} diff --git a/event-protocol/validator/schemas/test-step-finished.json b/event-protocol/validator/schemas/test-step-finished.json new file mode 100644 index 0000000000..9450579026 --- /dev/null +++ b/event-protocol/validator/schemas/test-step-finished.json @@ -0,0 +1,29 @@ +{ + "id": "https://raw.github.com/cucumber/cucumber/master/event-protocol/schemas/test-step-finished.json#", + "title": "test-step-finished", + "description": "A step of a test case has finished executing", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "testCase": { + "type": "object", + "properties": { + "sourceLocation": { + "$ref": "defs.json#/location" + } + }, + "required": [ "sourceLocation" ], + "additionalProperties": false + }, + "index": { + "type": "number" + }, + "result": { + "$ref": "defs.json#/result" + } + }, + "required": [ "testCase", "index", "result" ], + "additionalProperties": false +} diff --git a/event-protocol/validator/schemas/test-step-started.json b/event-protocol/validator/schemas/test-step-started.json new file mode 100644 index 0000000000..847b2fccd7 --- /dev/null +++ b/event-protocol/validator/schemas/test-step-started.json @@ -0,0 +1,26 @@ +{ + "id": "https://raw.github.com/cucumber/cucumber/master/event-protocol/schemas/test-step-started.json#", + "title": "test-step-started", + "description": "A step of a test case is about to be executed", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "testCase": { + "type": "object", + "properties": { + "sourceLocation": { + "$ref": "defs.json#/location" + } + }, + "required": [ "sourceLocation" ], + "additionalProperties": false + }, + "index": { + "type": "number" + } + }, + "required": [ "testCase", "index" ], + "additionalProperties": false +} diff --git a/gui/renderer/index.js b/gui/renderer/index.js index 1e7c7066bd..159b1b8e0d 100644 --- a/gui/renderer/index.js +++ b/gui/renderer/index.js @@ -6,7 +6,7 @@ const path = require('path') // const Options = require('../cli/options') // const options = new Options(electron.remote.process.argv) -const $ = window.jQuery = require('jquery') +const $ = (window.jQuery = require('jquery')) require('bootstrap') process.on('unhandledRejection', function (reason) { @@ -15,7 +15,7 @@ process.on('unhandledRejection', function (reason) { }) const setProgress = (bar, states) => { - Object.keys(states).forEach((status) => { + Object.keys(states).forEach(status => { bar.getElementsByClassName(status)[0].style.width = `${states[status]}%` }) } @@ -27,15 +27,22 @@ class State { } getTestCase(sourceLocation) { - return this.testCases.find(testCase => ( - testCase.sourceLocation.uri == sourceLocation.uri && - testCase.sourceLocation.line == sourceLocation.line - )) + const result = this.testCases.find( + testCase => + testCase.sourceLocation.uri == sourceLocation.uri && + testCase.sourceLocation.line == sourceLocation.line + ) + if (!result) + throw new Error( + `Unable to find test case\n${JSON.stringify(sourceLocation, null, 2)}\nin ${JSON.stringify(this.testCases, null, 2)}` + ) + return result } } + let state = new State() -const render = (state) => { +const render = state => { const projectName = path.basename(state.pwd) const startTime = moment(state.startTime).format('h:mm:ss a on MMMM Do YYYY') document.title = `Cucumber - ${projectName} (${startTime})` @@ -54,29 +61,38 @@ const render = (state) => { $('.status-running').show() } - console.log(state.currentTestCase) document.getElementById('current-test-case').innerHTML = - state.currentTestCase && locationToString(state.currentTestCase.sourceLocation) + state.currentTestCase && + locationToString(state.currentTestCase.sourceLocation) document.getElementById('current-test-step').innerHTML = - state.currentTestStep && locationToString(state.currentTestStep.actionLocation) + state.currentTestStep && + locationToString(state.currentTestStep.actionLocation) const completedTestCases = state.testCases.filter(testCase => testCase.result) $('.test-cases-finished-count').text(completedTestCases.length) - const completedTestCasesWithResult = (status) => { - return completedTestCases.filter(testCase => testCase.result.status == status) + const completedTestCasesWithResult = status => { + return completedTestCases.filter( + testCase => testCase.result.status == status + ) } - setProgress( - document.getElementById('progressOfTestRun'), - { - passed: completedTestCasesWithResult('passed').length / state.testCases.length * 100, - failed: completedTestCasesWithResult('failed').length / state.testCases.length * 100, - pending: completedTestCasesWithResult('pending').length / state.testCases.length * 100, - } - ) + setProgress(document.getElementById('progressOfTestRun'), { + passed: completedTestCasesWithResult('passed').length / + state.testCases.length * + 100, + failed: completedTestCasesWithResult('failed').length / + state.testCases.length * + 100, + pending: completedTestCasesWithResult('pending').length / + state.testCases.length * + 100 + }) - const allTestSteps = state.testCases.reduce((result, testCase) => result.concat(testCase.steps), []) + const allTestSteps = state.testCases.reduce( + (result, testCase) => result.concat(testCase.steps), + [] + ) const completedTestSteps = allTestSteps.filter(step => step.result) $('.test-steps-finished-count').text(completedTestSteps.length) } @@ -92,11 +108,11 @@ events.on('test-run-started', (event, message) => { events.on('source', (event, message) => { state.gherkinDocs[message.uri] = { - body: message.data, + body: message.data } }) -events.on('test-case-compiled', (event, testCase) => { +events.on('test-case-prepared', (event, testCase) => { state.testCases.push(testCase) const div = document.createElement('div') @@ -110,7 +126,7 @@ events.on('test-case-compiled', (event, testCase) => { div.appendChild(p) const ul = document.createElement('ul') - testCase.steps.forEach((step) => { + testCase.steps.forEach(step => { const li = document.createElement('li') step.element = li li.innerHTML = getStepHtml(step) @@ -127,16 +143,17 @@ events.on('test-case-started', (event, message) => { }) events.on('test-step-started', (event, message) => { - state.currentTestStep = state - .getTestCase(message.testCase.sourceLocation) - .steps[message.index] + state.currentTestStep = state.getTestCase( + message.testCase.sourceLocation + ).steps[message.index] render(state) }) events.on('test-step-finished', (event, message) => { - const testStep = state - .getTestCase(message.testCase.sourceLocation) - .steps[message.index] + console.log(message) + const testStep = state.getTestCase(message.testCase.sourceLocation).steps[ + message.index + ] testStep.result = message.result render(state) @@ -147,7 +164,8 @@ events.on('test-step-finished', (event, message) => { if (message.result.exception) { const error = document.createElement('pre') error.className = 'alert alert-danger' - error.innerHTML = message.result.exception.message || 'No error message was reported' + error.innerHTML = + message.result.exception.message || 'No error message was reported' li.appendChild(error) const stackTrace = document.createElement('pre') stackTrace.innerText = message.result.exception.stackTrace.join('\n') @@ -171,33 +189,36 @@ events.on('end', () => { render(state) }) -const createResultBadge = (result) => { +const createResultBadge = result => { const badge = document.createElement('span') badge.className = `badge ${result.status}` badge.innerText = result.status return badge } -const createDurationBadge = (result) => { +const createDurationBadge = result => { const badge = document.createElement('span') badge.className = 'badge' badge.innerText = `${Math.ceil(result.duration / 1000000)}ms` return badge } -const getStepHtml = (step) => { - if (isHook(step)) - return locationToString(step.actionLocation) +const getStepHtml = step => { + if (isHook(step)) return locationToString(step.actionLocation) const line = step.sourceLocation.line - const text = state.gherkinDocs[step.sourceLocation.uri].body.split('\n')[line - 1] + const text = state.gherkinDocs[step.sourceLocation.uri].body.split('\n')[ + line - 1 + ] return `${text}` -} -const locationToString = (location) => `${location.uri}:${location.line}` + function isHook(step) { + return !step.sourceLocation + } +} -const isHook = (step) => !state.gherkinDocs[step.sourceLocation.uri] +const locationToString = location => `${location.uri}:${location.line}` -const getTestCaseDiv = (sourceLocation) => { +const getTestCaseDiv = sourceLocation => { return state.getTestCase(sourceLocation).div }