knapsack_pro 1.20.1 → 1.22.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +42 -17
  4. data/lib/knapsack_pro.rb +6 -0
  5. data/lib/knapsack_pro/adapters/base_adapter.rb +16 -0
  6. data/lib/knapsack_pro/adapters/rspec_adapter.rb +11 -9
  7. data/lib/knapsack_pro/allocator.rb +7 -5
  8. data/lib/knapsack_pro/allocator_builder.rb +2 -1
  9. data/lib/knapsack_pro/base_allocator_builder.rb +41 -10
  10. data/lib/knapsack_pro/build_distribution_fetcher.rb +57 -0
  11. data/lib/knapsack_pro/client/api/v1/build_distributions.rb +13 -0
  12. data/lib/knapsack_pro/client/connection.rb +38 -16
  13. data/lib/knapsack_pro/config/env.rb +4 -0
  14. data/lib/knapsack_pro/queue_allocator.rb +7 -5
  15. data/lib/knapsack_pro/queue_allocator_builder.rb +2 -1
  16. data/lib/knapsack_pro/report.rb +1 -1
  17. data/lib/knapsack_pro/runners/queue/cucumber_runner.rb +10 -1
  18. data/lib/knapsack_pro/runners/queue/rspec_runner.rb +7 -0
  19. data/lib/knapsack_pro/runners/rspec_runner.rb +5 -2
  20. data/lib/knapsack_pro/slow_test_file_determiner.rb +33 -0
  21. data/lib/knapsack_pro/slow_test_file_finder.rb +27 -0
  22. data/lib/knapsack_pro/test_case_detectors/rspec_test_example_detector.rb +26 -7
  23. data/lib/knapsack_pro/test_case_mergers/base_merger.rb +29 -0
  24. data/lib/knapsack_pro/test_case_mergers/rspec_merger.rb +34 -0
  25. data/lib/knapsack_pro/test_file_finder.rb +43 -5
  26. data/lib/knapsack_pro/test_files_with_test_cases_composer.rb +22 -0
  27. data/lib/knapsack_pro/version.rb +1 -1
  28. data/spec/knapsack_pro/adapters/base_adapter_spec.rb +55 -0
  29. data/spec/knapsack_pro/adapters/rspec_adapter_spec.rb +61 -25
  30. data/spec/knapsack_pro/allocator_builder_spec.rb +7 -3
  31. data/spec/knapsack_pro/allocator_spec.rb +7 -5
  32. data/spec/knapsack_pro/base_allocator_builder_spec.rb +79 -27
  33. data/spec/knapsack_pro/build_distribution_fetcher_spec.rb +89 -0
  34. data/spec/knapsack_pro/client/api/v1/build_distributions_spec.rb +31 -0
  35. data/spec/knapsack_pro/client/connection_spec.rb +235 -104
  36. data/spec/knapsack_pro/config/env_spec.rb +14 -0
  37. data/spec/knapsack_pro/queue_allocator_builder_spec.rb +7 -3
  38. data/spec/knapsack_pro/queue_allocator_spec.rb +7 -5
  39. data/spec/knapsack_pro/report_spec.rb +1 -1
  40. data/spec/knapsack_pro/runners/queue/cucumber_runner_spec.rb +38 -25
  41. data/spec/knapsack_pro/runners/rspec_runner_spec.rb +4 -4
  42. data/spec/knapsack_pro/slow_test_file_determiner_spec.rb +74 -0
  43. data/spec/knapsack_pro/slow_test_file_finder_spec.rb +43 -0
  44. data/spec/knapsack_pro/test_case_detectors/rspec_test_example_detector_spec.rb +83 -37
  45. data/spec/knapsack_pro/test_case_mergers/base_merger_spec.rb +27 -0
  46. data/spec/knapsack_pro/test_case_mergers/rspec_merger_spec.rb +59 -0
  47. data/spec/knapsack_pro/test_file_finder_spec.rb +105 -29
  48. data/spec/knapsack_pro/test_files_with_test_cases_composer_spec.rb +41 -0
  49. metadata +20 -2
@@ -38,4 +38,35 @@ describe KnapsackPro::Client::API::V1::BuildDistributions do
38
38
  expect(subject).to eq action
