danger-apkstats 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apkstats::Command
4
+ module Executable
5
+ require "open3"
6
+
7
+ attr_reader :command_path
8
+
9
+ def executable?
10
+ File.executable?(command_path)
11
+ end
12
+
13
+ # Compare two apk files and return results.
14
+ #
15
+ # {
16
+ # base: {
17
+ # file_size: Integer,
18
+ # download_size: Integer,
19
+ # required_features: Array<String>,
20
+ # non_required_features: Array<String>,
21
+ # permissions: Array<String>,
22
+ # min_sdk: String,
23
+ # target_sdk: String,
24
+ # },
25
+ # other: {
26
+ # file_size: Integer,
27
+ # download_size: Integer,
28
+ # required_features: Array<String>,
29
+ # non_required_features: Array<String>,
30
+ # permissions: Array<String>,
31
+ # min_sdk: String,
32
+ # target_sdk: String,
33
+ # },
34
+ # diff: {
35
+ # file_size: Integer,
36
+ # download_size: Integer,
37
+ # required_features: {
38
+ # new: Array<String>,
39
+ # removed: Array<String>,
40
+ # },
41
+ # non_required_features:{
42
+ # new: Array<String>,
43
+ # removed: Array<String>,
44
+ # },
45
+ # permissions: {
46
+ # new: Array<String>,
47
+ # removed: Array<String>,
48
+ # },
49
+ # min_sdk: Array<String>,
50
+ # target_sdk: Array<String>,
51
+ # }
52
+ # }
53
+ #
54
+ # @return [Hash]
55
+ def compare_with(apk_filepath, other_apk_filepath)
56
+ base = Apkstats::Entity::ApkInfo.new(self, apk_filepath)
57
+ other = Apkstats::Entity::ApkInfo.new(self, other_apk_filepath)
58
+
59
+ Apkstats::Entity::ApkInfoDiff.new(base, other).to_h
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apkstats::Entity
4
+ class ApkInfo
5
+ KEYS = %i(
6
+ file_size
7
+ download_size
8
+ required_features
9
+ non_required_features
10
+ permissions
11
+ min_sdk
12
+ target_sdk
13
+ ).freeze
14
+
15
+ # Integer
16
+ attr_accessor :file_size, :download_size
17
+
18
+ # String
19
+ attr_accessor :min_sdk, :target_sdk
20
+
21
+ # Array<String>
22
+ attr_accessor :required_features, :non_required_features, :permissions
23
+
24
+ def initialize(command, apk_filepath)
25
+ KEYS.each do |key|
26
+ self.send("#{key}=", command.send(key, apk_filepath))
27
+ end
28
+ end
29
+
30
+ def [](key)
31
+ send(key)
32
+ end
33
+
34
+ def to_h
35
+ KEYS.each_with_object({}) do |key, acc|
36
+ acc[key] = self[key]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apkstats::Entity
4
+ class ApkInfoDiff
5
+ KEYS = Apkstats::Entity::ApkInfo::KEYS
6
+
7
+ # ApkInfo
8
+ attr_reader :base, :other
9
+
10
+ private(:base, :other)
11
+
12
+ def initialize(base, other)
13
+ @base = base
14
+ @other = other
15
+ end
16
+
17
+ def to_h
18
+ KEYS.each_with_object({}) do |key, acc|
19
+ acc[key] = self.send(key)
20
+ end.compact
21
+ end
22
+
23
+ def file_size
24
+ # Integer
25
+ @base[__method__].to_i - @other[__method__].to_i
26
+ end
27
+
28
+ def download_size
29
+ # Integer
30
+ @base[__method__].to_i - @other[__method__].to_i
31
+ end
32
+
33
+ def required_features
34
+ # Features
35
+ {
36
+ new: (@base[__method__] - @other[__method__]).to_a,
37
+ removed: (@other[__method__] - @base[__method__]).to_a,
38
+ }
39
+ end
40
+
41
+ def non_required_features
42
+ # Features
43
+ {
44
+ new: (@base[__method__] - @other[__method__]).to_a,
45
+ removed: (@other[__method__] - @base[__method__]).to_a,
46
+ }
47
+ end
48
+
49
+ def permissions
50
+ # Permissions
51
+ {
52
+ new: (@base[__method__] - @other[__method__]).to_a,
53
+ removed: (@other[__method__] - @base[__method__]).to_a,
54
+ }
55
+ end
56
+
57
+ def min_sdk
58
+ # String
59
+ [@base[__method__], @other[__method__]].uniq
60
+ end
61
+
62
+ def target_sdk
63
+ # String
64
+ [@base[__method__], @other[__method__]].uniq
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apkstats::Entity
4
+ class Feature
5
+ # String
6
+ attr_reader :name
7
+
8
+ # String?
9
+ attr_reader :implied_reason
10
+
11
+ def initialize(name, not_required: false, implied_reason: nil)
12
+ @name = name
13
+ # cast to Boolean
14
+ @not_required = not_required == true
15
+ @implied_reason = implied_reason || nil
16
+ end
17
+
18
+ def not_required?
19
+ @not_required
20
+ end
21
+
22
+ def implied?
23
+ @implied_reason
24
+ end
25
+
26
+ def to_s
27
+ if implied?
28
+ "#{name} (#{implied_reason})"
29
+ elsif not_required?
30
+ "#{name} (not-required)"
31
+ else
32
+ name
33
+ end
34
+ end
35
+
36
+ def ==(other)
37
+ return if !other || other.class != self.class
38
+
39
+ to_s == other.to_s
40
+ end
41
+
42
+ def eql?(other)
43
+ to_s.eql?(other.to_s)
44
+ end
45
+
46
+ def hash
47
+ h = not_required? ? 1 : 0
48
+ h *= 31
49
+ h += name.hash
50
+
51
+ if implied_reason
52
+ h *= 31
53
+ h += implied_reason.hash
54
+ end
55
+
56
+ h
57
+ end
58
+ end
59
+
60
+ class Features
61
+ attr_reader :values
62
+
63
+ # Array<Feature>
64
+ def initialize(feature_arr)
65
+ @values = feature_arr
66
+ end
67
+
68
+ def -(other)
69
+ raise "#{self.class} cannot handle #{other.class} with the minus operator" unless other.class == Features
70
+
71
+ self_hash = Features.hashnize(self)
72
+ other_hash = Features.hashnize(other)
73
+
74
+ diff_features = (self_hash.keys - other_hash.keys).map do |key|
75
+ self_hash[key]
76
+ end
77
+
78
+ Features.new(diff_features)
79
+ end
80
+
81
+ def to_a
82
+ values.map(&:to_s)
83
+ end
84
+
85
+ def eql?(other)
86
+ return if !other || other.class == Features
87
+ other.values == values
88
+ end
89
+
90
+ def hash
91
+ values.hash
92
+ end
93
+
94
+ def self.hashnize(features)
95
+ features.values.each_with_object({}) do |feature, acc|
96
+ acc[[feature.name, feature.not_required?]] = feature
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apkstats::Entity
4
+ class Permission
5
+ # String
6
+ attr_reader :name
7
+
8
+ # String?
9
+ attr_reader :max_sdk
10
+
11
+ def initialize(name, max_sdk: nil)
12
+ @name = name
13
+ @max_sdk = max_sdk
14
+ end
15
+
16
+ def to_s
17
+ if max_sdk
18
+ "#{name} maxSdkVersion=#{max_sdk}"
19
+ else
20
+ name
21
+ end
22
+ end
23
+
24
+ def ==(other)
25
+ return if !other || other.class != self.class
26
+
27
+ to_s == other.to_s
28
+ end
29
+
30
+ def eql?(other)
31
+ to_s.eql?(other.to_s)
32
+ end
33
+
34
+ def hash
35
+ h = name.hash
36
+
37
+ if max_sdk
38
+ h *= 31
39
+ h += max_sdk.hash
40
+ end
41
+
42
+ h
43
+ end
44
+ end
45
+
46
+ class Permissions
47
+ attr_reader :values
48
+
49
+ # Array<Permission>
50
+ def initialize(permission_arr)
51
+ @values = permission_arr
52
+ end
53
+
54
+ def -(other)
55
+ raise "#{self.class} cannot handle #{other.class} with the minus operator" unless other.class == Permissions
56
+
57
+ self_hash = Permissions.hashnize(self)
58
+ other_hash = Permissions.hashnize(other)
59
+
60
+ diff_permissions = (self_hash.keys - other_hash.keys).map do |key|
61
+ self_hash[key]
62
+ end
63
+
64
+ Permissions.new(diff_permissions)
65
+ end
66
+
67
+ def to_a
68
+ values.map(&:to_s)
69
+ end
70
+
71
+ def eql?(other)
72
+ return if !other || other.class == Permissions
73
+ other.values == values
74
+ end
75
+
76
+ def hash
77
+ values.hash
78
+ end
79
+
80
+ def self.hashnize(permissions)
81
+ permissions.values.each_with_object({}) do |permission, acc|
82
+ acc[[permission.name, permission.max_sdk]] = permission
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Apkstats
2
- VERSION = "0.0.2".freeze
4
+ VERSION = "0.1.0"
3
5
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apkstats::Helper
4
+ module Bytes
5
+ STEP = 2**10.to_f
6
+
7
+ def self.from_b(byte)
8
+ Byte.new(byte)
9
+ end
10
+
11
+ def self.from_kb(k_byte)
12
+ Byte.new(down_unit(k_byte))
13
+ end
14
+
15
+ def self.from_mb(m_byte)
16
+ Byte.new(down_unit(down_unit(m_byte)))
17
+ end
18
+
19
+ def self.up_unit(size)
20
+ (size.to_f / STEP).round(2)
21
+ end
22
+
23
+ def self.down_unit(size)
24
+ size.to_f * STEP
25
+ end
26
+
27
+ class Byte
28
+ attr_reader :value
29
+
30
+ def initialize(value)
31
+ @value = value
32
+ end
33
+
34
+ def to_b
35
+ value
36
+ end
37
+
38
+ def to_kb
39
+ Bytes.up_unit(value)
40
+ end
41
+
42
+ def to_mb
43
+ Bytes.up_unit(to_kb)
44
+ end
45
+
46
+ def to_s_b
47
+ add_op(to_b)
48
+ end
49
+
50
+ def to_s_kb
51
+ add_op(to_kb)
52
+ end
53
+
54
+ def to_s_mb
55
+ add_op(to_mb)
56
+ end
57
+
58
+ private
59
+
60
+ def add_op(size)
61
+ size.negative? ? size.to_s : "+#{size}"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,80 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper/bytes"
4
+
5
+ require_relative "entity/apk_info"
6
+ require_relative "entity/apk_info_diff"
7
+ require_relative "entity/feature"
8
+ require_relative "entity/permission"
9
+
10
+ require_relative "command/executable"
11
+ require_relative "command/apk_analyzer"
12
+
1
13
  module Danger
