gooddata 0.6.16 → 0.6.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/lib/gooddata/cli/commands/project_cmd.rb +1 -1
- data/lib/gooddata/core/logging.rb +15 -5
- data/lib/gooddata/core/rest.rb +4 -28
- data/lib/gooddata/helpers/global_helpers.rb +14 -138
- data/lib/gooddata/helpers/global_helpers_params.rb +145 -0
- data/lib/gooddata/mixins/md_object_indexer.rb +2 -2
- data/lib/gooddata/models/domain.rb +1 -1
- data/lib/gooddata/models/execution.rb +29 -1
- data/lib/gooddata/models/from_wire.rb +6 -0
- data/lib/gooddata/models/from_wire_parse.rb +125 -0
- data/lib/gooddata/models/metadata/attribute.rb +1 -1
- data/lib/gooddata/models/metadata/label.rb +11 -10
- data/lib/gooddata/models/model.rb +4 -0
- data/lib/gooddata/models/profile.rb +12 -2
- data/lib/gooddata/models/project.rb +6 -3
- data/lib/gooddata/models/project_blueprint.rb +4 -4
- data/lib/gooddata/models/project_creator.rb +8 -10
- data/lib/gooddata/models/report_data_result.rb +4 -2
- data/lib/gooddata/models/schedule.rb +121 -66
- data/lib/gooddata/models/to_wire.rb +12 -3
- data/lib/gooddata/models/user_filters/user_filter_builder.rb +3 -234
- data/lib/gooddata/models/user_filters/user_filter_builder_create.rb +115 -0
- data/lib/gooddata/models/user_filters/user_filter_builder_execute.rb +133 -0
- data/lib/gooddata/rest/client.rb +27 -13
- data/lib/gooddata/rest/connection.rb +102 -23
- data/lib/gooddata/version.rb +1 -1
- data/spec/data/gd_gse_data_blueprint.json +1 -0
- data/spec/data/test_project_model_spec.json +5 -2
- data/spec/data/wire_models/model_view.json +3 -0
- data/spec/data/wire_test_project.json +8 -1
- data/spec/integration/full_project_spec.rb +1 -1
- data/spec/unit/core/connection_spec.rb +16 -0
- data/spec/unit/core/logging_spec.rb +54 -6
- data/spec/unit/models/domain_spec.rb +10 -4
- data/spec/unit/models/execution_spec.rb +102 -0
- data/spec/unit/models/from_wire_spec.rb +11 -2
- data/spec/unit/models/model_spec.rb +2 -2
- data/spec/unit/models/project_blueprint_spec.rb +1 -1
- data/spec/unit/models/schedule_spec.rb +34 -24
- data/spec/unit/models/to_wire_spec.rb +9 -1
- metadata +8 -3
@@ -40,7 +40,7 @@ describe "Full project implementation", :constraint => 'slow' do
|
|
40
40
|
expect(results).to be_nil
|
41
41
|
|
42
42
|
# When we change the model using the original blueprint. Basically change the title back.
|
43
|
-
results =
|
43
|
+
results = @project.update_from_blueprint(@spec)
|
44
44
|
# It should offer no changes using the original blueprint
|
45
45
|
results = GoodData::Model::ProjectCreator.migrate_datasets(@spec, project: @project, client: @client, dry_run: true)
|
46
46
|
expect(results).to be_nil
|
@@ -32,4 +32,20 @@ describe GoodData::Rest::Connection do
|
|
32
32
|
c.disconnect
|
33
33
|
end
|
34
34
|
end
|
35
|
+
|
36
|
+
describe '#generate_request_id' do
|
37
|
+
it "Generates a non-empty string" do
|
38
|
+
c = ConnectionHelper.create_default_connection
|
39
|
+
|
40
|
+
# generate a request id, and pass it to a request
|
41
|
+
id = c.generate_request_id
|
42
|
+
resp = c.get('/gdc/md', :request_id => id)
|
43
|
+
|
44
|
+
id.should be_a(String)
|
45
|
+
id.should_not be_empty
|
46
|
+
|
47
|
+
c.disconnect
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
35
51
|
end
|
@@ -1,6 +1,30 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
3
|
require 'gooddata/core/logging'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
# Logger that remembers the last logged message
|
7
|
+
class TestLogger < Logger
|
8
|
+
attr_reader :last_message
|
9
|
+
def debug(*args)
|
10
|
+
@last_message = args[0]
|
11
|
+
super(*args)
|
12
|
+
end
|
13
|
+
def info(*args)
|
14
|
+
@last_message = args[0]
|
15
|
+
super(*args)
|
16
|
+
end
|
17
|
+
def warn(*args)
|
18
|
+
@last_message = args[0]
|
19
|
+
super(*args)
|
20
|
+
end
|
21
|
+
def error(*args)
|
22
|
+
@last_message = args[0]
|
23
|
+
super(*args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
4
28
|
|
5
29
|
describe 'GoodData - logging' do
|
6
30
|
TEST_MESSAGE = 'Hello World!'
|
@@ -17,21 +41,45 @@ describe 'GoodData - logging' do
|
|
17
41
|
GoodData.logger.warn TEST_MESSAGE
|
18
42
|
end
|
19
43
|
|
44
|
+
def test_request_id_logging
|
45
|
+
c = ConnectionHelper.create_default_connection
|
46
|
+
id = c.generate_request_id
|
47
|
+
GoodData.logger.info "Request id: #{id} Doing something very useful"
|
48
|
+
c.get('/gdc/md', :request_id => id)
|
49
|
+
id
|
50
|
+
end
|
51
|
+
|
20
52
|
def test_all
|
21
53
|
test_error
|
22
54
|
test_info
|
23
|
-
|
55
|
+
test_warn
|
56
|
+
test_request_id_logging
|
24
57
|
end
|
25
58
|
|
26
59
|
before(:each) do
|
27
|
-
|
28
|
-
|
29
|
-
# TODO: Use some kind of 'reset' instead
|
30
|
-
GoodData.logging_on if !@is_logging_on
|
60
|
+
# remember the state of logging before
|
61
|
+
@logging_on_at_start = GoodData.logging_on?
|
31
62
|
end
|
32
63
|
|
33
64
|
after(:each) do
|
34
|
-
|
65
|
+
# restore the logging state
|
66
|
+
if @logging_on_at_start
|
67
|
+
GoodData.logging_on
|
68
|
+
else
|
69
|
+
GoodData.logging_off
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#logger' do
|
74
|
+
it "can assign a custom logger" do
|
75
|
+
GoodData.logger = TestLogger.new(STDOUT)
|
76
|
+
test_all
|
77
|
+
end
|
78
|
+
it 'has the request id logged when I passed it' do
|
79
|
+
GoodData.logger = TestLogger.new(STDOUT)
|
80
|
+
id = test_request_id_logging
|
81
|
+
expect(GoodData.logger.last_message).to include(id)
|
82
|
+
end
|
35
83
|
end
|
36
84
|
|
37
85
|
|
@@ -125,12 +125,18 @@ describe GoodData::Domain do
|
|
125
125
|
.reject { |u| u.login == ConnectionHelper::DEFAULT_USERNAME }.sample
|
126
126
|
|
127
127
|
old_email = user.email
|
128
|
+
old_sso_provider = user.sso_provider || ''
|
128
129
|
user.email = 'john.doe@gooddata.com'
|
130
|
+
user.sso_provider = user.sso_provider ? user.sso_provider.reverse : 'some_sso_provider'
|
129
131
|
@domain.update_user(user)
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
132
|
+
updated_user = @domain.find_user_by_login(user.login)
|
133
|
+
expect(updated_user.email).to eq 'john.doe@gooddata.com'
|
134
|
+
expect(updated_user.sso_provider).to eq 'some_sso_provider'
|
135
|
+
updated_user.email = old_email
|
136
|
+
updated_user.sso_provider = old_sso_provider
|
137
|
+
@domain.update_user(updated_user)
|
138
|
+
expect(@domain.find_user_by_login(user.login).email).to eq old_email
|
139
|
+
expect(@domain.find_user_by_login(user.login).sso_provider).to eq old_sso_provider
|
134
140
|
end
|
135
141
|
end
|
136
142
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'gooddata/models/schedule'
|
2
|
+
|
3
|
+
describe GoodData::Execution do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@data = {"execution"=>
|
7
|
+
{"startTime"=>"2015-02-27T15:44:21.759Z",
|
8
|
+
"endTime"=>"2015-02-27T15:47:49.383Z",
|
9
|
+
"log"=>
|
10
|
+
"/gdc/projects/tk3b994vmdpcb0xjwexc9moen8t5bpiw/dataload/processes/2b031451-b1a2-4039-8e36-0672542a0e60/executions/54f090d5e4b0c9cbdcb0f45b/log",
|
11
|
+
"status"=>"OK",
|
12
|
+
"trigger"=>"MANUAL",
|
13
|
+
"links"=>
|
14
|
+
{"self"=>
|
15
|
+
"/gdc/projects/tk3b994vmdpcb0xjwexc9moen8t5bpiw/schedules/54f08d1de4b0c9cbdcb0f323/executions/54f090d5e4b0c9cbdcb0f45b"},
|
16
|
+
"createdTime"=>"2015-02-27T15:44:21.361Z"}}
|
17
|
+
@execution = GoodData::Execution.new(@data)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#created' do
|
21
|
+
it 'returns created as a Time instance' do
|
22
|
+
expect(@execution.created.class).to eq Time
|
23
|
+
expect(@execution.created.to_s).to eq '2015-02-27 15:44:21 UTC'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#error?' do
|
28
|
+
it 'returns true if executione errored out' do
|
29
|
+
expect(@execution.error?).to be_falsy
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#ok?' do
|
34
|
+
it 'returns true if executione finished ok' do
|
35
|
+
expect(@execution.ok?).to be_truthy
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#finished' do
|
40
|
+
it 'returns time when execution finished' do
|
41
|
+
expect(@execution.finished.class).to eq Time
|
42
|
+
expect(@execution.finished.to_s).to eq '2015-02-27 15:47:49 UTC'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns nil if it is not finished' do
|
46
|
+
@data['execution']['status'] = 'RUNNING'
|
47
|
+
@data['execution']['endTime'] = nil
|
48
|
+
running_execution = GoodData::Execution.new(@data)
|
49
|
+
expect(running_execution.finished).to be_nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#schedule_uri' do
|
54
|
+
it 'returns uri of schedule that was executed' do
|
55
|
+
expect(@execution.schedule_uri).to eq '/gdc/projects/tk3b994vmdpcb0xjwexc9moen8t5bpiw/schedules/54f08d1de4b0c9cbdcb0f323'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#running?' do
|
60
|
+
it 'returns false if executione is already finished' do
|
61
|
+
expect(@execution.running?).to be_falsy
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'returns true if executione is currently finished' do
|
65
|
+
@data['execution']['status'] = 'RUNNING'
|
66
|
+
running_execution = GoodData::Execution.new(@data)
|
67
|
+
expect(running_execution.running?).to be_truthy
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#started' do
|
72
|
+
it 'returns time when execution started' do
|
73
|
+
expect(@execution.started.class).to eq Time
|
74
|
+
expect(@execution.started.to_s).to eq '2015-02-27 15:44:21 UTC'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#status' do
|
79
|
+
it 'returns :ok for finished execution' do
|
80
|
+
expect(@execution.status).to eq :ok
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#uri' do
|
85
|
+
it 'returns time when execution started' do
|
86
|
+
expect(@execution.uri).to eq '/gdc/projects/tk3b994vmdpcb0xjwexc9moen8t5bpiw/schedules/54f08d1de4b0c9cbdcb0f323/executions/54f090d5e4b0c9cbdcb0f45b'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#duration' do
|
91
|
+
it 'returns time it took to run execution' do
|
92
|
+
expect(@execution.duration).to eq 207.624
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'returns nil if it is not finished' do
|
96
|
+
@data['execution']['status'] = 'RUNNING'
|
97
|
+
@data['execution']['endTime'] = nil
|
98
|
+
running_execution = GoodData::Execution.new(@data)
|
99
|
+
expect(running_execution.duration.class).to eq Float
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -42,11 +42,11 @@ describe GoodData::Model::FromWire do
|
|
42
42
|
end
|
43
43
|
|
44
44
|
it "should enable sorting" do
|
45
|
-
|
45
|
+
skip("UAAA")
|
46
46
|
end
|
47
47
|
|
48
48
|
it "should allow defining date dimensions" do
|
49
|
-
|
49
|
+
skip('UAAA')
|
50
50
|
end
|
51
51
|
|
52
52
|
it "should generate the same thing it parsed" do
|
@@ -61,6 +61,7 @@ describe GoodData::Model::FromWire do
|
|
61
61
|
{
|
62
62
|
type: 'anchor',
|
63
63
|
name: "techoppanalysis",
|
64
|
+
folder: "Opportunity Benchmark",
|
64
65
|
title: "Tech Opp. Analysis",
|
65
66
|
gd_data_type: "VARCHAR(128)",
|
66
67
|
gd_type: "GDC.text",
|
@@ -73,6 +74,7 @@ describe GoodData::Model::FromWire do
|
|
73
74
|
expect(x).to eq [
|
74
75
|
{
|
75
76
|
:type=>'attribute',
|
77
|
+
:folder => "Opportunity Benchmark",
|
76
78
|
:name=>"month",
|
77
79
|
:gd_data_type=>"VARCHAR(128)",
|
78
80
|
:gd_type=>"GDC.text",
|
@@ -88,6 +90,7 @@ describe GoodData::Model::FromWire do
|
|
88
90
|
},
|
89
91
|
{
|
90
92
|
:type=>'attribute',
|
93
|
+
:folder => "Opportunity Benchmark",
|
91
94
|
:name=>"cohorttype",
|
92
95
|
:title=>"Cohort Type",
|
93
96
|
:gd_data_type=>"VARCHAR(128)",
|
@@ -117,6 +120,12 @@ describe GoodData::Model::FromWire do
|
|
117
120
|
}]
|
118
121
|
end
|
119
122
|
|
123
|
+
it "should be able to parse description from both attributes and facts" do
|
124
|
+
expect(@blueprint.find_dataset('opportunity').anchor[:description]).to eq 'This is opportunity attribute description'
|
125
|
+
expect(@blueprint.find_dataset('stage_history').facts.find {|f| f[:name] == 'stage_velocity'}[:description]).to eq 'Velocity description'
|
126
|
+
expect(@blueprint.find_dataset('opp_owner').attributes.find {|f| f[:name] == 'region'}[:description]).to eq 'Owner Region description'
|
127
|
+
end
|
128
|
+
|
120
129
|
it "should be able to deal with fiscal dimensions with weird names" do
|
121
130
|
model_view = MultiJson.load(File.read('./spec/data/wire_models/nu_model.json'))
|
122
131
|
blueprint = FromWire.from_wire(model_view)
|
@@ -16,7 +16,7 @@ describe GoodData::Model do
|
|
16
16
|
{
|
17
17
|
:name => "commits",
|
18
18
|
:columns => [
|
19
|
-
{:type => "fact", :name => "lines_changed"}
|
19
|
+
{:type => "fact", :name => "lines_changed", :description=>"Fact description"}
|
20
20
|
]
|
21
21
|
}
|
22
22
|
]})
|
@@ -49,7 +49,7 @@ describe GoodData::Model do
|
|
49
49
|
stuff = GoodData::Model.merge_dataset_columns(first_dataset, additional_blueprint)
|
50
50
|
|
51
51
|
stuff[:columns].count.should == 4
|
52
|
-
stuff[:columns].include?({:type => "fact", :name => "lines_changed"}).should == true
|
52
|
+
stuff[:columns].include?({:type => "fact", :name => "lines_changed", :description=>"Fact description"}).should == true
|
53
53
|
stuff[:columns].group_by { |col| col[:name] }["lines_changed"].count.should == 1
|
54
54
|
end
|
55
55
|
|
@@ -174,7 +174,7 @@ describe GoodData::Model::ProjectBlueprint do
|
|
174
174
|
end
|
175
175
|
dataset = builder.to_blueprint
|
176
176
|
@blueprint.datasets.count.should == 3
|
177
|
-
@blueprint.add_dataset(dataset)
|
177
|
+
@blueprint.add_dataset!(dataset)
|
178
178
|
@blueprint.datasets.count.should == 4
|
179
179
|
end
|
180
180
|
|
@@ -4,8 +4,6 @@ describe GoodData::Schedule do
|
|
4
4
|
SCHEDULE_ID = ScheduleHelper::SCHEDULE_ID
|
5
5
|
SCHEDULE_URL = "/gdc/projects/#{ProjectHelper::PROJECT_ID}/schedules/#{SCHEDULE_ID}"
|
6
6
|
|
7
|
-
@test_cron = '0 15 27 7 *'
|
8
|
-
|
9
7
|
before(:all) do
|
10
8
|
@client = ConnectionHelper.create_default_connection
|
11
9
|
|
@@ -20,11 +18,16 @@ describe GoodData::Schedule do
|
|
20
18
|
|
21
19
|
@project = ProjectHelper.get_default_project(:client => @client)
|
22
20
|
@project_executable = 'graph/graph.grf'
|
21
|
+
@test_cron = '0 15 27 7 *'
|
23
22
|
@test_data = {
|
24
23
|
:timezone => 'UTC',
|
25
24
|
:cron => '2 2 2 2 *',
|
26
25
|
:client => @client,
|
27
|
-
:project => @project
|
26
|
+
:project => @project,
|
27
|
+
:params => {
|
28
|
+
'a' => 'b',
|
29
|
+
'b' => 'c'
|
30
|
+
}
|
28
31
|
}
|
29
32
|
|
30
33
|
@test_data_with_optional_param = {
|
@@ -76,12 +79,8 @@ describe GoodData::Schedule do
|
|
76
79
|
|
77
80
|
describe '#create' do
|
78
81
|
it 'Creates new schedule if mandatory params passed' do
|
79
|
-
schedule = nil
|
80
82
|
begin
|
81
|
-
|
82
|
-
schedule = @project.create_schedule(ProcessHelper::PROCESS_ID, @test_cron, @project_executable, @test_data)
|
83
|
-
}.not_to raise_error
|
84
|
-
|
83
|
+
schedule = @project.create_schedule(ProcessHelper::PROCESS_ID, @test_cron, @project_executable, @test_data)
|
85
84
|
expect(schedule).to be_truthy
|
86
85
|
ensure
|
87
86
|
schedule && schedule.delete
|
@@ -89,12 +88,8 @@ describe GoodData::Schedule do
|
|
89
88
|
end
|
90
89
|
|
91
90
|
it 'Creates new schedule if mandatory params passed and optional params are present' do
|
92
|
-
schedule = nil
|
93
91
|
begin
|
94
|
-
|
95
|
-
schedule = @project.create_schedule(ProcessHelper::PROCESS_ID, @test_cron, @project_executable, @test_data_with_optional_param)
|
96
|
-
}.not_to raise_error
|
97
|
-
|
92
|
+
schedule = @project.create_schedule(ProcessHelper::PROCESS_ID, @test_cron, @project_executable, @test_data_with_optional_param)
|
98
93
|
expect(schedule).to be_truthy
|
99
94
|
ensure
|
100
95
|
schedule && schedule.delete
|
@@ -138,25 +133,26 @@ describe GoodData::Schedule do
|
|
138
133
|
|
139
134
|
it 'Throws exception when no timezone specified' do
|
140
135
|
data = @test_data.deep_dup
|
141
|
-
|
142
|
-
schedule = nil
|
136
|
+
schedule = @project.create_schedule(ProcessHelper::PROCESS_ID, @test_cron, @project_executable, data)
|
137
|
+
schedule.timezone = nil
|
143
138
|
begin
|
144
139
|
expect {
|
145
|
-
schedule
|
140
|
+
schedule.save
|
146
141
|
}.to raise_error 'A timezone has to be provided'
|
147
142
|
ensure
|
148
143
|
schedule && schedule.delete
|
149
144
|
end
|
150
145
|
end
|
151
146
|
|
152
|
-
it 'Throws exception when no
|
153
|
-
data = @test_data.deep_dup
|
154
|
-
data[:type] = nil
|
147
|
+
it 'Throws exception when no schedule type is specified' do
|
155
148
|
schedule = nil
|
149
|
+
data = @test_data.deep_dup
|
156
150
|
begin
|
157
|
-
expect {
|
158
151
|
schedule = @project.create_schedule(ProcessHelper::PROCESS_ID, @test_cron, @project_executable, data)
|
159
|
-
|
152
|
+
schedule.type = nil
|
153
|
+
expect {
|
154
|
+
schedule.save
|
155
|
+
}.to raise_error 'Schedule type has to be provided'
|
160
156
|
ensure
|
161
157
|
schedule && schedule.delete
|
162
158
|
end
|
@@ -165,13 +161,13 @@ describe GoodData::Schedule do
|
|
165
161
|
|
166
162
|
describe '#cron' do
|
167
163
|
it 'Should return cron as string' do
|
168
|
-
schedule = nil
|
169
164
|
begin
|
170
165
|
schedule = @project.create_schedule(ProcessHelper::PROCESS_ID, @test_cron, @project_executable, @test_data)
|
171
166
|
res = schedule.cron
|
172
167
|
res.should_not be_nil
|
173
168
|
res.should_not be_empty
|
174
169
|
res.should be_a_kind_of(String)
|
170
|
+
expect(schedule.time_based?).to be_truthy
|
175
171
|
ensure
|
176
172
|
schedule && schedule.delete
|
177
173
|
end
|
@@ -334,12 +330,14 @@ describe GoodData::Schedule do
|
|
334
330
|
old_params = schedule.params
|
335
331
|
|
336
332
|
test_params = {
|
337
|
-
'
|
333
|
+
'some_new_param' => '1-2-3-4'
|
338
334
|
}
|
339
335
|
|
340
336
|
schedule.params = test_params
|
341
|
-
expect(schedule.params).to eq(
|
337
|
+
expect(schedule.params.keys).to eq(%w(PROCESS_ID EXECUTABLE some_new_param))
|
338
|
+
expect(schedule.params['some_new_param']).to eq '1-2-3-4'
|
342
339
|
expect(schedule.dirty).to eq(true)
|
340
|
+
schedule.save
|
343
341
|
ensure
|
344
342
|
schedule && schedule.delete
|
345
343
|
end
|
@@ -486,4 +484,16 @@ describe GoodData::Schedule do
|
|
486
484
|
end
|
487
485
|
end
|
488
486
|
end
|
487
|
+
|
488
|
+
describe '#executions' do
|
489
|
+
it 'Returns executions' do
|
490
|
+
begin
|
491
|
+
schedule = @project.create_schedule(ProcessHelper::PROCESS_ID, @test_cron, @project_executable, @test_data_with_optional_param)
|
492
|
+
expect(schedule.executions).to be_empty
|
493
|
+
schedule.execute
|
494
|
+
ensure
|
495
|
+
schedule && schedule.delete
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
489
499
|
end
|