39
39
  end
40
40
  end
41
+
42
+ describe '.last' do
43
+ let(:commit_hash) { double }
44
+ let(:branch) { double }
45
+ let(:node_total) { double }
46
+ let(:node_index) { double }
47
+
48
+ subject do
49
+ described_class.last(
50
+ commit_hash: commit_hash,
51
+ branch: branch,
52
+ node_total: node_total,
53
+ node_index: node_index,
54
+ )
55
+ end
56
+
57
+ it do
58
+ action = double
59
+ expect(KnapsackPro::Client::API::Action).to receive(:new).with({
60
+ endpoint_path: '/v1/build_distributions/last',
61
+ http_method: :get,
62
+ request_hash: {
63
+ commit_hash: commit_hash,
64
+ branch: branch,
65
+ node_total: node_total,
66
+ node_index: node_index,
67
+ }
68
+ }).and_return(action)
69
+ expect(subject).to eq action
70
+ end
71
+ end
41
72
  end
@@ -1,167 +1,298 @@
1
+ shared_examples 'when request got response from API' do
2
+ context 'when body response is JSON and API response code is 400' do
3
+ let(:body) { '{"errors": "value"}' }
4
+ let(:code) { '400' } # it must be string code
5
+
6
+ before do
7
+ expect(KnapsackPro).to receive(:logger).exactly(4).and_return(logger)
8
+ expect(logger).to receive(:debug).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
9
+ expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
10
+ expect(logger).to receive(:debug).with('API response:')
11
+ end
12
+
13
+ it do
14
+ parsed_response = { 'errors' => 'value' }
15
+
16
+ expect(logger).to receive(:error).with(parsed_response)
17
+
18
+ expect(subject).to eq(parsed_response)
19
+ expect(connection.success?).to be true
20
+ expect(connection.errors?).to be true
21
+ end
22
+ end
23
+
24
+ context 'when body response is JSON with build_distribution_id' do
25
+ let(:body) { '{"build_distribution_id": "seed-uuid"}' }
26
+ let(:code) { '200' } # it must be string code
27
+
28
+ before do
29
+ expect(KnapsackPro).to receive(:logger).exactly(5).and_return(logger)
30
+ expect(logger).to receive(:debug).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
31
+ expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
32
+ expect(logger).to receive(:debug).with("Test suite split seed: seed-uuid")
33
+ expect(logger).to receive(:debug).with('API response:')
34
+ end
35
+
36
+ it do
37
+ parsed_response = { 'build_distribution_id' => 'seed-uuid' }
38
+
39
+ expect(logger).to receive(:debug).with(parsed_response)
40
+
41
+ expect(subject).to eq(parsed_response)
42
+ expect(connection.success?).to be true
43
+ expect(connection.errors?).to be false
44
+ end
45
+ end
46
+
47
+ context 'when body response is empty' do
48
+ let(:body) { '' }
49
+ let(:code) { '200' } # it must be string code
50
+
51
+ before do
52
+ expect(KnapsackPro).to receive(:logger).exactly(4).and_return(logger)
53
+ expect(logger).to receive(:debug).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
54
+ expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
55
+ expect(logger).to receive(:debug).with('API response:')
56
+ end
57
+
58
+ it do
59
+ expect(logger).to receive(:debug).with('')
60
+
61
+ expect(subject).to eq('')
62
+ expect(connection.success?).to be true
63
+ expect(connection.errors?).to be false
64
+ end
65
+ end
66
+ end
67
+
68
+ shared_examples 'when retry request' do
69
+ context 'when body response is JSON and API response code is 500' do
70
+ let(:body) { '{"error": "Internal Server Error"}' }
71
+ let(:code) { '500' } # it must be string code
72
+
73
+ before do
74
+ expect(KnapsackPro).to receive(:logger).at_least(1).and_return(logger)
75
+ end
76
+
77
+ it do
78
+ expect(logger).to receive(:debug).exactly(3).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
79
+ expect(logger).to receive(:debug).exactly(3).with('API request UUID: fake-uuid')
80
+ expect(logger).to receive(:debug).exactly(3).with('API response:')
81
+
82
+ parsed_response = { 'error' => 'Internal Server Error' }
83
+
84
+ expect(logger).to receive(:error).exactly(3).with(parsed_response)
85
+
86
+ server_error = described_class::ServerError.new(parsed_response)
87
+ expect(logger).to receive(:warn).exactly(3).with(server_error.inspect)
88
+
89
+ expect(logger).to receive(:warn).with("Wait 8s and retry request to Knapsack Pro API.")
90
+ expect(logger).to receive(:warn).with("Next request in 8s...")
91
+ expect(logger).to receive(:warn).with("Next request in 6s...")
92
+ expect(logger).to receive(:warn).with("Next request in 4s...")
93
+ expect(logger).to receive(:warn).with("Next request in 2s...")
94
+ expect(logger).to receive(:warn).with("Wait 16s and retry request to Knapsack Pro API.")
95
+ expect(logger).to receive(:warn).with("Next request in 16s...")
96
+ expect(logger).to receive(:warn).with("Next request in 14s...")
97
+ expect(logger).to receive(:warn).with("Next request in 12s...")
98
+ expect(logger).to receive(:warn).with("Next request in 10s...")
99
+ expect(logger).to receive(:warn).with("Next request in 8s...")
100
+ expect(logger).to receive(:warn).with("Next request in 6s...")
101
+ expect(logger).to receive(:warn).with("Next request in 4s...")
102
+ expect(logger).to receive(:warn).with("Next request in 2s...")
103
+ expect(Kernel).to receive(:sleep).exactly(12).with(2)
104
+
105
+ expect(subject).to eq(parsed_response)
106
+
107
+ expect(connection.success?).to be false
108
+ expect(connection.errors?).to be true
109
+ end
110
+
111
+ context 'when Fallback Mode is disabled' do
112
+ before do
113
+ expect(KnapsackPro::Config::Env).to receive(:fallback_mode_enabled?).at_least(1).and_return(false)
114
+ end
115
+
116
+ it do
117
+ expect(logger).to receive(:debug).exactly(6).with("#{expected_http_method} http://api.knapsackpro.test:3000/v1/fake_endpoint")
118
+ expect(logger).to receive(:debug).exactly(6).with('API request UUID: fake-uuid')
119
+ expect(logger).to receive(:debug).exactly(6).with('API response:')
120
+
121
+ parsed_response = { 'error' => 'Internal Server Error' }
122
+
123
+ expect(logger).to receive(:error).exactly(6).with(parsed_response)
124
+
125
+ server_error = described_class::ServerError.new(parsed_response)
126
+ expect(logger).to receive(:warn).exactly(6).with(server_error.inspect)
127
+
128
+ expect(logger).to receive(:warn).with("Wait 8s and retry request to Knapsack Pro API.")
129
+ expect(logger).to receive(:warn).with("Next request in 8s...")
130
+ expect(logger).to receive(:warn).with("Next request in 6s...")
131
+ expect(logger).to receive(:warn).with("Next request in 4s...")
132
+ expect(logger).to receive(:warn).with("Next request in 2s...")
133
+
134
+ expect(logger).to receive(:warn).with("Wait 16s and retry request to Knapsack Pro API.")
135
+ expect(logger).to receive(:warn).with("Next request in 16s...")
136
+ expect(logger).to receive(:warn).with("Next request in 14s...")
137
+ expect(logger).to receive(:warn).with("Next request in 12s...")
138
+ expect(logger).to receive(:warn).with("Next request in 10s...")
139
+ expect(logger).to receive(:warn).with("Next request in 8s...")
140
+ expect(logger).to receive(:warn).with("Next request in 6s...")
141
+ expect(logger).to receive(:warn).with("Next request in 4s...")
142
+ expect(logger).to receive(:warn).with("Next request in 2s...")
143
+
144
+ expect(logger).to receive(:warn).with("Wait 24s and retry request to Knapsack Pro API.")
145
+ 12.times do |i|
146
+ expect(logger).to receive(:warn).with("Next request in #{(i+1)*2}s...")
147
+ end
148
+
149
+ expect(logger).to receive(:warn).with("Wait 32s and retry request to Knapsack Pro API.")
150
+ 16.times do |i|
151
+ expect(logger).to receive(:warn).with("Next request in #{(i+1)*2}s...")
152
+ end
153
+
154
+ expect(logger).to receive(:warn).with("Wait 40s and retry request to Knapsack Pro API.")
155
+ 20.times do |i|
156
+ expect(logger).to receive(:warn).with("Next request in #{(i+1)*2}s...")
157
+ end
158
+
159
+ expect(Kernel).to receive(:sleep).exactly(60).with(2)
160
+
161
+ expect(subject).to eq(parsed_response)
162
+
163
+ expect(connection.success?).to be false
164
+ expect(connection.errors?).to be true
165
+ end
166
+ end
167
+ end
168
+ end
169
+
1
170
  describe KnapsackPro::Client::Connection do
2
171
  let(:endpoint_path) { '/v1/fake_endpoint' }
3
- let(:http_method) { :post }
4
172
  let(:request_hash) { { fake: 'hash' } }
173
+ let(:http_method) { :post }
5
174
  let(:action) do
6
175
  instance_double(KnapsackPro::Client::API::Action,
7
176
  endpoint_path: endpoint_path,
8
177
  http_method: http_method,
9
178
  request_hash: request_hash)
10
179
  end
180
+ let(:test_suite_token) { '3fa64859337f6e56409d49f865d13fd7' }
11
181
 
12
182
  let(:connection) { described_class.new(action) }
13
183
 
14
184
  before do
15
185
  stub_const('ENV', {
16
186
  'KNAPSACK_PRO_ENDPOINT' => 'http://api.knapsackpro.test:3000',
17
- 'KNAPSACK_PRO_TEST_SUITE_TOKEN' => '3fa64859337f6e56409d49f865d13fd7',
187
+ 'KNAPSACK_PRO_TEST_SUITE_TOKEN' => test_suite_token,
18
188
  })
19
189
  end
20
190
 
21
191
  describe '#call' do
22
192
  let(:logger) { instance_double(Logger) }
193
+ let(:http) { instance_double(Net::HTTP) }
194
+ let(:http_response) do
195
+ header = { 'X-Request-Id' => 'fake-uuid' }
196
+ instance_double(Net::HTTPOK, body: body, header: header, code: code)
197
+ end
23
198
 
24
199
  subject { connection.call }
25
200
 
26
- context 'when http method is POST' do
27
- before do
28
- http = instance_double(Net::HTTP)
201
+ before do
202
+ expect(Net::HTTP).to receive(:new).with('api.knapsackpro.test', 3000).and_return(http)
29
203
 
30
- expect(Net::HTTP).to receive(:new).with('api.knapsackpro.test', 3000).and_return(http)
204
+ expect(http).to receive(:use_ssl=).with(false)
205
+ expect(http).to receive(:open_timeout=).with(15)
206
+ expect(http).to receive(:read_timeout=).with(15)
207
+ end
31
208
 
32
- expect(http).to receive(:use_ssl=).with(false)
33
- expect(http).to receive(:open_timeout=).with(15)
34
- expect(http).to receive(:read_timeout=).with(15)
209
+ context 'when http method is POST' do
210
+ let(:http_method) { :post }
35
211
 
36
- header = { 'X-Request-Id' => 'fake-uuid' }
37
- http_response = instance_double(Net::HTTPOK, body: body, header: header, code: code)
212
+ before do
38
213
  expect(http).to receive(:post).with(
39
214
  endpoint_path,
40
- "{\"fake\":\"hash\",\"test_suite_token\":\"3fa64859337f6e56409d49f865d13fd7\"}",
215
+ request_hash.to_json,
41
216
  {
42
217
  'Content-Type' => 'application/json',
43
218
  'Accept' => 'application/json',
44
219
  'KNAPSACK-PRO-CLIENT-NAME' => 'knapsack_pro-ruby',
45
220
  'KNAPSACK-PRO-CLIENT-VERSION' => KnapsackPro::VERSION,
221
+ 'KNAPSACK-PRO-TEST-SUITE-TOKEN' => test_suite_token,
46
222
  }
47
223
  ).and_return(http_response)
48
224
  end
49
225
 
50
- context 'when body response is json and API response code is 400' do
51
- let(:body) { '{"errors": "value"}' }
52
- let(:code) { '400' } # it must be string code
53
-
54
- before do
55
- expect(KnapsackPro).to receive(:logger).exactly(3).and_return(logger)
56
- expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
57
- expect(logger).to receive(:debug).with('API response:')
58
- end
59
-
60
- it do
61
- parsed_response = { 'errors' => 'value' }
62
-
63
- expect(logger).to receive(:error).with(parsed_response)
64
-
65
- expect(subject).to eq(parsed_response)
66
- expect(connection.success?).to be true
67
- expect(connection.errors?).to be true
68
- end
226
+ it_behaves_like 'when request got response from API' do
227
+ let(:expected_http_method) { 'POST' }
69
228
  end
229
+ end
70
230
 
71
- context 'when body response is json with build_distribution_id' do
72
- let(:body) { '{"build_distribution_id": "seed-uuid"}' }
73
- let(:code) { '200' } # it must be string code
74
-
75
- before do
76
- expect(KnapsackPro).to receive(:logger).exactly(4).and_return(logger)
77
- expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
78
- expect(logger).to receive(:debug).with("Test suite split seed: seed-uuid")
79
- expect(logger).to receive(:debug).with('API response:')
80
- end
81
-
82
- it do
83
- parsed_response = { 'build_distribution_id' => 'seed-uuid' }
84
-
85
- expect(logger).to receive(:debug).with(parsed_response)
231
+ context 'when http method is GET' do
232
+ let(:http_method) { :get }
86
233
 
87
- expect(subject).to eq(parsed_response)
88
- expect(connection.success?).to be true
89
- expect(connection.errors?).to be false
90
- end
234
+ before do
235
+ uri = URI.parse("http://api.knapsackpro.test:3000#{endpoint_path}")
236
+ uri.query = URI.encode_www_form(request_hash)
237
+ expect(http).to receive(:get).with(
238
+ uri,
239
+ {
240
+ 'Content-Type' => 'application/json',
241
+ 'Accept' => 'application/json',
242
+ 'KNAPSACK-PRO-CLIENT-NAME' => 'knapsack_pro-ruby',
243
+ 'KNAPSACK-PRO-CLIENT-VERSION' => KnapsackPro::VERSION,
244
+ 'KNAPSACK-PRO-TEST-SUITE-TOKEN' => test_suite_token,
245
+ }
246
+ ).and_return(http_response)
91
247
  end
92
248
 
93
- context 'when body response is empty' do
94
- let(:body) { '' }
95
- let(:code) { '200' } # it must be string code
96
-
97
- before do
98
- expect(KnapsackPro).to receive(:logger).exactly(3).and_return(logger)
99
- expect(logger).to receive(:debug).with('API request UUID: fake-uuid')
100
- expect(logger).to receive(:debug).with('API response:')
101
- end
102
-
103
- it do
104
- expect(logger).to receive(:debug).with('')
105
-
106
- expect(subject).to eq('')
107
- expect(connection.success?).to be true
108
- expect(connection.errors?).to be false
109
- end
249
+ it_behaves_like 'when request got response from API' do
250
+ let(:expected_http_method) { 'GET' }
110
251
  end
111
252
  end
112
253
 
113
254
  context 'when retry request for http method POST' do
114
- before do
115
- http = instance_double(Net::HTTP)
116
-
117
- expect(Net::HTTP).to receive(:new).exactly(3).with('api.knapsackpro.test', 3000).and_return(http)
255
+ let(:http_method) { :post }
118
256
 
119
- expect(http).to receive(:use_ssl=).exactly(3).with(false)
120
- expect(http).to receive(:open_timeout=).exactly(3).with(15)
121
- expect(http).to receive(:read_timeout=).exactly(3).with(15)
122
-
123
- header = { 'X-Request-Id' => 'fake-uuid' }
124
- http_response = instance_double(Net::HTTPOK, body: body, header: header, code: code)
125
- expect(http).to receive(:post).exactly(3).with(
257
+ before do
258
+ expect(http).to receive(:post).at_least(3).with(
126
259
  endpoint_path,
127
- "{\"fake\":\"hash\",\"test_suite_token\":\"3fa64859337f6e56409d49f865d13fd7\"}",
260
+ request_hash.to_json,
128
261
  {
129
262
  'Content-Type' => 'application/json',
130
263
  'Accept' => 'application/json',
131
264
  'KNAPSACK-PRO-CLIENT-NAME' => 'knapsack_pro-ruby',
132
265
  'KNAPSACK-PRO-CLIENT-VERSION' => KnapsackPro::VERSION,
266
+ 'KNAPSACK-PRO-TEST-SUITE-TOKEN' => test_suite_token,
133
267
  }
134
268
  ).and_return(http_response)
135
269
  end
136
270
 
137
- context 'when body response is json and API response code is 500' do
138
- let(:body) { '{"error": "Internal Server Error"}' }
139
- let(:code) { '500' } # it must be string code
140
-
141
- before do
142
- expect(KnapsackPro).to receive(:logger).at_least(1).and_return(logger)
143
- expect(logger).to receive(:debug).exactly(3).with('API request UUID: fake-uuid')
144
- expect(logger).to receive(:debug).exactly(3).with('API response:')
145
- end
146
-
147
- it do
148
- parsed_response = { 'error' => 'Internal Server Error' }
149
-
150
- expect(logger).to receive(:error).exactly(3).with(parsed_response)
151
-
152
- server_error = described_class::ServerError.new(parsed_response)
153
- expect(logger).to receive(:warn).exactly(3).with(server_error.inspect)
271
+ it_behaves_like 'when retry request' do
272
+ let(:expected_http_method) { 'POST' }
273
+ end
274
+ end
154
275
 
155
- expect(logger).to receive(:warn).with("Wait 4s and retry request to Knapsack Pro API.")
156
- expect(logger).to receive(:warn).with("Wait 8s and retry request to Knapsack Pro API.")
157
- expect(Kernel).to receive(:sleep).with(4)
158
- expect(Kernel).to receive(:sleep).with(8)
276
+ context 'when retry request for http method GET' do
277
+ let(:http_method) { :get }
159
278
 
160
- expect(subject).to eq(parsed_response)
279
+ before do
280
+ uri = URI.parse("http://api.knapsackpro.test:3000#{endpoint_path}")
281
+ uri.query = URI.encode_www_form(request_hash)
282
+ expect(http).to receive(:get).at_least(3).with(
283
+ uri,
284
+ {
285
+ 'Content-Type' => 'application/json',
286
+ 'Accept' => 'application/json',
287
+ 'KNAPSACK-PRO-CLIENT-NAME' => 'knapsack_pro-ruby',
288
+ 'KNAPSACK-PRO-CLIENT-VERSION' => KnapsackPro::VERSION,
289
+ 'KNAPSACK-PRO-TEST-SUITE-TOKEN' => test_suite_token,
290
+ }
291
+ ).and_return(http_response)
292
+ end
161
293
 
162
- expect(connection.success?).to be false
163
- expect(connection.errors?).to be true
164
- end
294
+ it_behaves_like 'when retry request' do
295
+ let(:expected_http_method) { 'GET' }
165
296
  end
166
297
  end
167
298
  end
@@ -183,6 +183,20 @@ describe KnapsackPro::Config::Env do
183
183
  end
184
184
  end
185
185
 
186
+ describe '.slow_test_file_pattern' do
187
+ subject { described_class.slow_test_file_pattern }
188
+
189
+ context 'when ENV exists' do
190
+ let(:slow_test_file_pattern) { 'spec/features/*_spec.rb' }
191
+ before { stub_const("ENV", { 'KNAPSACK_PRO_SLOW_TEST_FILE_PATTERN' => slow_test_file_pattern }) }
192
+ it { should eq slow_test_file_pattern }
193
+ end
194
+
195
+ context "when ENV doesn't exist" do
196
+ it { should be_nil }
197
+ end
198
+ end
199
+
186
200
  describe '.test_file_exclude_pattern' do
187
201
  subject { described_class.test_file_exclude_pattern }
188
202