2
- # This is your plugin class. Any attributes or methods you expose here will
3
- # be available from within your Dangerfile.
14
+ # Show stats of your apk file.
15
+ # By default, it's done using apkanalyzer in android sdk.
16
+ #
17
+ # All command need your apk filepath like below
18
+ #
19
+ # apkstats.apk_filepath=<your new apk filepath>
20
+ #
21
+ # @example Compare two apk files and print it.
22
+ #
23
+ # apkstats.compare_with(<your old apk filepath>, do_report: true) # report it in markdown table
24
+ # apkstats.compare_with(<your old apk filepath>, do_report: false) # just return results
25
+ #
26
+ # @example Show the file size of your apk file.
27
+ #
28
+ # apkstats.file_size
29
+ #
30
+ # @example Show the download size of your apk file.
31
+ #
32
+ # apkstats.download_size
33
+ #
34
+ # @example Show all required features of your apk file.
35
+ #
36
+ # apkstats.required_features
37
+ #
38
+ # @example Show all non-required features of your apk file.
39
+ #
40
+ # apkstats.non_required_features
4
41
  #
5
- # To be published on the Danger plugins site, you will need to have
6
- # the public interface documented. Danger uses [YARD](http://yardoc.org/)
7
- # for generating documentation from your plugin source, and you can verify
8
- # by running `danger plugins lint` or `bundle exec rake spec`.
42
+ # @example Show all requested permissions of your apk file.
9
43
  #
10
- # You should replace these comments with a public description of your library.
44
+ # apkstats.permissions
11
45
  #
12
- # @example Ensure people are well warned about merging on Mondays
46
+ # @example Show the min sdk version of your apk file.
13
47
  #
14
- # my_plugin.warn_on_mondays
48
+ # apkstats.min_sdk
49
+ #
50
+ # @example Show the target sdk version of your apk file.
51
+ #
52
+ # apkstats.target_sdk
15
53
  #
16
54
  # @see Jumpei Matsuda/danger-apkstats
17
55
  # @tags android, apk_stats
18
56
  #
19
57
  class DangerApkstats < Plugin
20
- require_relative 'command/executable_command'
21
- require_relative 'command/apk_analyzer'
22
-
23
58
  COMMAND_TYPE_MAP = {
24
- apk_analyzer: Danger::Apkstats::ApkAnalyzer,
59
+ apk_analyzer: Apkstats::Command::ApkAnalyzer,
25
60
  }.freeze
26
61
 
27
62
  private_constant(:COMMAND_TYPE_MAP)
28
63
 
29
- # A command type to be run
64
+ # *Optional*
65
+ # A command type to be run.
66
+ # One of keys of COMMAND_TYPE_MAP
30
67
  #
31
- # @return [Symbol, String] either of array( apk_analyzer )
68
+ # @return [Symbol, Nil] _
32
69
  attr_accessor :command_type
33
70
 
34
- # An apk file to be operated
71
+ # *Optional*
72
+ # A custom command path
73
+ #
74
+ # @return [Symbol, Nil] _
75
+ attr_accessor :command_path
76
+
77
+ # *Required*
78
+ # Your target apk filepath.
35
79
  #
36
80
  # @return [String]
37
81
  attr_accessor :apk_filepath
38
82
 
39
- # TODO multiple apks
83
+ # rubocop:disable Metrics/AbcSize
84
+
85
+ # Get stats of two apk files and calculate diffs between them.
86
+ #
87
+ # @param [String] other_apk_filepath your old apk
88
+ # @param [Boolean] do_report report markdown table if true, otherwise just return results
89
+ # @return [Hash] see command/executable#compare_with for more detail
90
+ def compare_with(other_apk_filepath, do_report: true)
91
+ raise "apk filepaths must be specified" if apk_filepath.nil? || apk_filepath.empty?
92
+
93
+ base_apk = Apkstats::Entity::ApkInfo.new(command, apk_filepath)
94
+ other_apk = Apkstats::Entity::ApkInfo.new(command, other_apk_filepath)
95
+
96
+ return {
97
+ base: base_apk.to_h,
98
+ other: base_apk.to_h,
99
+ diff: Apkstats::Entity::ApkInfoDiff.new(base_apk, other_apk).to_h,
100
+ }.tap do |result|
101
+ break unless do_report
102
+
103
+ diff = result[:diff]
104
+
105
+ md = +"### Apk comparision results" << "\n\n"
106
+ md << "Property | Summary" << "\n"
107
+ md << ":--- | :---" << "\n"
108
+
109
+ diff[:min_sdk].tap do |min_sdk|
110
+ break if min_sdk.size == 1
111
+
112
+ md << "Min SDK Change | Before #{min_sdk[1]} / After #{min_sdk[0]}" << "\n"
113
+ end
114
+
115
+ diff[:target_sdk].tap do |target_sdk|
116
+ break if target_sdk.size == 1
117
+
118
+ md << "Target SDK Change | Before #{target_sdk[1]} / After #{target_sdk[0]}" << "\n"
119
+ end
120
+
121
+ result[:base][:file_size].tap do |file_size|
122
+ size = Apkstats::Helper::Bytes.from_b(file_size)
123
+
124
+ md << "New File Size | #{size.to_b} Bytes. (#{size.to_mb} MB " << "\n"
125
+ end
40
126
 
41
- def compare_with(other_apk_filepath, opts={})
42
- raise 'apks must be specified' if apk_filepath.nil? || apk_filepath.empty?
127
+ diff[:file_size].tap do |file_size|
128
+ size = Apkstats::Helper::Bytes.from_b(file_size)
43
129
 
44
- out, err = command.compare_with(apk_filepath, other_apk_filepath)
130
+ md << "File Size Change | #{size.to_s_b} Bytes. (#{size.to_s_kb} KB) " << "\n"
131
+ end
132
+
133
+ diff[:download_size].tap do |download_size|
134
+ size = Apkstats::Helper::Bytes.from_b(download_size)
45
135
 
46
- if opts[:do_report]
47
- if out&.empty?
48
- message("Apk file size was not changed")
49
- elsif out
50
- left, right, diff, = out.split("\s")
51
- message("Apk file size was changed by #{diff} : from #{left} to #{right}")
52
- else
53
- warn(err)
136
+ md << "Download Size Change | #{size.to_s_b} Bytes. (#{size.to_s_kb} KB) " << "\n"
54
137
  end
138
+
139
+ report_hash_and_arrays = lambda { |key, name|
140
+ list_up_entities = lambda { |type_key, label|
141
+ diff[key][type_key].tap do |features|
142
+ break if features.empty?
143
+
144
+ md << "#{label} | " << features.map { |f| "- #{f}" }.join("<br>").to_s << "\n"
145
+ end
146
+ }
147
+
148
+ list_up_entities.call(:new, "New #{name}")
149
+ list_up_entities.call(:removed, "Removed #{name}")
150
+ }
151
+
152
+ report_hash_and_arrays.call(:required_features, "Required Features")
153
+ report_hash_and_arrays.call(:non_required_features, "Non-required Features")
154
+ report_hash_and_arrays.call(:permissions, "Permissions")
155
+
156
+ markdown(md)
55
157
  end
158
+ rescue StandardError => e
159
+ warn("apkstats failed to execute the command due to #{e.message}")
160
+
161
+ e.backtrace&.each { |line| STDOUT.puts line }
162
+ end
163
+
164
+ # rubocop:enable Metrics/AbcSize
165
+
166
+ # Show the file size of your apk file.
167
+ #
168
+ # @return [Fixnum] return positive value if exists, otherwise -1.
169
+ def file_size(_opts = {})
170
+ result = run_command(__method__)
171
+ result ? result.to_i : -1
172
+ end
173
+
174
+ # Show the download size of your apk file.
175
+ #
176
+ # @return [Fixnum] return positive value if exists, otherwise -1.
177
+ def download_size(_opts = {})
178
+ result = run_command(__method__)
179
+ result ? result.to_i : -1
180
+ end
56
181
 
57
- return out, err
182
+ # Show all required features of your apk file.
183
+ # The result doesn't contain non-required features.
184
+ #
185
+ # @return [Array<String>, Nil] return nil unless retrieved.
186
+ def required_features(_opts = {})
187
+ result = run_command(__method__)
188
+ result ? result.to_a : nil
58
189
  end
59
190
 
60
- def filesize(opts={})
61
- raise 'apks must be specified' if apk_filepath.nil? || apk_filepath.empty?
191
+ # Show all non-required features of your apk file.
192
+ # The result doesn't contain required features.
193
+ #
194
+ # @return [Array<String>, Nil] return nil unless retrieved.
195
+ def non_required_features(_opts = {})
196
+ result = run_command(__method__)
197
+ result ? result.to_a : nil
198
+ end
62
199
 
63
- out, = command.filesize(apk_filepath)
64
- out
200
+ # Show all permissions of your apk file.
201
+ #
202
+ # @return [Array<String>, Nil] return nil unless retrieved.
203
+ def permissions(_opts = {})
204
+ result = run_command(__method__)
205
+ result ? result.to_a : nil
65
206
  end
66
207
 
67
- def downloadsize(opts={})
68
- raise 'apks must be specified' if apk_filepath.nil? || apk_filepath.empty?
208
+ # Show the min sdk version of your apk file.
209
+ #
210
+ # @return [String, Nil] return nil unless retrieved.
211
+ def min_sdk(_opts = {})
212
+ run_command(__method__)
213
+ end
69
214
 
70
- out, = command.downloadsize(apk_filepath)
71
- out
215
+ # Show the target sdk version of your apk file.
216
+ #
217
+ # @return [String, Nil] return nil unless retrieved.
218
+ def target_sdk(_opts = {})
219
+ run_command(__method__)
72
220
  end
73
221
 
74
222
  private
75
223
 
224
+ def run_command(name)
225
+ raise "#{command.command_path} is not found or is not executable" unless command.executable?
226
+
227
+ return command.send(name, apk_filepath)
228
+ rescue StandardError => e
229
+ warn("apkstats failed to execute the command #{name} due to #{e.message}")
230
+
231
+ e.backtrace&.each { |line| puts line }
232
+
233
+ nil
234
+ end
235
+
76
236
  def command
77
- @command ||= COMMAND_TYPE_MAP[command_type.to_sym].new
237
+ command_type ||= :apk_analyzer
238
+ @command ||= COMMAND_TYPE_MAP[command_type.to_sym].new(command_path: command_path)
78
239
  end
79
240
  end
80
241
  end