matrixeval 0.1.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ require_relative "./context/find_by_command_options"
2
+ require_relative "./context/build_docker_compose_extend"
3
+
4
+ module Matrixeval
5
+ class Context
6
+
7
+ class << self
8
+
9
+ def find_by_command_options!(options)
10
+ FindByCommandOptions.call(options)
11
+ end
12
+
13
+ def all
14
+ Config.variant_combinations.map do |variants|
15
+ Context.new(
16
+ main_variant: variants.find { |v| v.vector.main? },
17
+ rest_variants: variants.reject { |v| v.vector.main? }
18
+ )
19
+ end.select do |context|
20
+ Config.exclusions.none? do |exclusion|
21
+ context.match_exclusion?(exclusion)
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ attr_reader :main_variant, :rest_variants
29
+
30
+ def initialize(main_variant:, rest_variants:)
31
+ @main_variant = main_variant
32
+ @rest_variants = (rest_variants || []).sort do |v1, v2|
33
+ v1.id <=> v2.id
34
+ end
35
+ end
36
+
37
+ def name
38
+ variants.map(&:name).join(", ")
39
+ end
40
+
41
+ def id
42
+ [[main_variant.id] + rest_variants.map(&:id)].join("_")
43
+ end
44
+
45
+ def env
46
+ rest_variants.map(&:env).reduce({}, &:merge)
47
+ .merge(main_variant.env)
48
+ end
49
+
50
+ def docker_compose_service_name
51
+ main_variant.id
52
+ end
53
+
54
+ def docker_compose_file_path
55
+ Matrixeval.working_dir.join(".matrixeval/docker-compose/#{id}.yml")
56
+ end
57
+
58
+ def variants
59
+ [main_variant] + rest_variants
60
+ end
61
+
62
+ def match_exclusion?(exclusion)
63
+ return false if exclusion.empty?
64
+
65
+ variants.all? do |variant|
66
+ vector_key = variant.vector.key
67
+ if exclusion.key?(vector_key)
68
+ exclusion[vector_key].to_s == variant.key
69
+ else
70
+ true
71
+ end
72
+ end
73
+ end
74
+
75
+ def docker_compose_extend
76
+ BuildDockerComposeExtend.call(self)
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ module Matrixeval
2
+ class DockerCompose
3
+ class Extend
4
+
5
+ def initialize(config)
6
+ @config = config || {}
7
+ end
8
+
9
+ def volumes
10
+ @config["volumes"] || {}
11
+ end
12
+
13
+ def services
14
+ @config["services"] || {}
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Matrixeval
2
+ class DockerCompose
3
+ class ExtendRaw
4
+
5
+ def initialize(config)
6
+ @config = config || {}
7
+ end
8
+
9
+ def content
10
+ @config.to_json
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,122 @@
1
+ module Matrixeval
2
+ class DockerCompose
3
+ class File
4
+ class << self
5
+
6
+ def create_all
7
+ FileUtils.mkdir_p folder
8
+
9
+ Context.all.each do |context|
10
+ new(context).create
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def folder
17
+ Matrixeval.working_dir.join(".matrixeval/docker-compose")
18
+ end
19
+ end
20
+
21
+ attr_reader :context
22
+
23
+ def initialize(context)
24
+ @context = context
25
+ end
26
+
27
+ def create
28
+ ::File.open(docker_compose_file_path, 'w+') do |file|
29
+ file.puts build_content
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def docker_compose_file_path
36
+ context.docker_compose_file_path
37
+ end
38
+
39
+ def build_content
40
+ {
41
+ "version" => "3",
42
+ "services" => services_json,
43
+ "volumes" => volumes_json
44
+ }.to_yaml.sub(/---\n/, "")
45
+ end
46
+
47
+ def services_json
48
+ services = {}
49
+
50
+ services[main_variant.docker_compose_service_name] = {
51
+ "image" => main_variant.container.image,
52
+ "volumes" => mounts,
53
+ "environment" => env,
54
+ "working_dir" => "/app"
55
+ }.merge(depends_on)
56
+
57
+ services.merge(docker_compose_extend.services)
58
+ end
59
+
60
+ def volumes_json
61
+ target.volumes(context).merge(
62
+ docker_compose_extend.volumes
63
+ )
64
+ end
65
+
66
+ def env
67
+ target.env(context).merge(
68
+ Config.env,
69
+ main_variant.container.env,
70
+ context.env
71
+ )
72
+ end
73
+
74
+ def depends_on
75
+ if docker_compose_extend.services.keys.empty?
76
+ {}
77
+ else
78
+ { "depends_on" => docker_compose_extend.services.keys }
79
+ end
80
+ end
81
+
82
+ def main_variant
83
+ context.main_variant
84
+ end
85
+
86
+ def mounts
87
+ ["../..:/app:cached"] + target.mounts(context) + extra_mounts
88
+ end
89
+
90
+ def extra_mounts
91
+ mounts = Config.mounts + context.variants.map(&:mounts).flatten
92
+ mounts.map do |mount|
93
+ local_path, in_docker_path = mount.split(':')
94
+ next mount if Pathname.new(local_path).absolute?
95
+
96
+ local_path = Matrixeval.working_dir.join(local_path)
97
+ docker_compose_folder_path = Matrixeval.working_dir.join(".matrixeval/docker-compose")
98
+ local_path = local_path.relative_path_from docker_compose_folder_path
99
+
100
+ "#{local_path}:#{in_docker_path}"
101
+ end
102
+ end
103
+
104
+ def docker_compose_extend
105
+ @docker_compose_extend ||= context.docker_compose_extend
106
+ end
107
+
108
+ def working_dir_name
109
+ Matrixeval.working_dir.basename
110
+ end
111
+
112
+ def project_name
113
+ Config.project_name.gsub(/[^A-Za-z0-9-]/,'_').downcase
114
+ end
115
+
116
+ def target
117
+ Config.target
118
+ end
119
+
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,65 @@
1
+ require_relative "./docker_compose/file"
2
+
3
+ module Matrixeval
4
+ class DockerCompose
5
+
6
+ attr_reader :context
7
+
8
+ def initialize(context)
9
+ @context = context
10
+ end
11
+
12
+ def run(arguments)
13
+ forward_arguments = arguments.map do |arg|
14
+ arg.match(/\s/) ? "\"#{arg}\"" : arg
15
+ end.join(" ")
16
+
17
+ no_tty = %w[bash sh zsh dash].include?(arguments[0]) ? '' : '--no-TTY'
18
+
19
+ system(
20
+ <<~DOCKER_COMPOSE_COMMAND
21
+ #{docker_compose} \
22
+ run --rm \
23
+ #{no_tty} \
24
+ #{context.docker_compose_service_name} \
25
+ #{forward_arguments}
26
+ DOCKER_COMPOSE_COMMAND
27
+ )
28
+ ensure
29
+ stop_containers
30
+ clean_containers_and_anonymous_volumes
31
+ turn_on_stty_opost
32
+ end
33
+
34
+ private
35
+
36
+ def stop_containers
37
+ system("#{docker_compose} stop >> /dev/null 2>&1")
38
+ end
39
+
40
+ def clean_containers_and_anonymous_volumes
41
+ system("#{docker_compose} rm -v -f >> /dev/null 2>&1")
42
+ end
43
+
44
+ def docker_compose
45
+ <<~DOCKER_COMPOSE_COMMAND.strip
46
+ docker --log-level error compose \
47
+ -f #{yaml_file} \
48
+ -p matrixeval-#{project_name}-#{context.id}
49
+ DOCKER_COMPOSE_COMMAND
50
+ end
51
+
52
+ def yaml_file
53
+ ".matrixeval/docker-compose/#{context.id}.yml"
54
+ end
55
+
56
+ def turn_on_stty_opost
57
+ system("stty opost")
58
+ end
59
+
60
+ def project_name
61
+ Config.project_name.gsub(/[^A-Za-z0-9-]/,'_').downcase
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ module Matrixeval
2
+ class ExtraMountFiles
3
+ class << self
4
+
5
+ def create
6
+ Config.all_mounts.each do |mount|
7
+ local_path, _ = mount.split(':')
8
+ next mount if Pathname.new(local_path).absolute?
9
+
10
+ local_path = Matrixeval.working_dir.join(local_path)
11
+ next if local_path.extname.empty?
12
+ next if local_path.ascend.none? { |path| path == Matrixeval.working_dir }
13
+
14
+ FileUtils.mkdir_p local_path.dirname
15
+ FileUtils.touch local_path
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ module Matrixeval
2
+ class Gitignore
3
+ class << self
4
+
5
+ def update
6
+ ignore_paths.map do |path|
7
+ ignore(path)
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def ignore_paths
14
+ [docker_compose] + Config.target.gitignore_paths
15
+ end
16
+
17
+ def docker_compose
18
+ ".matrixeval/docker-compose"
19
+ end
20
+
21
+ def ignore(path)
22
+ return if ignored?(path)
23
+
24
+ File.open(gitignore_path, 'a+') do |file|
25
+ file.puts path
26
+ end
27
+ end
28
+
29
+ def ignored?(path)
30
+ File.exist?(gitignore_path) &&
31
+ File.read(gitignore_path).include?(path)
32
+ end
33
+
34
+ def gitignore_path
35
+ Matrixeval.working_dir.join(".gitignore")
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,205 @@
1
+ require 'rainbow'
2
+ require "concurrent/utility/processor_counter"
3
+ require 'terminal-table'
4
+
5
+ module Matrixeval
6
+ class Runner
7
+ class << self
8
+ def start(argv)
9
+ new(argv).start
10
+ end
11
+ end
12
+
13
+ attr_reader :argv, :command
14
+
15
+ def initialize(argv)
16
+ @argv = argv
17
+ @command = CommandLine.new(argv)
18
+ @threads ||= []
19
+ @matrixeval_results ||= []
20
+ end
21
+
22
+ def start
23
+ load_plugin
24
+
25
+ validates
26
+
27
+ if command.init?
28
+ init
29
+ elsif command.all?
30
+ run_all_contexts
31
+ else
32
+ run_a_specific_context
33
+ end
34
+ rescue OptionParser::InvalidOption => e
35
+ puts <<~ERROR
36
+ #{e.message}
37
+ See 'matrixeval --help'
38
+ ERROR
39
+ exit
40
+ rescue Config::YAML::MissingError
41
+ puts "Please run 'matrixeval init' first to generate matrixeval.yml"
42
+ puts "See 'matrixeval init -h'"
43
+ exit
44
+ ensure
45
+ turn_on_stty_opost
46
+ end
47
+
48
+ private
49
+
50
+ def validates
51
+ return if command.valid?
52
+
53
+ puts <<~ERROR
54
+ matrixeval: '#{argv.join(' ')}' is not a MatrixEval command.
55
+ See 'matrixeval --help'
56
+ ERROR
57
+ exit
58
+ end
59
+
60
+ def init
61
+ Config::YAML.create_for(command.init_options[:target])
62
+ Gitignore.update
63
+ end
64
+
65
+ def run_all_contexts
66
+ DockerCompose::File.create_all
67
+ Gitignore.update
68
+ ExtraMountFiles.create
69
+ Config.target.create_files
70
+
71
+ pull_all_images
72
+
73
+ if workers_count == 1
74
+ run_all_contexts_sequentially
75
+ else
76
+ run_all_contexts_in_parallel
77
+ end
78
+ end
79
+
80
+ def run_all_contexts_sequentially
81
+ Context.all.each do |context|
82
+ puts Rainbow("[ MatrixEval ] ").blue.bright + Rainbow(" #{context.name} ").white.bright.bg(:blue)
83
+ puts Rainbow("[ MatrixEval ] Run \"#{command.rest_arguments.join(" ")}\"").blue.bright
84
+
85
+ docker_compose = DockerCompose.new(context)
86
+ success = docker_compose.run(command.rest_arguments)
87
+
88
+ @matrixeval_results << [context, !!success]
89
+ end
90
+
91
+ report
92
+ end
93
+
94
+ def run_all_contexts_in_parallel
95
+ parallel(contexts) do |sub_contexts|
96
+ Thread.current[:matrixeval_results] = []
97
+
98
+ sub_contexts.each do |context|
99
+ docker_compose = DockerCompose.new(context)
100
+ success = docker_compose.run(command.rest_arguments)
101
+
102
+ Thread.current[:matrixeval_results] << [context, !!success]
103
+ end
104
+ end
105
+
106
+ report
107
+ end
108
+
109
+ def run_a_specific_context
110
+ DockerCompose::File.create_all
111
+ Gitignore.update
112
+ ExtraMountFiles.create
113
+ Config.target.create_files
114
+
115
+ context = Context.find_by_command_options!(command.context_options)
116
+
117
+ puts Rainbow("[ MatrixEval ] ").blue.bright + Rainbow(" #{context.name} ").white.bright.bg(:blue)
118
+ puts Rainbow("[ MatrixEval ] Run \"#{command.rest_arguments.join(" ")}\"").blue.bright
119
+
120
+ docker_compose = DockerCompose.new(context)
121
+ docker_compose.run(command.rest_arguments)
122
+ end
123
+
124
+ def pull_all_images
125
+ parallel(Config.main_vector_variants) do |sub_variants|
126
+ sub_variants.each do |variant|
127
+ puts "Docker image check/pull #{variant.container.image}"
128
+ image_exists = system %Q{[ -n "$(docker images -q #{variant.container.image})" ]}
129
+ next if image_exists
130
+
131
+ system "docker pull #{variant.container.image}"
132
+ end
133
+ end
134
+ end
135
+
136
+ def report
137
+ turn_on_stty_opost
138
+
139
+ table = Terminal::Table.new(title: Rainbow("MatrixEval").blue.bright + " Summary", alignment: :center) do |table|
140
+
141
+ headers = Config.vectors.map(&:key) + ['result']
142
+ table.add_row headers.map { |value| { value: value, alignment: :center } }
143
+ table.add_separator
144
+
145
+ @matrixeval_results.each do |context, success|
146
+ success_cell = [success ? Rainbow('Success').green : Rainbow('Failed').red]
147
+ row = (context.variants.map(&:key) + success_cell).map do |value|
148
+ { value: value, alignment: :center }
149
+ end
150
+
151
+ table.add_row row
152
+ end
153
+
154
+ end
155
+
156
+ puts table
157
+ end
158
+
159
+ def parallel(collection)
160
+ @threads = [] unless @threads.empty?
161
+ @matrixeval_results = [] unless @matrixeval_results.empty?
162
+
163
+ collection.each_slice(per_worker_contexts_count) do |sub_collection|
164
+ @threads << Thread.new do
165
+ yield sub_collection
166
+ end
167
+ end
168
+
169
+ @threads.each(&:join)
170
+
171
+ @threads.each do |thread|
172
+ @matrixeval_results += (thread[:matrixeval_results] || [])
173
+ end
174
+ end
175
+
176
+
177
+ def per_worker_contexts_count
178
+ [(contexts.count / workers_count), 1].max
179
+ end
180
+
181
+ def contexts
182
+ @contexts ||= Context.all
183
+ end
184
+
185
+ def workers_count
186
+ count = if Config.parallel_workers == "number_of_processors"
187
+ Concurrent.physical_processor_count
188
+ else
189
+ Integer(Config.parallel_workers)
190
+ end
191
+
192
+ [count, 1].max
193
+ end
194
+
195
+ def turn_on_stty_opost
196
+ system("stty opost")
197
+ end
198
+
199
+ def load_plugin
200
+ require "matrixeval/#{Config.target_name}"
201
+ rescue LoadError, Config::YAML::MissingError
202
+ end
203
+
204
+ end
205
+ end
@@ -0,0 +1,45 @@
1
+ module Matrixeval
2
+ class Target
3
+
4
+ def version
5
+ Matrixeval::VERSION
6
+ end
7
+
8
+ def matrixeval_yml_template_path
9
+ Matrixeval.root.join("lib/matrixeval/templates/matrixeval.yml")
10
+ end
11
+
12
+ def vector_key
13
+ nil
14
+ end
15
+
16
+ def env(context)
17
+ {}
18
+ end
19
+
20
+ def mounts(context)
21
+ []
22
+ end
23
+
24
+ def volumes(context)
25
+ {}
26
+ end
27
+
28
+ def gitignore_paths
29
+ []
30
+ end
31
+
32
+ def support_commands
33
+ []
34
+ end
35
+
36
+ def cli_example_lines
37
+ []
38
+ end
39
+
40
+ def create_files
41
+ # Do nothing
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,70 @@
1
+ version: 0.4
2
+ project_name: REPLACE_ME
3
+ parallel_workers: number_of_processors
4
+ # commands:
5
+ # - ps
6
+ # - top
7
+ # - an_additional_command
8
+ # mounts:
9
+ # - /a/path/need/to/mount:/a/path/mount/to
10
+ matrix:
11
+ ruby:
12
+ main: true
13
+ variants:
14
+ - key: 2.7
15
+ container:
16
+ image: ruby:2.7.1
17
+ - key: 3.0
18
+ default: true
19
+ container:
20
+ image: ruby:3.0.0
21
+ - key: 3.1
22
+ container:
23
+ image: ruby:3.1.0
24
+ # - key: jruby-9.3
25
+ # container:
26
+ # image: jruby:9.3
27
+ # env:
28
+ # PATH: "/opt/jruby/bin:/app/bin:/bundle/bin:$PATH"
29
+ # mounts:
30
+ # - /a/path/need/to/mount:/a/path/mount/to
31
+
32
+ # rails:
33
+ # variants:
34
+ # - key: 6.1
35
+ # default: true
36
+ # env:
37
+ # RAILS_VERSION: "~> 6.1.0"
38
+ # - key: 7.0
39
+ # env:
40
+ # RAILS_VERSION: "~> 7.0.0"
41
+ # another:
42
+ # variants:
43
+ # - key: key1
44
+ # default: true
45
+ # env:
46
+ # ENV_KEY: 1
47
+ # - key: key2
48
+ # env:
49
+ # ENV_KEY: 2
50
+
51
+ exclude:
52
+ # - ruby: 3.0
53
+ # rails: 4.2
54
+ # - ruby: jruby-9.3
55
+ # rails: 7.0
56
+
57
+ docker-compose-extend:
58
+ # services:
59
+ # postgres:
60
+ # image: postgres:12.8
61
+ # volumes:
62
+ # - postgres12:/var/lib/postgresql/data
63
+ # environment:
64
+ # POSTGRES_HOST_AUTH_METHOD: trust
65
+
66
+ # redis:
67
+ # image: redis:6.2-alpine
68
+
69
+ # volumes:
70
+ # postgres12: