cyrun 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f36d84fb3bdf5d10aa072b188d2777c968004eadd8a6f9fcaf99369c03f1f08f
4
+ data.tar.gz: 24d85427355c870c69c0ad77e2d058a7ae5b8f5e1f5b21214efb87cb7ba12e60
5
+ SHA512:
6
+ metadata.gz: a18ca4b67327cbe269aa455fb5fe2425874142cbcbe05b6e303856a1dd94160c3faa91d5314aace4558482d308e2c349505aa8f787549f5bbdab021827bdf794
7
+ data.tar.gz: baf623d30f79bea0a540115b7f184593e9d703aea7ee85ade631d01b7923518c5b75f046979c0afa7f972aa89742e1bfe94d7a540de1fd9e0671a6f9a46c5b31
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 2.6
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in cyrun.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ gem 'minitest', '~> 5.0'
11
+
12
+ gem 'standard', '~> 1.3'
13
+
14
+
15
+ gem 'nokogiri'
16
+
17
+ gem 'colorize'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Oskar Kirmis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Cyrun
2
+
3
+ cyrun is a command line utility to run and automatically retry failed cypress specs.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add cyrun
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install cyrun
14
+
15
+ ## Usage
16
+
17
+ cyrun uses npm to run the specs. It will call:
18
+
19
+ ```shell
20
+ npm run <task-name-you-specify> -- --specs <list-of-specs-to-run> --config <screenshot-configuration> --headless --reporter junit --reporter-options <options>
21
+ ```
22
+
23
+ So for example, cyrun could be used as follows:
24
+
25
+ ```shell
26
+ cyrun --task npm-task-for-cypress --working-directory --pattern "apps/e2e/**/*.spec.ts"
27
+ ```
28
+
29
+ ## License
30
+
31
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "standard/rake"
13
+
14
+ task default: %i[test standard]
data/cyrun.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/cyrun/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'cyrun'
7
+ spec.version = Cyrun::VERSION
8
+ spec.authors = ['Oskar Kirmis']
9
+ spec.email = ['oskar.kirmis@posteo.de']
10
+
11
+ spec.summary = 'Run and retry cypress specs'
12
+ spec.description = 'Run cypress specs in batches and re-run specs or test them for stability.'
13
+ spec.homepage = 'https://git.iftrue.de/okirmis/cyrun'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 2.6.0'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://git.iftrue.de/okirmis/cyrun'
19
+ spec.metadata['changelog_uri'] = 'https://git.iftrue.de/okirmis/cyrun'
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ spec/ features/ .git .circleci appveyor])
26
+ end
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+ end
data/exe/cyrun ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'cyrun'
5
+ require 'optparse'
6
+
7
+ npm_task_name = 'e2e'
8
+ pattern = '*/**/*.cy.{ts,js}'
9
+ working_directory = Dir.pwd
10
+ results_directory = "#{Dir.pwd}/results"
11
+ group_size = 10000
12
+ max_retries = nil
13
+ iterations = nil
14
+ screenshots_directory = "#{Dir.pwd}/screenshots"
15
+ screenshots_pattern = '*/**/screenshots/**/*.png'
16
+
17
+ OptionParser.new do |opts|
18
+ opts.banner = 'Usage: cyrun [options]'
19
+
20
+ opts.on('-t', '--task TASK', String, 'NPM task to execute') do |t|
21
+ npm_task_name = t
22
+ end
23
+
24
+ opts.on('-p', '--pattern PATTERN', String, 'File path pattern') do |p|
25
+ pattern = p
26
+ end
27
+
28
+ opts.on('-w', '--working-directory DIRECTORY', String, 'Working directory') do |w|
29
+ working_directory = w
30
+ end
31
+
32
+ opts.on('-r', '--retries RETRIES', Integer, 'Maximum number of retries') do |r|
33
+ max_retries = r
34
+ end
35
+
36
+ opts.on('-g', '--group-size GROUPSIZE', Integer, 'Number of specs to process in a batch') do |g|
37
+ group_size = g
38
+ end
39
+
40
+ opts.on('-i', '--stable-runs ITERATIONS', Integer, 'Number of times the spec is run (an has to pass)') do |i|
41
+ iterations = i
42
+ end
43
+
44
+ opts.on('-d', '--results-directory DIRECTORY', String, 'Directory where to store the result files') do |d|
45
+ results_directory = d
46
+ end
47
+
48
+ opts.on('-s', '--screenshots-directory DIRECTORY', String, 'Directory where to store the screenshot files') do |d|
49
+ screenshots_directory = d
50
+ end
51
+
52
+ opts.on('--screenshots-pattern PATTERN', String, 'Glob pattern where to look for screenshots') do |p|
53
+ screenshots_pattern = p
54
+ end
55
+ end.parse!
56
+
57
+ if !iterations.nil? && !max_retries.nil?
58
+ Cyrun::Logger.error 'Only --iterations OR --retries can be specified.'
59
+ Kernel.exit!(-1)
60
+ end
61
+
62
+ max_retries = 1 if iterations.nil? && max_retries.nil?
63
+
64
+ base_directory = File.expand_path(working_directory)
65
+ Dir.chdir base_directory
66
+
67
+ runner = Cyrun::Runner.new(
68
+ base_directory,
69
+ pattern,
70
+ screenshots_pattern,
71
+ group_size,
72
+ npm_task_name,
73
+ max_retries: max_retries,
74
+ iterations: iterations
75
+ )
76
+ runner.run
77
+ runner.store_results(results_directory, screenshots_directory)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrun
4
+ class GroupBuilder
5
+ def initialize(group_size)
6
+ @queue = []
7
+ @group_size = group_size
8
+ end
9
+
10
+ # @param [SpecTask] task
11
+ def add_task(task)
12
+ @queue.push task
13
+ end
14
+
15
+ def next_group
16
+ group = []
17
+ group.push @queue.shift while @queue.any? && group.size < @group_size
18
+ group
19
+ end
20
+
21
+ def any?
22
+ @queue.any?
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ require 'colorize'
3
+
4
+ module Cyrun
5
+ class Logger
6
+ def self.error(*message)
7
+ puts "[ERROR] #{message.map(&:to_s).join}".red
8
+ end
9
+
10
+ def self.info(*message)
11
+ puts "[INFO] #{message.map(&:to_s).join}"
12
+ end
13
+ end
14
+
15
+ module Logging
16
+ def info(*args)
17
+ Logger.info(*args)
18
+ end
19
+
20
+ def error(*args)
21
+ Logger.error(*args)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module Cyrun
6
+ class ResultDirectory
7
+ attr_reader :temp_directory, :screenshots_pattern, :spec_tasks
8
+
9
+ def initialize(temp_directory, screenshots_pattern, spec_tasks)
10
+ @temp_directory = temp_directory
11
+ @spec_tasks = spec_tasks
12
+ @screenshots_pattern = screenshots_pattern
13
+ end
14
+
15
+ def read_result_files
16
+ absolute_filenames = Dir
17
+ .glob('results-*.xml', base: temp_directory)
18
+ .map { |filename| File.expand_path(filename, temp_directory) }
19
+
20
+ results = absolute_filenames
21
+ .map { |filename| read_result_file filename }
22
+ .reject(&:nil?)
23
+
24
+ spec_tasks.map do |task|
25
+ find_result_for_task(results, task) || create_failed_spec_result(task)
26
+ end
27
+ end
28
+
29
+ def screenshots
30
+ Dir
31
+ .glob(screenshots_pattern, base: Dir.pwd)
32
+ .map { |filename| File.expand_path(filename, Dir.pwd) }
33
+ end
34
+
35
+ protected
36
+
37
+ def read_result_file(absolute_path)
38
+ document = Nokogiri::XML(File.read(absolute_path))
39
+
40
+ failure_count = extract_failures_from_document document
41
+ time_in_seconds = extract_time_in_seconds_from_document document
42
+ filename = extract_filename_from_document document
43
+
44
+ spec = spec_tasks.detect { |task| task.filename.end_with?(File.basename(filename)) }
45
+ return nil if spec.nil?
46
+
47
+ SingleSpecExecutionResult.new(spec, absolute_path, time_in_seconds, failure_count.zero?)
48
+ end
49
+
50
+ def extract_failures_from_document(document)
51
+ document.css('testsuites').first.attr('failures').to_i
52
+ end
53
+
54
+ def extract_time_in_seconds_from_document(document)
55
+ document.css('testsuites').first.attr('time').to_f.round
56
+ end
57
+
58
+ # @param [Nokogiri::XML::Document] document
59
+ def extract_filename_from_document(document)
60
+ document.css('testsuite').first&.attr('file').to_s
61
+ end
62
+
63
+ private
64
+
65
+ def create_failed_spec_result(task)
66
+ SingleSpecExecutionResult.new(task, nil, 0.0, false)
67
+ end
68
+
69
+ def find_result_for_task(results, task)
70
+ results.detect { |result| result.task == task }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Cyrun
6
+ class Runner
7
+ include Logging
8
+
9
+ attr_reader :collector, :group_builder, :task_runner,
10
+ :overall_results, :max_retries, :iterations
11
+
12
+ def initialize(base_directory, pattern, screenshots_pattern, group_size, npm_task_name, max_retries: nil, iterations: nil)
13
+ @collector = SpecCollector.new(base_directory, npm_task_name, [pattern])
14
+ @group_builder = GroupBuilder.new(group_size)
15
+ @task_runner = TaskRunner.new(screenshots_pattern)
16
+
17
+ @overall_results = SpecGroupExecutionResult.create_empty
18
+ @max_retries = max_retries
19
+ @iterations = iterations
20
+ end
21
+
22
+ def run
23
+ collect_specs
24
+ run_next_group while group_builder.any?
25
+
26
+ overall_results
27
+ end
28
+
29
+ def store_results(results_directory, screenshots_directory)
30
+ latest_results = overall_results.latest_results
31
+ invalid_results = latest_results.select { |result| result.result_filename.nil? }
32
+ valid_results = latest_results - invalid_results
33
+
34
+ copy_result_files valid_results, results_directory
35
+ copy_screenshots overall_results, screenshots_directory
36
+
37
+ invalid_results.empty?
38
+ end
39
+
40
+ private
41
+
42
+ def copy_result_files(results, directory)
43
+ FileUtils.mkdir_p directory
44
+
45
+ success = true
46
+
47
+ info "Copying #{results.size} result files"
48
+ results.each do |result|
49
+ FileUtils.cp(result.result_filename, directory)
50
+ rescue StandardError
51
+ error "Failed to copy results for #{result.task.filename}"
52
+ success = false
53
+ end
54
+
55
+ success
56
+ end
57
+
58
+ def copy_screenshots(results, directory)
59
+ FileUtils.mkdir_p directory
60
+
61
+ success = true
62
+
63
+ info "Copying #{results.screenshots.size} screenshots"
64
+ results.screenshots.each do |path|
65
+ FileUtils.cp(path, "#{directory}/#{SecureRandom.uuid}-#{File.basename(path)}")
66
+ rescue StandardError
67
+ error "Failed to copy screenshot #{path}"
68
+ success = false
69
+ end
70
+
71
+ success
72
+ end
73
+
74
+ def collect_specs
75
+ info 'Collecting specs ...'
76
+ collector.files.each do |filename|
77
+ info filename
78
+
79
+ group_builder.add_task(
80
+ collector.create_task(filename)
81
+ )
82
+ end
83
+
84
+ info "Collected #{collector.files.size} specs"
85
+ end
86
+
87
+ def run_next_group
88
+ group = group_builder.next_group
89
+ info("Running #{group.size} specs")
90
+ results = task_runner.run(group)
91
+ info('Group completed with the following results:')
92
+
93
+ overall_results.merge!(results)
94
+
95
+ results
96
+ .single_results
97
+ .each(&method(:handle_single_execution_result))
98
+
99
+ log_results overall_results
100
+ end
101
+
102
+ def handle_single_execution_result(result)
103
+ log_spec_result result
104
+ requeue_on_for_retries(result, overall_results, max_retries, group_builder) unless max_retries.nil?
105
+ requeue_for_iteration(result, overall_results, iterations, group_builder) unless iterations.nil?
106
+ end
107
+
108
+ def requeue_on_for_retries(result, overall_results, max_retries, group_builder)
109
+ return if result.success
110
+
111
+ if overall_results.task_fail_count(result.task) > max_retries
112
+ Cyrun::Logger.info(" -> Finally failed #{result.task.name}")
113
+ else
114
+ Cyrun::Logger.info(" -> Re-queuing #{result.task.name}")
115
+ group_builder.add_task result.task
116
+ end
117
+ end
118
+
119
+ def requeue_for_iteration(result, overall_results, iterations, group_builder)
120
+ if result.success
121
+ group_builder.add_task result.task if overall_results.task_result_count(result.task) < iterations
122
+ else
123
+ Cyrun::Logger.info(" -> Not re-queuing #{result.task.name}")
124
+ end
125
+ end
126
+
127
+ def log_results(overall_results)
128
+ info(
129
+ "Execution time: #{overall_results.gross_time_in_seconds} seconds, ",
130
+ "Spec time: #{overall_results.net_time_in_seconds} seconds, ",
131
+ "Effectiveness: #{(overall_results.effectiveness * 100).round} %"
132
+ )
133
+ end
134
+
135
+ def log_spec_result(result)
136
+ if result.success
137
+ info(" - Spec #{result.task.name} completed (#{result.net_time_in_seconds} seconds)")
138
+ else
139
+ info(" - Spec #{result.task.name} failed (#{result.net_time_in_seconds} seconds)")
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrun
4
+ class SingleSpecExecutionResult
5
+ attr_reader :task, :net_time_in_seconds, :success, :result_filename
6
+
7
+ def initialize(task, result_filename, net_time_in_seconds, success)
8
+ @task = task
9
+ @net_time_in_seconds = net_time_in_seconds
10
+ @success = success
11
+ @result_filename = result_filename
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrun
4
+ class SpecCollector
5
+ attr_reader :patterns, :base_directory, :npm_task_name
6
+
7
+ def initialize(base_directory, npm_task_name, patterns)
8
+ @patterns = patterns
9
+ @npm_task_name = npm_task_name
10
+ @base_directory = base_directory
11
+ end
12
+
13
+ def files
14
+ @files ||= patterns
15
+ .map { |pattern| Dir.glob(pattern, base: base_directory) }
16
+ .flatten
17
+ .map { |filename| File.expand_path(filename, base_directory) }
18
+ end
19
+
20
+ def create_task(filename)
21
+ SpecTask.new(filename, @npm_task_name)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrun
4
+ class SpecGroupExecutionResult
5
+ attr_reader :single_results, :screenshots, :gross_time_in_seconds
6
+
7
+ def self.create_empty
8
+ SpecGroupExecutionResult.new([], [], 0)
9
+ end
10
+
11
+ def initialize(single_results, screenshots, gross_time_in_seconds)
12
+ @single_results = single_results
13
+ @gross_time_in_seconds = gross_time_in_seconds
14
+ @screenshots = screenshots
15
+ end
16
+
17
+ def net_time_in_seconds
18
+ single_results.map(&:net_time_in_seconds).sum
19
+ end
20
+
21
+ def effectiveness
22
+ return 0.0 if gross_time_in_seconds.zero?
23
+
24
+ net_time_in_seconds.to_f / gross_time_in_seconds
25
+ end
26
+
27
+ def merge!(other)
28
+ @single_results += other.single_results
29
+ @gross_time_in_seconds += other.gross_time_in_seconds
30
+ @screenshots += other.screenshots
31
+ end
32
+
33
+ def task_fail_count(task)
34
+ single_results
35
+ .select { |result| !result.success && result.task == task }
36
+ .size
37
+ end
38
+
39
+ def task_result_count(task)
40
+ task_results(task).size
41
+ end
42
+
43
+ def task_results(task)
44
+ single_results
45
+ .select { |result| result.task == task }
46
+ end
47
+
48
+ def latest_results
49
+ tasks = single_results.map(&:task).uniq
50
+
51
+ tasks.map do |task|
52
+ task_results(task).last
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrun
4
+ class SpecTask
5
+ attr_reader :filename, :npm_task_name
6
+
7
+ def initialize(filename, npm_task_name)
8
+ @filename = filename
9
+ @npm_task_name = npm_task_name
10
+ end
11
+
12
+ def name
13
+ File.basename(filename)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'tmpdir'
5
+ require 'securerandom'
6
+ require 'time'
7
+
8
+ module Cyrun
9
+ class TaskRunner
10
+ include Logging
11
+
12
+ CURRENT_SPEC_REGEX = /^\s*Running:\s+(\S+)\s+\([0-9]+ of [0-9]+\)\s*$/.freeze
13
+
14
+ attr_reader :screenshots_pattern
15
+
16
+ def initialize(screenshots_pattern)
17
+ @screenshots_pattern = screenshots_pattern
18
+ end
19
+
20
+ def run(group)
21
+ temp_directory = create_directory
22
+ command = build_command(group, temp_directory)
23
+
24
+ info "Command: #{command.join " "}"
25
+
26
+ execution_time = Cyrun::Timing.run do
27
+ execute_command command
28
+ end
29
+
30
+ results = ResultDirectory.new(temp_directory, screenshots_pattern, group)
31
+ SpecGroupExecutionResult.new(
32
+ results.read_result_files,
33
+ results.screenshots,
34
+ execution_time
35
+ )
36
+ end
37
+
38
+ protected
39
+
40
+ def create_directory
41
+ Dir.mktmpdir(SecureRandom.uuid)
42
+ end
43
+
44
+ def build_command(spec_tasks, temp_directory)
45
+ [
46
+ 'npm', 'run', spec_tasks.first&.npm_task_name, '--'
47
+ ] +
48
+ build_spec_arguments(spec_tasks) +
49
+ build_reporter_options(temp_directory)
50
+ end
51
+
52
+ def build_spec_arguments(spec_tasks)
53
+ ['--spec', spec_tasks.map(&:filename).join(',')]
54
+ end
55
+
56
+ def build_reporter_options(temp_directory)
57
+ ['--headless', '--reporter', 'junit', '--reporter-options',
58
+ "mochaFile=#{temp_directory}/results-[hash].xml,toConsole=true"]
59
+ end
60
+
61
+ private
62
+
63
+ def execute_command(command)
64
+ output = []
65
+
66
+ # @type [IO]
67
+ process = IO.popen(command, err: %i[child out])
68
+ output.push process_command_output_line(process.gets) until process.eof?
69
+ process.close
70
+
71
+ return if $CHILD_STATUS.success?
72
+
73
+ Logger.error('Command failed')
74
+ Logger.error(output.join("\n"))
75
+ end
76
+
77
+ def process_command_output_line(line)
78
+ line ||= ''
79
+ current_spec_parts = line.scan CURRENT_SPEC_REGEX
80
+
81
+ Logger.info(' - Running ', current_spec_parts.first&.first) if current_spec_parts.any?
82
+
83
+ line
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrun
4
+ module Timing
5
+ def self.run
6
+ start_time = Time.now.to_i
7
+
8
+ yield
9
+
10
+ end_time = Time.now.to_i
11
+ end_time - start_time
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrun
4
+ VERSION = "0.1.0"
5
+ end
data/lib/cyrun.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cyrun/version'
4
+ require_relative 'cyrun/timing'
5
+ require_relative 'cyrun/logger'
6
+ require_relative 'cyrun/result_directory'
7
+ require_relative 'cyrun/group_builder'
8
+ require_relative 'cyrun/task_runner'
9
+ require_relative 'cyrun/spec_collector'
10
+ require_relative 'cyrun/spec_task'
11
+ require_relative 'cyrun/single_spec_execution_result'
12
+ require_relative 'cyrun/spec_group_execution_result'
13
+ require_relative 'cyrun/runner'
14
+
15
+ module Cyrun
16
+ end
@@ -0,0 +1,15 @@
1
+ # Download sources
2
+ sources:
3
+ - name: ruby/gem_rbs_collection
4
+ remote: https://github.com/ruby/gem_rbs_collection.git
5
+ revision: main
6
+ repo_dir: gems
7
+
8
+ # A directory to install the downloaded RBSs
9
+ path: .gem_rbs_collection
10
+
11
+ gems:
12
+ # Skip loading rbs gem's RBS.
13
+ # It's unnecessary if you don't use rbs as a library.
14
+ - name: rbs
15
+ ignore: true
@@ -0,0 +1,6 @@
1
+ module Cyrun
2
+ module Logging
3
+ def error: (*String? message) -> void
4
+ def info: (*String? message) -> void
5
+ end
6
+ end
data/sig/cyrun.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Cyrun
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,12 @@
1
+ module Cyrun
2
+ class GroupBuilder
3
+ @group_size: Integer
4
+ @queue: Array[SpecTask]
5
+
6
+ def add_task: (task: SpecTask) -> void
7
+
8
+ def any?: -> bool
9
+
10
+ def next_group: -> Array[SpecTask]
11
+ end
12
+ end
data/sig/logger.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module Cyrun
2
+ class Logger
3
+ def self.error: (*String? message) -> void
4
+ def self.info: (*String? message) -> void
5
+ end
6
+ end
@@ -0,0 +1,23 @@
1
+ module Cyrun
2
+ class ResultDirectory
3
+ attr_reader spec_tasks: Array[SpecTask]
4
+ attr_reader temp_directory: String
5
+ attr_reader screenshots_pattern: String
6
+
7
+ def initialize: (String, String, Array[SpecTask]) -> void
8
+
9
+ def extract_failures_from_document: (Nokogiri::XML::Document) -> Integer
10
+ def extract_filename_from_document: (Nokogiri::XML::Document) -> String
11
+ def extract_time_in_seconds_from_document: (Nokogiri::XML::Document) -> Float
12
+
13
+ def read_result_file: (String) -> SingleSpecExecutionResult?
14
+
15
+ def read_result_files: -> Array[SingleSpecExecutionResult]
16
+ def screenshots: -> Array[String]
17
+
18
+ private
19
+
20
+ def find_result_for_task: (Array[SingleSpecExecutionResult], SpecTask) -> SingleSpecExecutionResult?
21
+ def create_failed_spec_result: (SpecTask) -> SingleSpecExecutionResult
22
+ end
23
+ end
data/sig/runner.rbs ADDED
@@ -0,0 +1,34 @@
1
+ module Cyrun
2
+ class Runner
3
+
4
+ attr_reader collector: SpecCollector
5
+ attr_reader group_builder: GroupBuilder
6
+ attr_reader iterations: Integer?
7
+ attr_reader max_retries: Integer?
8
+ attr_reader npm_task_name: String
9
+ attr_reader overall_results: SpecGroupExecutionResult
10
+ attr_reader task_runner: TaskRunner
11
+
12
+ def initialize: (String, String, String, Integer, String, max_retries: Integer?, iterations: Integer?) -> void
13
+
14
+ def store_results: (String, String)-> bool
15
+
16
+ def run: -> SpecGroupExecutionResult
17
+
18
+ private
19
+
20
+ def collect_specs: (String) -> void
21
+
22
+ def copy_result_files: (Array[SingleSpecExecutionResult], String) -> bool
23
+ def copy_screenshots: (SpecGroupExecutionResult, String) -> bool
24
+
25
+ def handle_single_execution_result: (SingleSpecExecutionResult) -> void
26
+ def log_results: (SpecGroupExecutionResult) -> void
27
+ def log_spec_result: (SingleSpecExecutionResult) -> void
28
+
29
+ def requeue_on_for_retries: (SingleSpecExecutionResult, SpecGroupExecutionResult, Integer, GroupBuilder) -> void
30
+ def requeue_for_iteration: (SingleSpecExecutionResult, SpecGroupExecutionResult, Integer, GroupBuilder) -> void
31
+
32
+ def run_next_group: -> void
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ module Cyrun
2
+ class SingleSpecExecutionResult
3
+ attr_reader net_time_in_seconds: Float
4
+ attr_reader result_filename: String?
5
+ attr_reader success: bool
6
+ attr_reader task: SpecTask
7
+
8
+ def initialize: (SpecTask, String?, Float, bool) -> void
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ module Cyrun
2
+ class SpecCollector
3
+ @base_directory: String
4
+ @files: Array[String]
5
+ @npm_task_name: String
6
+ @patterns: Array[String]
7
+
8
+ attr_reader base_directory: String
9
+ attr_reader npm_task_name: String
10
+ attr_reader patterns: String
11
+
12
+ def initialize: (base_directory: String, patterns: Array[String]) -> void
13
+
14
+ def create_task: (String) -> SpecTask
15
+
16
+ def files: -> Array[String]
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Cyrun
2
+ class SpecGroupExecutionResult
3
+ def self.create_empty: -> SpecGroupExecutionResult
4
+
5
+ attr_reader gross_time_in_seconds: Integer
6
+ attr_reader screenshots: Array[String]
7
+ attr_reader single_results: Array[SingleSpecExecutionResult]
8
+
9
+ def initialize: (Array[SingleSpecExecutionResult], Array[String], Integer) -> void
10
+ def effectiveness: -> Float
11
+ def merge!: (SpecGroupExecutionResult)-> void
12
+ def net_time_in_seconds: -> Integer
13
+ def task_fail_count: (SpecTask) -> Integer
14
+ def task_result_count: (SpecTask) -> Integer
15
+ def task_results: (SpecTask) -> Array[SingleSpecExecutionResult]
16
+ def latest_results: -> Array[SingleSpecExecutionResult]
17
+ end
18
+ end
data/sig/spec_task.rbs ADDED
@@ -0,0 +1,10 @@
1
+ module Cyrun
2
+ class SpecTask
3
+
4
+ attr_reader base_directory: String
5
+ attr_reader filename: String
6
+ attr_reader npm_task_name: String
7
+
8
+ def name: -> String
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ module Cyrun
2
+ class TaskRunner
3
+ CURRENT_SPEC_REGEX: Regexp
4
+
5
+ attr_reader screenshots_pattern: String
6
+
7
+ def build_command: (Array[SpecTask], String) -> Array[String]
8
+ def build_reporter_options: (String) -> Array[String]
9
+
10
+ def build_spec_arguments: (Array[SpecTask]) -> Array[String]
11
+
12
+ def create_directory: -> String
13
+
14
+
15
+ def run: (Array[SpecTask]) -> SpecGroupExecutionResult
16
+
17
+ private
18
+
19
+ def execute_command: (Array[String]) -> void
20
+ def process_command_output_line: (String?) -> String
21
+ end
22
+ end
data/sig/timing.rbs ADDED
@@ -0,0 +1,5 @@
1
+ module Cyrun
2
+ module Timing
3
+ def self.run: { () -> void } -> Integer
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cyrun
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Oskar Kirmis
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-04-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Run cypress specs in batches and re-run specs or test them for stability.
14
+ email:
15
+ - oskar.kirmis@posteo.de
16
+ executables:
17
+ - cyrun
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".standard.yml"
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - cyrun.gemspec
27
+ - exe/cyrun
28
+ - lib/cyrun.rb
29
+ - lib/cyrun/group_builder.rb
30
+ - lib/cyrun/logger.rb
31
+ - lib/cyrun/result_directory.rb
32
+ - lib/cyrun/runner.rb
33
+ - lib/cyrun/single_spec_execution_result.rb
34
+ - lib/cyrun/spec_collector.rb
35
+ - lib/cyrun/spec_group_execution_result.rb
36
+ - lib/cyrun/spec_task.rb
37
+ - lib/cyrun/task_runner.rb
38
+ - lib/cyrun/timing.rb
39
+ - lib/cyrun/version.rb
40
+ - rbs_collection.yaml
41
+ - sig/cyrun.rbs
42
+ - sig/cyrun/logging.rbs
43
+ - sig/group_builder.rbs
44
+ - sig/logger.rbs
45
+ - sig/result_directory.rbs
46
+ - sig/runner.rbs
47
+ - sig/single_spec_execution_result.rbs
48
+ - sig/spec_collector.rbs
49
+ - sig/spec_group_execution_result.rbs
50
+ - sig/spec_task.rbs
51
+ - sig/task_runner.rbs
52
+ - sig/timing.rbs
53
+ homepage: https://git.iftrue.de/okirmis/cyrun
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ homepage_uri: https://git.iftrue.de/okirmis/cyrun
58
+ source_code_uri: https://git.iftrue.de/okirmis/cyrun
59
+ changelog_uri: https://git.iftrue.de/okirmis/cyrun
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.6.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.3.13
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Run and retry cypress specs
79
+ test_files: []