matrixeval 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: