fastlane-plugin-flutter_tests 0.1.1 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c6b440f953fc91d3c8c62244e2fa370a5627be5bd10fbb1b2b869b51d436173
4
- data.tar.gz: 2be21cbbb17644f0389b410b28466c968a8f231d7374c1b5a927907d0ac40435
3
+ metadata.gz: 42e082a14be1e8489bfcc34fc32ca6863d3d8a4d8d9595d1d2ca52020d600c31
4
+ data.tar.gz: 2d2987def7370d09cd0f044c8379b18fca146ae7335099604ebc398484307955
5
5
  SHA512:
6
- metadata.gz: 21f033edbc4738a67cf7348290b3e984ed47c93af3bafa42220a9a0d323e86e2b293fe95d5c2445135d98131a71522dd41c9aa262447349f565ba66099e9cdf3
7
- data.tar.gz: c3f07e418887a032153c0a49419a185346c8580f0b5ee9df8b8a5f6ef64185d789e6d45f3e423988956ede7eb6bb849170597b612edf15d0b39dd5d17abaad0c
6
+ metadata.gz: 167e72db449d86ae77abb6c9cef671fe922967e57a94b9f9b34aea45d19adc3d3a8386b56938d27e75c4b2fd770a6d811dbdd623c62b93efeccb2fd9743e127a
7
+ data.tar.gz: 1a1c09c0272c465312f03dd25c5bc6448072d3217ce08ffbe6ddc9c882ba18feb783269bfe1e93b54533563a445cd8740e3e937e2dcf51365d977b7077d98e8d
@@ -1,185 +1,36 @@
1
1
  require 'fastlane/action'
2
- require_relative '../helper/flutter_tests_helper'
2
+ require_relative '../helper/flutter_unit_test_helper'
3
+ require_relative '../helper/flutter_integration_test_helper'
3
4
  require 'open3'
4
5
  require 'json'
6
+ require_relative '../model/test_item'
5
7
 
6
8
  module Fastlane
7
9
  module Actions
8
10
  class FlutterTestsAction < Action
9
- # Class that represents a single test that has been run
10
- class Test
11
- def initialize(id, name)
12
- @test_id = id
13
- @test_name = name
14
- @test_done = false
15
- @test_successful = ''
16
- @test_error = nil
17
- @test_stacktrace = nil
18
- @test_was_skipped = false
19
- @test_was_printed = false
20
- end
21
-
22
- def mark_as_done(success, skipped, error, stacktrace)
23
- @test_done = true
24
- @test_successful = success
25
- @test_was_skipped = skipped
26
- @test_error = error
27
- unless stacktrace.nil?
28
- stacktrace = stacktrace.gsub(/ {2,}/, "\n")
29
- @test_stacktrace = stacktrace
30
- end
31
- end
32
-
33
- def get_name
34
- @test_name
35
- end
36
-
37
- def get_id
38
- @test_id
39
- end
40
-
41
- def can_print
42
- !@test_was_printed
43
- end
44
-
45
- def get_status
46
- if @test_was_skipped
47
- 'skipped'
48
- else
49
- @test_successful
50
- end
51
- end
52
-
53
- def _generate_message
54
- tag = @test_was_skipped ? 'skipped' : @test_successful
55
-
56
- default_message = "[#{tag}] #{@test_name}"
57
- if @test_successful != 'success'
58
- default_message += "\n[ERROR] -> #{@test_error}\n[STACKTRACE]\n#{@test_stacktrace}"
59
- end
60
-
61
- if %w[success error].include?(@test_successful) || @test_was_skipped
62
- color = if @test_was_skipped
63
- 34 # Skipped tests are displayed in blue
64
- else
65
- # Successful tests are in green and the failed in red
66
- @test_successful == 'success' ? 32 : 31
67
- end
68
-
69
- "\e[#{color}m#{default_message}\e[0m"
70
- else
71
- default_message
72
- end
73
- end
74
-
75
- def print
76
- UI.message(_generate_message)
77
- @test_was_printed = true
78
- end
79
- end
80
-
81
- class TestRunner
82
- def initialize
83
- @launched_tests = Hash.new { |hash, key| hash[key] = nil }
84
- end
85
-
86
- # Wraps the message to color it
87
- #
88
- # @param message [String] the message that has to be wrapped
89
- # @param color [Integer] the color of the message (34 -> blue, 32 -> green, 31 -> red)
90
- def _colorize(message, color)
91
- "\e[#{color}m#{message}\e[0m"
11
+ def self.run(params)
12
+ test_type = params[:test_type]
13
+ if %w[all unit].include? test_type
14
+ Helper::FlutterUnitTestHelper.new.run(params[:flutter_command], params[:print_only_failed], params[:print_stats])
92
15
  end
16
+ if %w[all integration].include? test_type
93
17
 
94
- # Launches all the unit tests contained in the project
95
- # folder
96
- def run(flutter_command, print_only_failed, print_stats)
97
- Open3.popen3("#{flutter_command} test --machine") do |stdin, stdout, stderr, thread|
98
- stdout.each_line do |line|
99
- parse_json_output(line, print_only_failed)
100
- end
18
+ if params[:driver_path].nil? || params[:integration_tests].nil?
19
+ UI.user_error!("If launching integration tests, 'driver_path' and 'integration_tests' parameters must be inserted")
20
+ exit(1)
101
21
  end
102
22
 
103
- if print_stats
104
- stats = Hash.new { |hash, key| hash[key] = 0 }
105
- @launched_tests.values.each do |item|
106
- unless item.nil?
107
- stats[item.get_status] += 1
108
- end
109
- end
110
-
111
- skipped_tests = stats['skipped'].nil? ? 0 : stats['skipped']
112
- failed_tests = stats['error'].nil? ? 0 : stats['error']
113
- successful_tests = stats['success'].nil? ? 0 : stats['success']
114
- table = [
115
- %w[Successful Failed Skipped],
116
- [successful_tests, failed_tests, skipped_tests]
117
- ]
118
-
119
- messages = ["Ran #{@launched_tests.values.count { |e| !e.nil? }} tests"]
120
- colors = { 0 => 32, 1 => 31, 2 => 34 }
121
- max_length = 0
122
- (0..2).each do |i|
123
- msg = "#{table[0][i]}:\t#{table[1][i]}"
124
- max_length = [max_length, msg.length].max
125
- messages.append(_colorize(msg, colors[i]))
126
- end
23
+ platform = params[:platform]
127
24
 
128
- UI.message('-' * max_length)
129
- messages.each { |m| UI.message(m) }
130
- UI.message('-' * max_length)
25
+ if params[:reuse_build] && platform != 'android'
26
+ UI.error("If 'reuse_build' is set to true, the platform must be android")
27
+ platform = 'android'
131
28
  end
132
- end
133
29
 
134
- # Parses the json output given by [self.run]
135
- def parse_json_output(line, print_only_failed)
136
- unless line.to_s.strip.empty?
137
- output = JSON.parse(line)
138
- unless output.kind_of?(Array)
139
- type = output['type']
140
- case type
141
- when 'testStart'
142
- id = output['test']['id']
143
- name = output['test']['name']
144
- if name.include?('loading')
145
- return
146
- end
147
-
148
- test_item = Test.new(id, name)
149
- @launched_tests[test_item.get_id] = test_item
150
- when 'testDone'
151
- test_id = output['testID']
152
- test_item = @launched_tests[test_id]
153
- if !test_item.nil? && test_item.can_print
154
- was_skipped = output['skipped']
155
- test_item.mark_as_done(output['result'], was_skipped, nil, nil)
156
- if was_skipped || !print_only_failed
157
- test_item.print
158
- end
159
- end
160
- when 'error'
161
- test_id = output['testID']
162
- test_item = @launched_tests[test_id]
163
- if !test_item.nil? && test_item.can_print
164
- test_item.mark_as_done('error', false, output['error'], output['stackTrace'])
165
- test_item.print
166
- end
167
- else
168
- # ignored
169
- end
170
- end
171
- end
172
- rescue StandardError => e
173
- UI.error("Got error during parse_json: #{e.message}")
174
- UI.error(e.backtrace.join('\n'))
175
- exit(1)
30
+ Helper::FlutterIntegrationTestHelper.new(params[:driver_path], params[:integration_tests], params[:flutter_command]).run(platform, params[:force_launch], params[:reuse_build])
176
31
  end
177
32
  end
178
33
 
179
- def self.run(params)
180
- TestRunner.new.run(params[:flutter_command], params[:print_only_failed], params[:print_stats])
181
- end
182
-
183
34
  def self.description
184
35
  "Extension that helps to run flutter tests"
185
36
  end
@@ -220,6 +71,57 @@ module Fastlane
220
71
  optional: false,
221
72
  type: Boolean
222
73
  ),
74
+ FastlaneCore::ConfigItem.new(
75
+ key: :test_type,
76
+ default_value: 'all',
77
+ description: "Specifies which tests should be run. Accepted values",
78
+ verify_block: proc do |value|
79
+ UI.user_error!("Wrong value, #{value} not accepted. Should be 'unit','integration' or 'all'.") unless %w[unit integration all].include? value
80
+ end,
81
+ optional: false,
82
+ type: String,
83
+ ),
84
+ FastlaneCore::ConfigItem.new(
85
+ key: :driver_path,
86
+ description: "Specifies the path of the driver file",
87
+ optional: true,
88
+ type: String,
89
+ verify_block: proc do |value|
90
+ UI.user_error!("Driver file doesn't exists") unless File.file?(value)
91
+ end
92
+ ),
93
+ FastlaneCore::ConfigItem.new(
94
+ key: :integration_tests,
95
+ description: "Specifies the path of the folder containing all the integration tests",
96
+ optional: true,
97
+ type: String,
98
+ verify_block: proc do |value|
99
+ UI.user_error!("Integration test folder doesn't exists") unless File.exist?(value) && File.directory?(value)
100
+ end
101
+ ),
102
+ FastlaneCore::ConfigItem.new(
103
+ key: :platform,
104
+ description: "Specifies the os on which the tests should run on",
105
+ optional: false,
106
+ type: String,
107
+ verify_block: proc do |value|
108
+ UI.user_error!("Platform #{value} is not supported") unless %w[android ios].include? value.to_s.downcase
109
+ end
110
+ ),
111
+ FastlaneCore::ConfigItem.new(
112
+ key: :force_launch,
113
+ description: "If true, the plugin will try to launch an emulator in case it's not already running",
114
+ optional: false,
115
+ type: Boolean,
116
+ default_value: true,
117
+ ),
118
+ FastlaneCore::ConfigItem.new(
119
+ key: :reuse_build,
120
+ description: "If true, builds the app only for the first time then the other integration tests are run on the same build (much faster)",
121
+ default_value: false,
122
+ optional: false,
123
+ type: Boolean,
124
+ )
223
125
  ]
224
126
  end
225
127
 
@@ -0,0 +1,136 @@
1
+ require 'fastlane/action'
2
+ require 'open3'
3
+
4
+ module Fastlane
5
+ module Helper
6
+ class FlutterIntegrationTestHelper
7
+ # Initialize the helper that launches the integration tests
8
+ #
9
+ # @param driver [String] the path to the file that will be used as driver
10
+ # @param test_folder [String] the path to the folder that contains the
11
+ # @param flutter_command [String] the command to launch flutter
12
+ # integration tests
13
+ def initialize(driver, test_folder, flutter_command)
14
+ @driver = driver
15
+ @integration_tests = _load_files(test_folder)
16
+ @flutter_command = flutter_command
17
+ end
18
+
19
+ # Loads all the integration test files
20
+ #
21
+ # @param test_folder [String] the path that contains the test files
22
+ # @return [Array] An array containing all the paths to the files found
23
+ def _load_files(test_folder)
24
+ test_files = Dir.glob("#{test_folder}/**/*").reject do |f|
25
+ File.directory?(f) || !f.end_with?('_test.dart')
26
+ end
27
+ UI.message("Found #{test_files.length} test files")
28
+ test_files
29
+ end
30
+
31
+ # Launches the tests sequentially
32
+ #
33
+ # @param platform [String] Specifies on which platform the tests should be run
34
+ # @param force_launch [Boolean] If it's true and there aren't any devices ready, the plugin will try to start one for the given platform
35
+ # @param reuse_build [Boolean] If it's true, it will run the build only for the first integration test
36
+ def run(platform, force_launch, reuse_build)
37
+ UI.message("Checking for running devices")
38
+ device_id = _run_test_device(platform, force_launch)
39
+ if !device_id.nil?
40
+ _launch_tests(device_id, reuse_build)
41
+ else
42
+ UI.error("Failed to find a device to launch the tests on")
43
+ exit(1)
44
+ end
45
+ end
46
+
47
+ # Executes the tests found on the device_id
48
+ #
49
+ # @param device_id [String] the id of the device previously found
50
+ # @param reuse_build [Boolean] If it's true, it will run the build only for the first integration test
51
+ def _launch_tests(device_id, reuse_build)
52
+ apk_path = nil
53
+ if reuse_build
54
+ UI.message("Building apk")
55
+ out, err, status = Open3.capture3("#{@flutter_command} build apk")
56
+ if _get_exit_code(status) != '0'
57
+ UI.error("Failed to build apk")
58
+ puts err
59
+ exit(1)
60
+ else
61
+ apk_path = _get_apk_path(out)
62
+ if !apk_path.nil? && File.file?(apk_path)
63
+ UI.message("Build apk at path #{apk_path}")
64
+ #TODO
65
+ else
66
+ UI.error("Apk path not found or it's not accessible")
67
+ exit(1)
68
+ end
69
+
70
+ end
71
+ end
72
+
73
+ count = 0
74
+ @integration_tests.each do |test|
75
+ UI.message("Launching test #{count}/#{@integration_tests.length}: #{test.split("/").last}")
76
+ _, __, status = Open3.capture3("#{@flutter_command} drive --target #{@driver} --driver #{test} -d #{device_id} #{reuse_build ? "--use-application-binary #{apk_path}" : ''}")
77
+ UI.message("Test #{count} ended with code '#{_get_exit_code(status)}'")
78
+ count += 1
79
+ end
80
+ end
81
+
82
+ # Returns the exit code of a process
83
+ #
84
+ # @param exit_status [String] status given back by [Open3]
85
+ # @return The exit code (0|1) as string
86
+ def _get_exit_code(exit_status)
87
+ exit_status.to_s.split(' ').last
88
+ end
89
+
90
+ # Parse the flutter build output looking for a .apk path
91
+ #
92
+ # @param message [String] the stdout of flutter build process
93
+ # @return the path to the apk that has been built
94
+ def _get_apk_path(message)
95
+ components = message.split(/\n/).last.split(' ')
96
+ if components.any? { |line| line.end_with? '.apk' }
97
+ components.detect { |c| c.end_with? '.apk' }
98
+ else
99
+ UI.warn('Apk path not found in the stdout')
100
+ nil
101
+ end
102
+ end
103
+
104
+ # Checks if there's a device running and gets its id
105
+ # @param platform [String] Specifies the type of device that should be found
106
+ # @param force_launch [Boolean] If it's true and there aren't any devices ready, the plugin will try to start one for the given platform
107
+ # @return The deviceId if the device exists or [nil]
108
+ def _run_test_device(platform, force_launch)
109
+ out, _ = Open3.capture2("#{@flutter_command} devices | grep #{platform}")
110
+ device_id = nil
111
+ if out.to_s.strip.empty? && force_launch
112
+ out, _ = Open3.capture2("#{@flutter_command} emulators | grep #{platform}")
113
+ if out.to_s.strip.empty?
114
+ UI.error("No emulators found for platform #{platform}")
115
+ exit(1)
116
+ end
117
+
118
+ emulator_id = out.to_s.split('•')[0]
119
+ Open3.capture2("#{@flutter_command} emulators --launch #{emulator_id}")
120
+
121
+ out, _ = Open3.capture2("#{@flutter_command} devices | grep #{platform}")
122
+ else
123
+ device_id = (out.to_s.split("•")[1]).strip
124
+ UI.message("Found already running device: #{device_id}")
125
+ end
126
+
127
+ unless out.to_s.strip.empty?
128
+ device_id = (out.to_s.split("•")[1]).strip
129
+ UI.message("Got device id #{device_id}")
130
+ end
131
+
132
+ device_id.nil? ? nil : device_id
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,114 @@
1
+ require 'fastlane/action'
2
+
3
+ module Fastlane
4
+
5
+ module Helper
6
+ class FlutterUnitTestHelper
7
+ def initialize
8
+ @launched_tests = Hash.new { |hash, key| hash[key] = nil }
9
+ end
10
+
11
+ # Wraps the message to color it
12
+ #
13
+ # @param message [String] the message that has to be wrapped
14
+ # @param color [Integer] the color of the message (34 -> blue, 32 -> green, 31 -> red)
15
+ #
16
+ # @return [String] the colorized message ready to be printed
17
+ def _colorize(message, color)
18
+ "\e[#{color}m#{message}\e[0m"
19
+ end
20
+
21
+ # Launches all the unit tests contained in the project
22
+ # folder
23
+ #
24
+ # @param flutter_command [String] Contains the command to launch flutter
25
+ # @param print_only_failed [Boolean] If true, prints only skipped and failed tests
26
+ # @param print_stats [Boolean] If true, it prints a table containing the info about
27
+ # the launched tests
28
+ def run(flutter_command, print_only_failed, print_stats)
29
+ Open3.popen3("#{flutter_command} test --machine") do |stdin, stdout, stderr, thread|
30
+ stdout.each_line do |line|
31
+ parse_json_output(line, print_only_failed)
32
+ end
33
+ end
34
+
35
+ if print_stats
36
+ stats = Hash.new { |hash, key| hash[key] = 0 }
37
+ @launched_tests.values.each do |item|
38
+ unless item.nil?
39
+ stats[item.get_status] += 1
40
+ end
41
+ end
42
+
43
+ skipped_tests = stats['skipped'].nil? ? 0 : stats['skipped']
44
+ failed_tests = stats['error'].nil? ? 0 : stats['error']
45
+ successful_tests = stats['success'].nil? ? 0 : stats['success']
46
+ table = [
47
+ %w[Successful Failed Skipped],
48
+ [successful_tests, failed_tests, skipped_tests]
49
+ ]
50
+
51
+ messages = ["Ran #{@launched_tests.values.count { |e| !e.nil? }} tests"]
52
+ colors = { 0 => 32, 1 => 31, 2 => 34 }
53
+ max_length = 0
54
+ (0..2).each do |i|
55
+ msg = "#{table[0][i]}:\t#{table[1][i]}"
56
+ max_length = [max_length, msg.length].max
57
+ messages.append(_colorize(msg, colors[i]))
58
+ end
59
+
60
+ UI.message('-' * max_length)
61
+ messages.each { |m| UI.message(m) }
62
+ UI.message('-' * max_length)
63
+ end
64
+ end
65
+
66
+ # Parses the json output given by [self.run]
67
+ #
68
+ # @param line [String] The json as string that has to be parsed
69
+ # @param print_only_failed [Boolean] See definition on run
70
+ def parse_json_output(line, print_only_failed)
71
+ unless line.to_s.strip.empty?
72
+ output = JSON.parse(line)
73
+ unless output.kind_of?(Array)
74
+ type = output['type']
75
+ case type
76
+ when 'testStart'
77
+ id = output['test']['id']
78
+ name = output['test']['name']
79
+ if name.include?('loading')
80
+ return
81
+ end
82
+
83
+ test_item = Test.new(id, name)
84
+ @launched_tests[test_item.get_id] = test_item
85
+ when 'testDone'
86
+ test_id = output['testID']
87
+ test_item = @launched_tests[test_id]
88
+ if !test_item.nil? && test_item.can_print
89
+ was_skipped = output['skipped']
90
+ test_item.mark_as_done(output['result'], was_skipped, nil, nil)
91
+ if was_skipped || !print_only_failed
92
+ UI.message(test_item.generate_message)
93
+ end
94
+ end
95
+ when 'error'
96
+ test_id = output['testID']
97
+ test_item = @launched_tests[test_id]
98
+ if !test_item.nil? && test_item.can_print
99
+ test_item.mark_as_done('error', false, output['error'], output['stackTrace'])
100
+ UI.message(test_item.generate_message)
101
+ end
102
+ else
103
+ # ignored
104
+ end
105
+ end
106
+ end
107
+ rescue StandardError => e
108
+ UI.error("Got error during parse_json: #{e.message}")
109
+ UI.error(e.backtrace.join('\n'))
110
+ exit(1)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,71 @@
1
+ # Class that represents a single test that has been run
2
+ class Test
3
+ def initialize(id, name)
4
+ @test_id = id
5
+ @test_name = name
6
+ @test_done = false
7
+ @test_successful = ''
8
+ @test_error = nil
9
+ @test_stacktrace = nil
10
+ @test_was_skipped = false
11
+ @test_was_printed = false
12
+ end
13
+
14
+ def mark_as_done(success, skipped, error, stacktrace)
15
+ @test_done = true
16
+ @test_successful = success
17
+ @test_was_skipped = skipped
18
+ @test_error = error
19
+ unless stacktrace.nil?
20
+ stacktrace = stacktrace.gsub(/ {2,}/, "\n")
21
+ @test_stacktrace = stacktrace
22
+ end
23
+ end
24
+
25
+ def get_name
26
+ @test_name
27
+ end
28
+
29
+ def get_id
30
+ @test_id
31
+ end
32
+
33
+ def can_print
34
+ !@test_was_printed
35
+ end
36
+
37
+ def get_status
38
+ if @test_was_skipped
39
+ 'skipped'
40
+ else
41
+ @test_successful
42
+ end
43
+ end
44
+
45
+ # Generates a loggable message for the given test
46
+ #
47
+ # @return message [String] the message to print
48
+ def generate_message
49
+ @test_was_printed = true
50
+ tag = @test_was_skipped ? 'skipped' : @test_successful
51
+
52
+ default_message = "[#{tag}] #{@test_name}"
53
+ if @test_successful != 'success'
54
+ default_message += "\n[ERROR] -> #{@test_error}\n[STACKTRACE]\n#{@test_stacktrace}"
55
+ end
56
+
57
+ if %w[success error].include?(@test_successful) || @test_was_skipped
58
+ color = if @test_was_skipped
59
+ 34 # Skipped tests are displayed in blue
60
+ else
61
+ # Successful tests are in green and the failed in red
62
+ @test_successful == 'success' ? 32 : 31
63
+ end
64
+
65
+ "\e[#{color}m#{default_message}\e[0m"
66
+ else
67
+ default_message
68
+ end
69
+ end
70
+
71
+ end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module FlutterTests
3
- VERSION = "0.1.1"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-flutter_tests
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - smaso
@@ -160,7 +160,9 @@ files:
160
160
  - README.md
161
161
  - lib/fastlane/plugin/flutter_tests.rb
162
162
  - lib/fastlane/plugin/flutter_tests/actions/flutter_tests_action.rb
163
- - lib/fastlane/plugin/flutter_tests/helper/flutter_tests_helper.rb
163
+ - lib/fastlane/plugin/flutter_tests/helper/flutter_integration_test_helper.rb
164
+ - lib/fastlane/plugin/flutter_tests/helper/flutter_unit_test_helper.rb
165
+ - lib/fastlane/plugin/flutter_tests/model/test_item.rb
164
166
  - lib/fastlane/plugin/flutter_tests/version.rb
165
167
  homepage: https://github.com/smsimone/fastlane_flutter_tests.git
166
168
  licenses:
@@ -1,16 +0,0 @@
1
- require 'fastlane_core/ui/ui'
2
-
3
- module Fastlane
4
- UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
5
-
6
- module Helper
7
- class FlutterTestsHelper
8
- # class methods that you define here become available in your action
9
- # as `Helper::FlutterTestsHelper.your_method`
10
- #
11
- def self.show_message
12
- UI.message("Hello from the flutter_tests plugin helper!")
13
- end
14
- end
15
- end
16
- end