matrixeval 0.1.0 → 0.4.0

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,207 @@
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
+ validates
24
+
25
+ if command.init?
26
+ init
27
+ elsif command.all?
28
+ run_all_contexts
29
+ else
30
+ run_a_specific_context
31
+ end
32
+ rescue OptionParser::InvalidOption => e
33
+ puts <<~ERROR
34
+ #{e.message}
35
+ See 'matrixeval --help'
36
+ ERROR
37
+ exit
38
+ rescue Config::YAML::MissingError
39
+ puts "Please run 'matrixeval init' first to generate matrixeval.yml"
40
+ puts "See 'matrixeval init -h'"
41
+ exit
42
+ ensure
43
+ turn_on_stty_opost
44
+ end
45
+
46
+ private
47
+
48
+ def validates
49
+ return if command.valid?
50
+
51
+ puts <<~ERROR
52
+ matrixeval: '#{argv.join(' ')}' is not a MatrixEval command.
53
+ See 'matrixeval --help'
54
+ ERROR
55
+ exit
56
+ end
57
+
58
+ def init
59
+ Config::YAML.create_for(command.init_options[:target])
60
+ Gitignore.update
61
+ end
62
+
63
+ def run_all_contexts
64
+ load_plugin
65
+
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
+ load_plugin
111
+
112
+ DockerCompose::File.create_all
113
+ Gitignore.update
114
+ ExtraMountFiles.create
115
+ Config.target.create_files
116
+
117
+ context = Context.find_by_command_options!(command.context_options)
118
+
119
+ puts Rainbow("[ MatrixEval ] ").blue.bright + Rainbow(" #{context.name} ").white.bright.bg(:blue)
120
+ puts Rainbow("[ MatrixEval ] Run \"#{command.rest_arguments.join(" ")}\"").blue.bright
121
+
122
+ docker_compose = DockerCompose.new(context)
123
+ docker_compose.run(command.rest_arguments)
124
+ end
125
+
126
+ def pull_all_images
127
+ parallel(Config.main_vector_variants) do |sub_variants|
128
+ sub_variants.each do |variant|
129
+ puts "Docker image check/pull #{variant.container.image}"
130
+ image_exists = system %Q{[ -n "$(docker images -q #{variant.container.image})" ]}
131
+ next if image_exists
132
+
133
+ system "docker pull #{variant.container.image}"
134
+ end
135
+ end
136
+ end
137
+
138
+ def report
139
+ turn_on_stty_opost
140
+
141
+ table = Terminal::Table.new(title: Rainbow("MatrixEval").blue.bright + " Summary", alignment: :center) do |table|
142
+
143
+ headers = Config.vectors.map(&:key) + ['result']
144
+ table.add_row headers.map { |value| { value: value, alignment: :center } }
145
+ table.add_separator
146
+
147
+ @matrixeval_results.each do |context, success|
148
+ success_cell = [success ? Rainbow('Success').green : Rainbow('Failed').red]
149
+ row = (context.variants.map(&:key) + success_cell).map do |value|
150
+ { value: value, alignment: :center }
151
+ end
152
+
153
+ table.add_row row
154
+ end
155
+
156
+ end
157
+
158
+ puts table
159
+ end
160
+
161
+ def parallel(collection)
162
+ @threads = [] unless @threads.empty?
163
+ @matrixeval_results = [] unless @matrixeval_results.empty?
164
+
165
+ collection.each_slice(per_worker_contexts_count) do |sub_collection|
166
+ @threads << Thread.new do
167
+ yield sub_collection
168
+ end
169
+ end
170
+
171
+ @threads.each(&:join)
172
+
173
+ @threads.each do |thread|
174
+ @matrixeval_results += (thread[:matrixeval_results] || [])
175
+ end
176
+ end
177
+
178
+
179
+ def per_worker_contexts_count
180
+ [(contexts.count / workers_count), 1].max
181
+ end
182
+
183
+ def contexts
184
+ @contexts ||= Context.all
185
+ end
186
+
187
+ def workers_count
188
+ count = if Config.parallel_workers == "number_of_processors"
189
+ Concurrent.physical_processor_count
190
+ else
191
+ Integer(Config.parallel_workers)
192
+ end
193
+
194
+ [count, 1].max
195
+ end
196
+
197
+ def turn_on_stty_opost
198
+ system("stty opost")
199
+ end
200
+
201
+ def load_plugin
202
+ require "matrixeval/#{Config.target_name}"
203
+ rescue LoadError
204
+ end
205
+
206
+ end
207
+ 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: