fastlane-plugin-mango 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75221491bc8b2b1f16e769b0208203efbd55ac56a5577a632e522b1caa5348ba
4
- data.tar.gz: facf07278586a34970216bf5bef7984bdd0e2cebb9854ae2f75e84d62693f310
3
+ metadata.gz: 968c3231a114d8f5b371c05d887cc9e9dcb908f7baa8729370e9ba97dfab4c2c
4
+ data.tar.gz: c0a3a9c50e99ca15806fbb253e10349f6cd263677cf6e45dad03078e04ebc584
5
5
  SHA512:
6
- metadata.gz: be523d5eaaf2475812c9aac88e1a82a8135b06649228d0c6684d3201064d4fceeaf28551dfcef541a5b820c8bb27d11bee76bfdb99e78a05600240585cf2c818
7
- data.tar.gz: f095125f165f15695d06fb6d11589c57a61be876fb54bf1f9201919289ac93532c1d1f0eafd74af29577f062ab6a210539ec9a2b60fd3b2e22662a57faa1a9b4
6
+ metadata.gz: db22d989c74376ec53583f7acb8254b4b01cabaff64a8742fa068b137bd023c1bc74dd80b3b0d8ebdde6f60e87b1e17324acf4968433239e1d3c5ac1023d4547
7
+ data.tar.gz: 7d87f2df5052ade2b4aa40eb3999e28b2fe6f30c9eaf0974681048042a0ac53a2270ccaab4579db8178e82ef6797ebe9f54a37efe3be30d02d5799c5c4c33729
data/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
 
2
2
  # Mango - Fastlane plugin
3
3
 
4
- [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-mango)
4
+ [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-mango) [![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/xing/mango/blob/master/LICENSE)
5
+ [![Gem](https://img.shields.io/gem/v/fastlane-plugin-mango.svg?style=flat)](http://rubygems.org/gems/fastlane-plugin-mango)
5
6
 
6
7
  A fastlane plugin that runs Android tasks on a specified [Docker](https://www.docker.com/) image
7
8
 
8
9
  <img src="assets/mango_logo.png" alt="Mango Logo" width="256px" height="256px"/>
9
10
 
10
- Running Android tests, especially [Espresso](https://developer.android.com/training/testing/espresso/) on a continuous integration environment like [Jenkins](https://jenkins.io/) can be a hassle. You need to boot, manage and destroy an [Android Virtual Device (AVD)](https://developer.android.com/studio/run/managing-avds) during the test run. This is why we, the mobile releases team at [XING](https://www.xing/com), built this plugin. It spins up a specified [Docker](https://www.docker.com/) image and runs a given task on it.
11
+ Running Android tests, especially [Espresso](https://developer.android.com/training/testing/espresso/) on a continuous integration environment like [Jenkins](https://jenkins.io/) can be a hassle. You need to boot, manage and destroy an [Android Virtual Device (AVD)](https://developer.android.com/studio/run/managing-avds) during the test run. This is why we, the mobile releases team at [XING](https://www.xing.com), built this plugin. It spins up a specified [Docker](https://www.docker.com/) image and runs a given task on it.
11
12
 
12
13
  Another requirement we had was to run on a clean environment, which is why Mango also helps us to run our unit test and more. For an example check out the [example `Fastfile`](sample-android/fastlane/Fastfile)
13
14
 
@@ -16,7 +17,7 @@ Another requirement we had was to run on a clean environment, which is why Mango
16
17
  In order to use this plugin you will need to to have [Docker](https://www.docker.com/) installed on the machine you are using.
17
18
  Documentation on how to set it up properly on a Linux (ubuntu) machine can be found [here](docs/docker-linux.md).
18
19
 
19
- If you need an Android Virtual Device (AVD) to run your tests (for example Espresso, Calabash or Appium), it's necessary to check that your CPU supports kvm virtualisation. We already experienced, that it doesn't fully work on macOS and are using Linux for that.
20
+ If you need an Android Virtual Device (AVD) to run your tests (for example Espresso, Calabash or Appium), it's necessary to check that your CPU supports kvm virtualisation. We already experienced that it doesn't fully work on macOS and are using Linux for that.
20
21
 
21
22
  ## Getting Started
22
23
 
@@ -30,16 +31,33 @@ fastlane add_plugin mango
30
31
 
31
32
  After installing this plugin you have access to one additional action (`mango`) in your `Fastfile`.
32
33
 
33
- So a lane in your `Fastfile` could look similar to this:
34
+ So a lane in your `Fastfile` could look similar to this for Espresso tests:
34
35
  ```ruby
35
36
  desc "Run espresso tests on docker images"
36
- lane :Espresso_Tests do |options|
37
- mango(
37
+ lane :Espresso_Tests do
38
+ run_dockerized_task(
38
39
  container_name: "espresso_container",
39
- docker_image: "thyrlian/android-sdk:latest",
40
+ port_factor: options[:port_factor],
41
+ docker_image: "joesss/mango-docker:latest",
40
42
  container_timeout: 120,
41
43
  android_task: "./gradlew connectedAndroidTest",
42
- post_actions: "adb logcat -d > logcat.txt"
44
+ post_actions: "adb logcat -d > logcat.txt",
45
+ pull_latest_image: true
46
+ )
47
+ end
48
+ ```
49
+
50
+ or to this for unit tests or other gradle tasks:
51
+ ```ruby
52
+ desc "Run unit tests on docker images"
53
+ lane :Unit_Tests do
54
+ run_dockerized_task(
55
+ container_name: "unit_tests_container",
56
+ port_factor: options[:port_factor],
57
+ docker_image: "joesss/mango-base:latest",
58
+ is_running_on_emulator: false,
59
+ android_task: "./gradlew testDebug",
60
+ pull_latest_image: true
43
61
  )
44
62
  end
45
63
  ```
@@ -48,8 +66,6 @@ Now you can call this new lane by calling `bundle exec fastlane Espresso_Tests`.
48
66
 
49
67
  The Plugin will start up the given `docker_image`, execute the given `android_task` and afterwards execute the `post_actions`.
50
68
 
51
- Of
52
-
53
69
  ## Configuration options
54
70
  The `mango` action has plenty of options to configure it.
55
71
 
@@ -1,11 +1,12 @@
1
+ require_relative '../helper/docker_commander'
2
+
1
3
  module Fastlane
2
4
  module Actions
3
5
  class RunDockerizedTaskAction < Action
4
6
  def self.run(params)
5
7
  UI.important("The mango plugin is working!")
6
- emulator_name = params[:emulator_name]
7
- docker_emulator = Fastlane::Helper::MangoHelper.new(params)
8
- docker_emulator.setup_container
8
+ mango_helper = Fastlane::Helper::MangoHelper.new(params)
9
+ mango_helper.setup_container
9
10
 
10
11
  failure_buffer_timeout = 5
11
12
  timeout_command = "timeout #{params[:maximal_run_time] - failure_buffer_timeout}m"
@@ -16,17 +17,17 @@ module Fastlane
16
17
  UI.success("Starting Android Task.")
17
18
  bundle_install = params[:bundle_install] ? '&& bundle install ' : ''
18
19
 
19
- docker_emulator.docker_exec("cd #{workspace_dir} #{bundle_install}&& #{timeout_command} #{android_task} || exit 1")
20
+ Helper::DockerCommander.docker_exec(command: "cd #{workspace_dir} #{bundle_install}&& #{timeout_command} #{android_task} || exit 1", container_name: mango_helper.container_name)
20
21
  end
21
22
 
22
23
  ensure
23
24
  post_actions = params[:post_actions]
24
- if post_actions
25
- docker_emulator&.docker_exec("cd #{workspace_dir} && #{post_actions}")
25
+ if post_actions && !mango_helper.kvm_disabled?
26
+ Helper::DockerCommander.docker_exec(command: "cd #{workspace_dir} && #{post_actions}", container_name: mango_helper.container_name)
26
27
  end
27
28
 
28
- UI.important("Cleaning up #{emulator_name} container")
29
- docker_emulator.clean_container if docker_emulator.instance_variable_get('@container')
29
+ UI.important("Cleaning up #{params[:emulator_name]} container")
30
+ mango_helper.clean_container if mango_helper.instance_variable_get('@container')
30
31
  end
31
32
 
32
33
  def self.description
@@ -115,7 +116,7 @@ module Fastlane
115
116
  FastlaneCore::ConfigItem.new(key: :workspace_dir,
116
117
  env_name: "WORKSPACE_DIR",
117
118
  default_value: '/root/tests/',
118
- description: "Path to the workspace to execute commands",
119
+ description: "Path to the workspace to execute commands. If you want to execute your `android_task` from a different directory you have to specify `workspace_dir`",
119
120
  optional: true,
120
121
  type: String),
121
122
 
@@ -0,0 +1,66 @@
1
+ require 'docker'
2
+ require 'os'
3
+
4
+ module Fastlane
5
+ module Helper
6
+ module DockerCommander
7
+
8
+ def self.pull_image(docker_image_name:)
9
+ handle_thin_pool_exception do
10
+ Actions.sh("docker pull #{docker_image_name}")
11
+ end
12
+ end
13
+
14
+ def self.start_container(emulator_args:, docker_name:, docker_image:)
15
+ docker_name = if docker_name
16
+ "--name #{docker_name}"
17
+ else
18
+ ''
19
+ end
20
+
21
+ # Action.sh returns all output that the command produced but we are only
22
+ # interested in the last line, since it contains the id of the created container.
23
+ UI.important("Attaching #{ENV['PWD']} to the docker container")
24
+ handle_thin_pool_exception do
25
+ Actions.sh("docker run -v $PWD:/root/tests --privileged -t -d #{emulator_args} #{docker_name} #{docker_image}").chomp
26
+ end
27
+ end
28
+
29
+ def self.stop_container(container_name:)
30
+ Actions.sh("docker stop #{container_name}") if container_name
31
+ end
32
+
33
+ def self.delete_container(container_name:)
34
+ Actions.sh("docker rm #{container_name}") if container_name
35
+ end
36
+
37
+ def self.disconnect_network_bridge(container_name:)
38
+ Actions.sh("docker network disconnect -f bridge #{container_name}") if container_name
39
+ rescue StandardError
40
+ # Do nothing if the network bridge is already gone
41
+ end
42
+
43
+ def self.prune
44
+ Action.sh('docker system prune -f')
45
+ end
46
+
47
+ def self.handle_thin_pool_exception(&block)
48
+ begin
49
+ block.call
50
+ rescue FastlaneCore::Interface::FastlaneShellError => exception
51
+ retry_counter = retry_counter.to_i + 1
52
+ if exception.message =~ /Create more free space in thin pool/ && retry_counter < 2
53
+ prune
54
+ retry
55
+ else
56
+ raise exception
57
+ end
58
+ end
59
+ end
60
+
61
+ def self.docker_exec(command:, container_name:)
62
+ Actions.sh("docker exec -i #{container_name} bash -l -c \"#{command}\"") if container_name
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'docker_commander'
2
+
3
+ module Fastlane
4
+ module Helper
5
+ module EmulatorCommander
6
+
7
+ def self.disable_animations(container_name:)
8
+ DockerCommander.docker_exec(command: 'adb shell settings put global window_animation_scale 0.0', container_name: container_name)
9
+ DockerCommander.docker_exec(command: 'adb shell settings put global transition_animation_scale 0.0', container_name: container_name)
10
+ DockerCommander.docker_exec(command: 'adb shell settings put global animator_duration_scale 0.0', container_name: container_name)
11
+ end
12
+
13
+ def self.increase_logcat_storage(container_name:)
14
+ DockerCommander.docker_exec(command: 'adb logcat -G 16m', container_name: container_name)
15
+ end
16
+
17
+ # Checks if created emulator is connected
18
+ def self.check_connection(container_name:)
19
+ UI.success('Checking if emulator is connected to ADB.')
20
+
21
+ if emulator_is_healthy?(container_name: container_name)
22
+ UI.success('Emulator connected successfully')
23
+ true
24
+ else
25
+ UI.important("Something went wrong. Newly created device couldn't connect to the adb")
26
+ false
27
+ end
28
+ end
29
+
30
+ def self.emulator_is_healthy?(container_name: container_name)
31
+ list_devices = DockerCommander.docker_exec(command: 'adb devices', container_name: container_name)
32
+ list_devices.include? "\tdevice"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,7 +1,9 @@
1
1
  require 'docker'
2
- require 'socket'
3
2
  require 'timeout'
4
3
  require 'os'
4
+ require 'net/http'
5
+ require_relative 'docker_commander'
6
+ require_relative 'emulator_commander'
5
7
 
6
8
  module Fastlane
7
9
  module Helper
@@ -46,22 +48,53 @@ module Fastlane
46
48
  pull_from_registry if @pull_latest_image
47
49
 
48
50
  # Make sure that network bridge for the current container is not already used
49
- disconnect_network_bridge if container_name
51
+ DockerCommander.disconnect_network_bridge(container_name: container_name)
50
52
 
51
53
  create_container
52
54
 
53
- begin
54
- wait_for_healthy_container false
55
- check_emulator_connection if is_running_on_emulator
56
- rescue StandardError
55
+ if is_running_on_emulator && kvm_disabled?
56
+ raise 'Linux requires GPU acceleration for running emulators, but KVM virtualization is not supported by your CPU. Exiting..'
57
+ end
58
+
59
+ container_state = wait_for_healthy_container
60
+
61
+ if is_running_on_emulator
62
+ connection_state = EmulatorCommander.check_connection(container_name: container_name)
63
+ container_state = connection_state && connection_state
64
+ end
65
+
66
+ unless container_state
57
67
  UI.important("Will retry checking for a healthy docker container after #{sleep_interval} seconds")
58
68
  @container.stop
59
69
  @container.delete(force: true)
60
70
  sleep @sleep_interval
61
71
  create_container
62
- wait_for_healthy_container
63
- check_emulator_connection if is_running_on_emulator
72
+
73
+ unless wait_for_healthy_container
74
+ UI.important('Container is unhealthy. Exiting..')
75
+ # We use code "2" as we need something than just standard error code 1, so we can differentiate the next step in CI
76
+ exit 2
77
+ end
78
+
79
+ if is_running_on_emulator && !EmulatorCommander.check_connection(container_name: container_name)
80
+ UI.important('Cannot connect to emulator. Exiting..')
81
+ exit 2
82
+ end
83
+ end
84
+
85
+ if is_running_on_emulator
86
+ EmulatorCommander.disable_animations(container_name: container_name)
87
+ EmulatorCommander.increase_logcat_storage(container_name: container_name)
88
+ end
89
+ end
90
+
91
+ def kvm_disabled?
92
+ begin
93
+ DockerCommander.docker_exec(command: 'kvm-ok > kvm-ok.txt', container_name: container_name)
94
+ rescue StandardError
95
+ # kvm-ok will always throw regardless of the result. therefore we save the output in the file and ignore the error
64
96
  end
97
+ DockerCommander.docker_exec(command: 'cat kvm-ok.txt', container_name: container_name).include?('KVM acceleration can NOT be used')
65
98
  end
66
99
 
67
100
  # Stops and remove container
@@ -70,11 +103,6 @@ module Fastlane
70
103
  @container.delete(force: true)
71
104
  end
72
105
 
73
- # Executes commands inside docker container
74
- def docker_exec(command)
75
- Actions.sh("docker exec -i #{container_name} bash -l -c \"#{command}\"")
76
- end
77
-
78
106
  private
79
107
 
80
108
  # Sets path to adb
@@ -90,25 +118,6 @@ module Fastlane
90
118
  UI.success("Link to VNC: http://#{@host_ip_address}:#{@no_vnc_port}")
91
119
  end
92
120
 
93
- # Restarts adb on the separate port and checks if created emulator is connected
94
- def check_emulator_connection
95
- UI.success('Checking if emulator is connected to ADB.')
96
-
97
- if emulator_is_healthy?
98
- UI.success('Emulator connected successfully')
99
- else
100
- raise "Something went wrong. Newly created device couldn't connect to the adb"
101
- end
102
-
103
- disable_animations
104
- increase_logcat_storage
105
- end
106
-
107
- def emulator_is_healthy?
108
- list_devices = docker_exec('adb devices')
109
- list_devices.include? "\tdevice"
110
- end
111
-
112
121
  # Creates new container using params
113
122
  def create_container
114
123
  UI.important("Creating container: #{container_name}")
@@ -119,8 +128,8 @@ module Fastlane
119
128
  rescue StandardError
120
129
  UI.important("Something went wrong while creating: #{container_name}, will retry in #{@sleep_interval} seconds")
121
130
  print_cpu_load
122
- `docker stop #{container_name}` if container_name
123
- `docker rm #{container_name}` if container_name
131
+ DockerCommander.stop_container(container_name: container_name)
132
+ DockerCommander.delete_container(container_name: container_name)
124
133
  sleep @sleep_interval
125
134
  container = create_container_call
126
135
  @container_name = container unless container_name
@@ -143,19 +152,9 @@ module Fastlane
143
152
  # When CPU is under load we cannot create a healthy container
144
153
  wait_cpu_to_idle
145
154
 
146
- docker_name = if container_name
147
- "--name #{container_name}"
148
- else
149
- ''
150
- end
151
-
152
155
  emulator_args = is_running_on_emulator ? "-p #{no_vnc_port}:6080 -e DEVICE='#{device_name}'" : ''
153
156
 
154
- # Action.sh returns all output that the command produced but we are only
155
- # interested in the last line, since it contains the id of the created container.
156
- UI.important("Attaching #{ENV['PWD']} to the docker container")
157
- output = Actions.sh("docker run -v $PWD:/root/tests --privileged -t -d #{emulator_args} #{docker_name} #{docker_image}").chomp
158
- output.split("\n").last
157
+ DockerCommander.start_container(emulator_args: emulator_args, docker_name: container_name, docker_image: docker_image)
159
158
  end
160
159
 
161
160
  def execute_pre_action
@@ -166,7 +165,7 @@ module Fastlane
166
165
  def pull_from_registry
167
166
  docker_image_name = docker_image.gsub(':latest', '')
168
167
  Actions.sh(@docker_registry_login) if @docker_registry_login
169
- Actions.sh("docker pull #{docker_image_name}")
168
+ DockerCommander.pull_image(docker_image_name: docker_image_name)
170
169
  end
171
170
 
172
171
  # Checks that chosen ports are not already allocated. If they are, it will stop the allocated container
@@ -177,11 +176,11 @@ module Fastlane
177
176
  vnc_allocated_container.stop
178
177
  end
179
178
 
180
- if ports_open?('0.0.0.0', [@no_vnc_port])
181
- UI.important('Something went wrong. One of the required ports is still busy')
179
+ if port_open?('0.0.0.0', @no_vnc_port)
180
+ UI.important('Something went wrong. VNC port is still busy')
182
181
  sleep @sleep_interval
183
- `docker stop #{container_name}` if container_name
184
- `docker rm #{container_name}` if container_name
182
+ DockerCommander.stop_container(container_name: container_name)
183
+ DockerCommander.delete_container(container_name: container_name)
185
184
  end
186
185
  end
187
186
 
@@ -222,7 +221,7 @@ module Fastlane
222
221
  end
223
222
 
224
223
  # Waits until container is healthy using specified timeout
225
- def wait_for_healthy_container(will_exit = true)
224
+ def wait_for_healthy_container
226
225
  UI.important('Waiting for Container to be in the Healthy state.')
227
226
 
228
227
  number_of_tries = timeout / sleep_interval
@@ -232,31 +231,25 @@ module Fastlane
232
231
  UI.success('Your container is ready to work')
233
232
  return true
234
233
  end
235
- UI.important("Container status: #{@container.json['State']['Status']}")
234
+
235
+ if @container.json['State']['Health']
236
+ UI.important("Container status: #{@container.json['State']['Health']['Status']}")
237
+ else
238
+ UI.important("Container status: #{@container.json['State']['Status']}")
239
+ end
240
+
236
241
  sleep sleep_interval
237
242
  end
238
243
  UI.important("The Container failed to load after '#{timeout}' seconds timeout. Reason: '#{@container.json['State']['Status']}'")
239
- # We use code "2" as we need something than just standard error code 1, so we can differentiate the next step in CI
240
- exit 2 if will_exit
241
- raise 'Fail'
244
+ false
242
245
  end
243
246
 
244
- # Checks if port is already open
245
- def ports_open?(ip, ports)
246
- raise "'ports' should be an array" unless ports.is_a? Array
247
- ports.each do |port|
248
- begin
249
- Timeout.timeout(1) do
250
- begin
251
- s = TCPSocket.new(ip, port)
252
- s.close
253
- return true
254
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
255
- end
256
- end
257
- rescue Timeout::Error
258
- end
259
- end
247
+ # Checks if port is already openZ
248
+ def port_open?(server, port)
249
+ http = Net::HTTP.start(server, port, open_timeout: 5, read_timeout: 5)
250
+ response = http.head('/')
251
+ response.code == '200'
252
+ rescue Timeout::Error, SocketError, Errno::ECONNREFUSED
260
253
  false
261
254
  end
262
255
 
@@ -288,23 +281,6 @@ module Fastlane
288
281
  raise "CPU was overloaded. Couldn't start emulator"
289
282
  end
290
283
 
291
- # Disables animation for faster and stable testing
292
- def disable_animations
293
- docker_exec('adb shell settings put global window_animation_scale 0.0')
294
- docker_exec('adb shell settings put global transition_animation_scale 0.0')
295
- docker_exec('adb shell settings put global animator_duration_scale 0.0')
296
- end
297
-
298
- # Increases logcat storage
299
- def increase_logcat_storage
300
- docker_exec('adb logcat -G 16m')
301
- end
302
-
303
- def disconnect_network_bridge
304
- `docker network disconnect -f bridge #{container_name}`
305
- rescue StandardError
306
- # Do nothing if the network bridge is already gone
307
- end
308
284
  end
309
285
  end
310
286
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Mango
3
- VERSION = '1.0.0'.freeze
3
+ VERSION = '1.1.0'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-mango
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Serghei Moret, Daniel Hartwich
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-15 00:00:00.000000000 Z
11
+ date: 2018-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: docker-api
@@ -146,6 +146,8 @@ files:
146
146
  - README.md
147
147
  - lib/fastlane/plugin/mango.rb
148
148
  - lib/fastlane/plugin/mango/actions/run_dockerized_task_action.rb
149
+ - lib/fastlane/plugin/mango/helper/docker_commander.rb
150
+ - lib/fastlane/plugin/mango/helper/emulator_commander.rb
149
151
  - lib/fastlane/plugin/mango/helper/mango_helper.rb
150
152
  - lib/fastlane/plugin/mango/version.rb
151
153
  homepage: https://github.com/xing/mango
@@ -168,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
170
  version: '0'
169
171
  requirements: []
170
172
  rubyforge_project:
171
- rubygems_version: 2.7.7
173
+ rubygems_version: 2.7.4
172
174
  signing_key:
173
175
  specification_version: 4
174
176
  summary: This plugin Android tasks on docker images