• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

observatorycontrolsystem / observation-portal / 21694896293

05 Feb 2026 01:14AM UTC coverage: 96.575% (-0.002%) from 96.577%
21694896293

push

github

web-flow
Merge pull request #352 from observatorycontrolsystem/feature/suspend_scheduling

Added suspend_until field to Requst and serializer and used it to fil…

182 of 184 new or added lines in 6 files covered. (98.91%)

59 existing lines in 6 files now uncovered.

36124 of 37405 relevant lines covered (96.58%)

1.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.78
/observation_portal/requestgroups/test/test.py
1
from django.utils import timezone
1✔
2
from django.test import TestCase
2✔
3
from mixer.backend.django import mixer
2✔
4
from rest_framework.serializers import ValidationError
2✔
5
from datetime import datetime, timedelta
2✔
6
from unittest.mock import patch
2✔
7
import math
2✔
8
import copy
2✔
9

10
from observation_portal.requestgroups.models import (
2✔
11
    Request, Configuration, Target, RequestGroup, Window, Location, Constraints, InstrumentConfig,
12
    AcquisitionConfig, GuidingConfig
13
)
14
from observation_portal.proposals.models import Proposal, TimeAllocation, Semester
2✔
15
from observation_portal.common.configdb import ConfigDBException, configdb
2✔
16
from observation_portal.common.test_helpers import SetTimeMixin
2✔
17
from observation_portal.requestgroups.duration_utils import PER_CONFIGURATION_STARTUP_TIME
2✔
18
from observation_portal.requestgroups.serializers import ConfigurationTypeValidationHelper, InstrumentTypeValidationHelper, ModeValidationHelper
2✔
19
from observation_portal.requestgroups.test.test_api import generic_payload
2✔
20
from observation_portal.observations.models import Observation
2✔
21

22

23
class TestRequestGroupTotalDuration(SetTimeMixin, TestCase):
2✔
24
    def setUp(self):
2✔
25
        super().setUp()
2✔
26
        self.proposal = mixer.blend(Proposal)
2✔
27
        semester = mixer.blend(
2✔
28
            Semester, id='2016B', start=datetime(2016, 9, 1, tzinfo=timezone.utc),
29
            end=datetime(2016, 12, 31, tzinfo=timezone.utc)
30
        )
31
        self.time_allocation_1m0 = mixer.blend(
2✔
32
            TimeAllocation, proposal=self.proposal, semester=semester, std_allocation=100.0, std_time_used=0.0,
33
            instrument_types=['1M0-SCICAM-SBIG'], rr_allocation=10, rr_time_used=0.0, ipp_limit=10.0,
34
            ipp_time_available=5.0
35
        )
36
        self.rg_single = mixer.blend(RequestGroup, proposal=self.proposal, operator='SINGLE',
2✔
37
                                     observation_type=RequestGroup.NORMAL)
38
        self.rg_many = mixer.blend(RequestGroup, proposal=self.proposal,
2✔
39
                                   observation_type=RequestGroup.NORMAL)
40

41
        self.request = mixer.blend(Request, request_group=self.rg_single)
2✔
42
        self.requests = mixer.cycle(3).blend(Request, request_group=self.rg_many)
2✔
43

44
        self.configuration_expose = mixer.blend(
2✔
45
            Configuration, request=self.request, instrument_type='1M0-SCICAM-SBIG', type='EXPOSE'
46
        )
47
        self.configuration_exposes = mixer.cycle(3).blend(
2✔
48
            Configuration, request=(r for r in self.requests),  instrument_type='1M0-SCICAM-SBIG', type='EXPOSE'
49
        )
50
        self.instrument_config = mixer.blend(
2✔
51
            InstrumentConfig, configuration=self.configuration_expose, mode='1m0_sbig_2',
52
            optical_elements={'filter': 'blah'}, exposure_time=600, exposure_count=2, extra_params={'bin_x': 2, 'bin_y': 2}
53
        )
54
        self.instrument_configs = mixer.cycle(3).blend(
2✔
55
            InstrumentConfig, configuration=(c for c in self.configuration_exposes), extra_params={'bin_x': 2, 'bin_y': 2},
56
            optical_elements=({'filter': f} for f in ('uv', 'uv', 'ir')), mode='1m0_sbig_2', exposure_time=1000, exposure_count=1,
57
        )
58
        mixer.blend(
2✔
59
            Window, request=self.request, start=datetime(2016, 9, 29, tzinfo=timezone.utc),
60
            end=datetime(2016, 10, 29, tzinfo=timezone.utc)
61
        )
62
        mixer.cycle(3).blend(
2✔
63
            Window, request=(r for r in self.requests), start=datetime(2016, 9, 29, tzinfo=timezone.utc),
64
            end=datetime(2016, 10, 29, tzinfo=timezone.utc)
65
        )
66
        mixer.blend(Target, configuration=self.configuration_expose)
2✔
67
        mixer.cycle(3).blend(Target, configuration=(c for c in self.configuration_exposes))
2✔
68

69
        mixer.blend(Location, request=self.request, telescope_class='1m0')
2✔
70
        mixer.cycle(3).blend(Location, request=(r for r in self.requests), telescope_class='1m0')
2✔
71

72
        mixer.blend(Constraints, configuration=self.configuration_expose)
2✔
73
        mixer.cycle(3).blend(Constraints, configuration=(c for c in self.configuration_exposes))
2✔
74

75
        mixer.blend(AcquisitionConfig, configuration=self.configuration_expose)
2✔
76
        mixer.cycle(3).blend(AcquisitionConfig, configuration=(c for c in self.configuration_exposes))
2✔
77

78
        mixer.blend(GuidingConfig, configuration=self.configuration_expose)
2✔
79
        mixer.cycle(3).blend(GuidingConfig, configuration=(c for c in self.configuration_exposes))
2✔
80

81
    def test_single_rg_total_duration(self):
2✔
82
        request_duration = self.request.duration
2✔
83
        total_duration = self.rg_single.total_duration
2✔
84
        taks = self.request.time_allocation_keys
2✔
85
        self.assertEqual(request_duration, total_duration[taks[0]])
2✔
86

87
    def test_many_rg_takes_highest_duration(self):
2✔
88
        self.rg_many.operator = 'MANY'
2✔
89
        self.rg_many.save()
2✔
90

91
        highest_duration = max(r.duration for r in self.requests)
2✔
92
        total_duration = self.rg_many.total_duration
2✔
93
        taks = self.requests[0].time_allocation_keys
2✔
94
        self.assertEqual(highest_duration, total_duration[taks[0]])
2✔
95

96
    def test_and_rg_takes_sum_of_durations(self):
2✔
97
        self.rg_many.operator = 'AND'
2✔
98
        self.rg_many.save()
2✔
99

100
        sum_duration = sum(r.duration for r in self.requests)
2✔
101
        total_duration = self.rg_many.total_duration
2✔
102
        taks = self.requests[0].time_allocation_keys
2✔
103
        self.assertEqual(sum_duration, total_duration[taks[0]])
2✔
104

105

106
class TestRequestDuration(SetTimeMixin, TestCase):
2✔
107
    def setUp(self):
2✔
108
        super().setUp()
2✔
109
        self.request = mixer.blend(Request)
2✔
110
        mixer.blend(Location, request=self.request)
2✔
111

112
        self.configuration_expose = mixer.blend(Configuration, instrument_type='1M0-SCICAM-SBIG', type='EXPOSE')
2✔
113
        self.configuration_expose_2 = mixer.blend(Configuration, instrument_type='1M0-SCICAM-SBIG', type='EXPOSE')
2✔
114
        self.configuration_expose_3 = mixer.blend(Configuration, instrument_type='1M0-SCICAM-SBIG', type='EXPOSE')
2✔
115
        self.configuration_spectrum = mixer.blend(Configuration, instrument_type='2M0-FLOYDS-SCICAM', type='SPECTRUM')
2✔
116
        self.configuration_spectrum_2 = mixer.blend(Configuration, instrument_type='2M0-FLOYDS-SCICAM', type='SPECTRUM')
2✔
117
        self.configuration_arc = mixer.blend(Configuration, instrument_type='2M0-FLOYDS-SCICAM', type='ARC')
2✔
118
        self.configuration_lampflat = mixer.blend(Configuration, instrument_type='2M0-FLOYDS-SCICAM', type='LAMP_FLAT')
2✔
119
        self.configuration_repeat_expose = mixer.blend(Configuration, instrument_type='1M0-SCICAM-SBIG',
2✔
120
                                                       type='REPEAT_EXPOSE', repeat_duration=500)
121

122
        configurations = [self.configuration_expose, self.configuration_spectrum, self.configuration_arc,
2✔
123
                          self.configuration_lampflat, self.configuration_expose_2, self.configuration_expose_3,
124
                          self.configuration_spectrum_2, self.configuration_repeat_expose]
125

126
        mixer.cycle(len(configurations)).blend(AcquisitionConfig, configuration=(c for c in configurations))
2✔
127
        mixer.cycle(len(configurations)).blend(GuidingConfig, configuration=(c for c in configurations))
2✔
128
        mixer.cycle(len(configurations)).blend(Constraints, configuration=(c for c in configurations))
2✔
129
        mixer.cycle(len(configurations)).blend(Target, configuration=(c for c in configurations), type='ICRS',
2✔
130
                                               ra=10.0, dec=10.0, proper_motion_ra=0, proper_motion_dec=0)
131

132
        self.instrument_config_expose = mixer.blend(
2✔
133
            InstrumentConfig, extra_params={'bin_x': 2, 'bin_y': 2}, exposure_time=600, exposure_count=2,
134
            optical_elements={'filter': 'blah'}, mode='1m0_sbig_2'
135
        )
136
        self.instrument_config_expose_1 = mixer.blend(
2✔
137
            InstrumentConfig, extra_params={'bin_x': 2, 'bin_y': 2}, exposure_time=1000, exposure_count=1,
138
            optical_elements={'filter': 'uv'}, mode='1m0_sbig_2'
139
        )
140
        self.instrument_config_expose_2 = mixer.blend(
2✔
141
            InstrumentConfig, extra_params={'bin_x': 2, 'bin_y': 2}, exposure_time=10, exposure_count=5,
142
            optical_elements={'filter': 'uv'}, mode='1m0_sbig_2'
143
        )
144
        self.instrument_config_expose_3 = mixer.blend(
2✔
145
            InstrumentConfig, extra_params={'bin_x': 2, 'bin_y': 2}, exposure_time=3, exposure_count=3,
146
            optical_elements={'filter': 'ir'}, mode='1m0_sbig_2'
147
        )
148
        self.instrument_config_spectrum = mixer.blend(
2✔
149
            InstrumentConfig, extra_params={'bin_x': 1, 'bin_y': 1}, exposure_time=1800, exposure_count=1,
150
            optical_elements={'slit': 'slit_1.6as'}, configuration=self.configuration_spectrum, mode='2m0_floyds_1'
151
        )
152
        self.instrument_config_arc = mixer.blend(
2✔
153
            InstrumentConfig, extra_params={'bin_x': 1, 'bin_y': 1}, exposure_time=30, exposure_count=2,
154
            optical_elements={'slit': 'slit_1.6as'}, configuration=self.configuration_arc, mode='2m0_floyds_1'
155
        )
156
        self.instrument_config_lampflat = mixer.blend(
2✔
157
            InstrumentConfig, extra_params={'bin_x': 1, 'bin_y': 1}, exposure_time=60, exposure_count=1,
158
            optical_elements={'slit': 'slit_1.6as'}, configuration=self.configuration_lampflat, mode='2m0_floyds_1'
159
        )
160
        self.instrument_config_repeat_expose = mixer.blend(
2✔
161
            InstrumentConfig, extra_params={'bin_x': 2, 'bin_y': 2}, exposure_time=30, exposure_count=1,
162
            optical_elements={'filter': 'b'}, mode='1m0_sbig_2', configuration=self.configuration_repeat_expose
163
        )
164

165
        self.instrument_change_overhead_1m = 0
2✔
166
        self.minimum_slew_time = 2
2✔
167

168
        self.sbig_fixed_overhead_per_exposure = 1
2✔
169
        self.sbig_filter_optical_element_change_overhead = 2
2✔
170
        self.sbig_front_padding = 90
2✔
171
        self.sbig_readout_time2 = 14.5
2✔
172

173
        self.floyds_fixed_overhead_per_exposure = 0.5
2✔
174
        self.floyds_filter_change_time = 0
2✔
175
        self.floyds_front_padding = 240
2✔
176
        self.floyds_readout_time1 = 25
2✔
177
        self.floyds_config_change_time = 30
2✔
178
        self.floyds_acquire_processing_time = 90
2✔
179
        self.floyds_acquire_exposure_time = 30
2✔
180

181
    def test_ccd_single_configuration_request_duration(self):
2✔
182
        self.configuration_expose.request = self.request
2✔
183
        self.configuration_expose.save()
2✔
184

185
        self.instrument_config_expose.configuration = self.configuration_expose
2✔
186
        self.instrument_config_expose.save()
2✔
187

188
        duration = self.request.duration
2✔
189
        exp_count = self.instrument_config_expose.exposure_count
2✔
190
        exp_time = self.instrument_config_expose.exposure_time
2✔
191

192
        self.assertEqual(duration, math.ceil(exp_count*(exp_time + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure) + self.sbig_front_padding + self.sbig_filter_optical_element_change_overhead + PER_CONFIGURATION_STARTUP_TIME + self.instrument_change_overhead_1m + self.minimum_slew_time))
2✔
193

194
    def test_ccd_single_configuration_duration(self):
2✔
195
        self.instrument_config_expose.configuration = self.configuration_expose
2✔
196
        self.instrument_config_expose.save()
2✔
197

198
        duration = self.configuration_expose.duration
2✔
199

200
        exp_time = self.instrument_config_expose.exposure_time
2✔
201
        exp_count = self.instrument_config_expose.exposure_count
2✔
202

203
        self.assertEqual(duration, (exp_count*(exp_time + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure) + PER_CONFIGURATION_STARTUP_TIME))
2✔
204

205
    def test_ccd_multiple_instrument_configuration_request_duration(self):
2✔
206
        self.configuration_expose.request = self.request
2✔
207
        self.configuration_expose.save()
2✔
208
        self.instrument_config_expose_1.configuration = self.configuration_expose
2✔
209
        self.instrument_config_expose_1.save()
2✔
210
        self.instrument_config_expose_2.configuration = self.configuration_expose
2✔
211
        self.instrument_config_expose_2.save()
2✔
212
        self.instrument_config_expose_3.configuration = self.configuration_expose
2✔
213
        self.instrument_config_expose_3.save()
2✔
214

215
        duration = self.request.duration
2✔
216

217
        exp_time1 = self.instrument_config_expose_1.exposure_time
2✔
218
        exp_count1 = self.instrument_config_expose_1.exposure_count
2✔
219
        exp_time2 = self.instrument_config_expose_2.exposure_time
2✔
220
        exp_count2 = self.instrument_config_expose_2.exposure_count
2✔
221
        exp_time3 = self.instrument_config_expose_3.exposure_time
2✔
222
        exp_count3 = self.instrument_config_expose_3.exposure_count
2✔
223

224
        exp_1_duration = exp_count1*(exp_time1 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
225
        exp_2_duration = exp_count2*(exp_time2 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
226
        exp_3_duration = exp_count3*(exp_time3 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
227

228
        num_filter_changes = 2
2✔
229

230
        self.assertEqual(duration, math.ceil(exp_1_duration + exp_2_duration + exp_3_duration + self.sbig_front_padding + num_filter_changes*self.sbig_filter_optical_element_change_overhead + (PER_CONFIGURATION_STARTUP_TIME + self.minimum_slew_time)))
2✔
231

232
    def test_single_repeat_configuration_duration(self):
2✔
233
        self.configuration_repeat_expose.request = self.request
2✔
234
        self.configuration_repeat_expose.save()
2✔
235

236
        configuration_duration = self.configuration_repeat_expose.repeat_duration
2✔
237
        self.assertEqual(configuration_duration, self.configuration_repeat_expose.duration)
2✔
238

239
        mixer.blend(
2✔
240
            InstrumentConfig, extra_params={'bin_x': 2, 'bin_y': 2}, exposure_time=30, exposure_count=1,
241
            optical_elements={'filter': 'g'}, mode='1m0_sbig_2', configuration=self.configuration_repeat_expose
242
        )
243
        mixer.blend(
2✔
244
            InstrumentConfig, extra_params={'bin_x': 2, 'bin_y': 2}, exposure_time=30, exposure_count=1,
245
            optical_elements={'filter': 'r'}, mode='1m0_sbig_2'
246
        )
247
        # configuration duration is unchanged after adding instrument configs
248
        self.assertEqual(configuration_duration, self.configuration_repeat_expose.duration)
2✔
249

250
    def test_repeat_configuration_multi_config_request_duration(self):
2✔
251
        self.configuration_repeat_expose.request = self.request
2✔
252
        self.configuration_repeat_expose.save()
2✔
253
        self.configuration_expose.request = self.request
2✔
254
        self.configuration_expose.save()
2✔
255
        self.configuration_expose_2.request = self.request
2✔
256
        self.configuration_expose_2.save()
2✔
257
        self.instrument_config_expose_1.configuration = self.configuration_expose
2✔
258
        self.instrument_config_expose_1.save()
2✔
259
        self.instrument_config_expose_2.configuration = self.configuration_expose_2
2✔
260
        self.instrument_config_expose_2.save()
2✔
261

262
        exp_time1 = self.instrument_config_expose_1.exposure_time
2✔
263
        exp_count1 = self.instrument_config_expose_1.exposure_count
2✔
264
        exp_time2 = self.instrument_config_expose_2.exposure_time
2✔
265
        exp_count2 = self.instrument_config_expose_2.exposure_count
2✔
266
        exp_1_duration = exp_count1 * (exp_time1 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
267
        exp_2_duration = exp_count2 * (exp_time2 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
268
        repeat_config_duration = self.configuration_repeat_expose.repeat_duration
2✔
269
        num_configurations = 3
2✔
270
        num_filter_changes = 2
2✔
271
        duration = self.request.duration
2✔
272

273
        self.assertEqual(duration, math.ceil(exp_1_duration + exp_2_duration + (repeat_config_duration - PER_CONFIGURATION_STARTUP_TIME) + self.sbig_front_padding + num_configurations*(
2✔
274
            PER_CONFIGURATION_STARTUP_TIME + self.minimum_slew_time) + num_filter_changes*self.sbig_filter_optical_element_change_overhead))
275

276
    def test_ccd_multiple_configuration_request_duration(self):
2✔
277
        self.instrument_config_expose_1.configuration = self.configuration_expose
2✔
278
        self.instrument_config_expose_1.save()
2✔
279
        self.instrument_config_expose_2.configuration = self.configuration_expose_2
2✔
280
        self.instrument_config_expose_2.save()
2✔
281
        self.instrument_config_expose_3.configuration = self.configuration_expose_3
2✔
282
        self.instrument_config_expose_3.save()
2✔
283
        self.configuration_expose.request = self.request
2✔
284
        self.configuration_expose.save()
2✔
285
        self.configuration_expose_2.request = self.request
2✔
286
        self.configuration_expose_2.save()
2✔
287
        self.configuration_expose_3.request = self.request
2✔
288
        self.configuration_expose_3.save()
2✔
289
        duration = self.request.duration
2✔
290

291
        exp_time1 = self.instrument_config_expose_1.exposure_time
2✔
292
        exp_count1 = self.instrument_config_expose_1.exposure_count
2✔
293
        exp_time2 = self.instrument_config_expose_2.exposure_time
2✔
294
        exp_count2 = self.instrument_config_expose_2.exposure_count
2✔
295
        exp_time3 = self.instrument_config_expose_3.exposure_time
2✔
296
        exp_count3 = self.instrument_config_expose_3.exposure_count
2✔
297

298
        exp_1_duration = exp_count1 * (exp_time1 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
299
        exp_2_duration = exp_count2 * (exp_time2 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
300
        exp_3_duration = exp_count3 * (exp_time3 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
301
        num_configurations = 3
2✔
302
        num_filter_changes = 2
2✔
303

304
        self.assertEqual(duration, math.ceil(exp_1_duration + exp_2_duration + exp_3_duration + self.sbig_front_padding + num_configurations*(PER_CONFIGURATION_STARTUP_TIME + self.minimum_slew_time) + num_filter_changes*self.sbig_filter_optical_element_change_overhead))
2✔
305

306
    def test_ccd_multiple_configuration_duration(self):
2✔
307
        self.instrument_config_expose_1.configuration = self.configuration_expose
2✔
308
        self.instrument_config_expose_1.save()
2✔
309
        self.instrument_config_expose_2.configuration = self.configuration_expose_2
2✔
310
        self.instrument_config_expose_2.save()
2✔
311
        self.instrument_config_expose_3.configuration = self.configuration_expose_3
2✔
312
        self.instrument_config_expose_3.save()
2✔
313
        duration = self.configuration_expose.duration
2✔
314
        duration += self.configuration_expose_2.duration
2✔
315
        duration += self.configuration_expose_3.duration
2✔
316

317
        exp_time1 = self.instrument_config_expose_1.exposure_time
2✔
318
        exp_count1 = self.instrument_config_expose_1.exposure_count
2✔
319
        exp_time2 = self.instrument_config_expose_2.exposure_time
2✔
320
        exp_count2 = self.instrument_config_expose_2.exposure_count
2✔
321
        exp_time3 = self.instrument_config_expose_3.exposure_time
2✔
322
        exp_count3 = self.instrument_config_expose_3.exposure_count
2✔
323

324
        exp_1_duration = exp_count1 * (exp_time1 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
325
        exp_2_duration = exp_count2 * (exp_time2 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
326
        exp_3_duration = exp_count3 * (exp_time3 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
327
        num_configurations = 3
2✔
328

329
        self.assertEqual(duration, (exp_1_duration + exp_2_duration + exp_3_duration + num_configurations*(PER_CONFIGURATION_STARTUP_TIME)))
2✔
330

331
    def test_floyds_single_configuration_request_duration_with_acquire_on(self):
2✔
332
        self.configuration_spectrum.request = self.request
2✔
333
        self.configuration_spectrum.acquisition_config.mode = 'WCS'
2✔
334
        self.configuration_spectrum.acquisition_config.save()
2✔
335
        self.configuration_spectrum.save()
2✔
336

337
        duration = self.request.duration
2✔
338

339
        exp_time = 1800
2✔
340
        exp_count = 1
2✔
341

342
        self.assertEqual(duration, math.ceil(exp_count*(exp_time + self.floyds_readout_time1 + self.floyds_fixed_overhead_per_exposure) + self.floyds_front_padding + self.floyds_acquire_processing_time + self.floyds_acquire_exposure_time + PER_CONFIGURATION_STARTUP_TIME + self.minimum_slew_time))
2✔
343

344
    def test_floyds_uses_supplied_acquisition_config_exposure_time(self):
2✔
345
        self.configuration_spectrum.request = self.request
2✔
346
        self.configuration_spectrum.acquisition_config.mode = 'WCS'
2✔
347
        self.configuration_spectrum.acquisition_config.save()
2✔
348
        self.configuration_spectrum.save()
2✔
349

350
        duration = self.request.duration
2✔
351

352
        self.configuration_spectrum.acquisition_config.exposure_time = 20.0  # Default for test floyds is 30sec
2✔
353
        self.configuration_spectrum.acquisition_config.save()
2✔
354
        self.configuration_spectrum.save()
2✔
355

356
        del self.request.duration
2✔
357
        new_duration = self.request.duration
2✔
358

359
        self.assertEqual(new_duration, duration - 10)
2✔
360

361
    def test_floyds_multiple_spectrum_configuration_request_duration_with_acquire_on(self):
2✔
362
        self.configuration_spectrum.request = self.request
2✔
363
        self.configuration_spectrum.acquisition_config.mode = 'WCS'
2✔
364
        self.configuration_spectrum.acquisition_config.save()
2✔
365
        self.configuration_spectrum.save()
2✔
366

367
        mixer.blend(
2✔
368
            InstrumentConfig, configuration=self.configuration_spectrum_2, extra_params={'bin_x': 1, 'bin_y': 1},
369
            exposure_time=1800, exposure_count=1, mode='2m0_floyds_1'
370
        )
371
        self.configuration_spectrum_2.request = self.request
2✔
372
        self.configuration_spectrum_2.acquisition_config.mode = 'WCS'
2✔
373
        self.configuration_spectrum_2.acquisition_config.save()
2✔
374
        self.configuration_spectrum_2.save()
2✔
375

376
        duration = self.request.duration
2✔
377

378
        exp_time = 1800
2✔
379
        exp_count = 1
2✔
380
        num_spectrum_configurations = 2
2✔
381

382
        self.assertEqual(duration, math.ceil(exp_count*num_spectrum_configurations*(exp_time + self.floyds_readout_time1 + self.floyds_fixed_overhead_per_exposure) + self.floyds_front_padding + num_spectrum_configurations*(self.floyds_acquire_processing_time + PER_CONFIGURATION_STARTUP_TIME + self.minimum_slew_time + self.floyds_acquire_exposure_time)))
2✔
383

384
    def test_floyds_single_configuration_duration(self):
2✔
385
        duration = self.configuration_spectrum.duration
2✔
386

387
        exp_time = self.instrument_config_spectrum.exposure_time
2✔
388
        exp_count = self.instrument_config_spectrum.exposure_count
2✔
389

390
        self.assertEqual(duration, (exp_count*(exp_time + self.floyds_readout_time1 + self.floyds_fixed_overhead_per_exposure) + PER_CONFIGURATION_STARTUP_TIME))
2✔
391

392
    def test_floyds_multiple_configuration_request_duration_with_acquire_on(self):
2✔
393
        self.configuration_lampflat.request = self.request
2✔
394
        self.configuration_lampflat.save()
2✔
395
        self.configuration_arc.request = self.request
2✔
396
        self.configuration_arc.save()
2✔
397
        self.configuration_spectrum.request = self.request
2✔
398
        self.configuration_spectrum.acquisition_config.mode = 'WCS'
2✔
399
        self.configuration_spectrum.acquisition_config.save()
2✔
400
        self.configuration_spectrum.save()
2✔
401

402
        duration = self.request.duration
2✔
403

404
        exp_time_s = self.instrument_config_spectrum.exposure_time
2✔
405
        exp_count_s = self.instrument_config_spectrum.exposure_count
2✔
406
        exp_time_a = self.instrument_config_arc.exposure_time
2✔
407
        exp_count_a = self.instrument_config_arc.exposure_count
2✔
408
        exp_time_l = self.instrument_config_lampflat.exposure_time
2✔
409
        exp_count_l = self.instrument_config_lampflat.exposure_count
2✔
410

411
        exp_s_duration = exp_count_s*(exp_time_s + self.floyds_readout_time1 + self.floyds_fixed_overhead_per_exposure)
2✔
412
        exp_a_duration = exp_count_a*(exp_time_a + self.floyds_readout_time1 + self.floyds_fixed_overhead_per_exposure)
2✔
413
        exp_l_duration = exp_count_l*(exp_time_l + self.floyds_readout_time1 + self.floyds_fixed_overhead_per_exposure)
2✔
414

415
        num_configurations = 3
2✔
416

417
        self.assertEqual(duration, math.ceil(exp_s_duration + exp_a_duration + exp_l_duration + self.floyds_front_padding + self.floyds_acquire_processing_time + self.floyds_acquire_exposure_time + (num_configurations-1)*self.floyds_config_change_time + num_configurations*(PER_CONFIGURATION_STARTUP_TIME + self.minimum_slew_time)))
2✔
418

419
    def test_floyds_multiple_configuration_duration(self):
2✔
420
        duration = self.configuration_lampflat.duration
2✔
421
        duration += self.configuration_arc.duration
2✔
422
        duration += self.configuration_spectrum.duration
2✔
423

424
        exp_time_s = self.instrument_config_spectrum.exposure_time
2✔
425
        exp_count_s = self.instrument_config_spectrum.exposure_count
2✔
426
        exp_time_a = self.instrument_config_arc.exposure_time
2✔
427
        exp_count_a = self.instrument_config_arc.exposure_count
2✔
428
        exp_time_l = self.instrument_config_lampflat.exposure_time
2✔
429
        exp_count_l = self.instrument_config_lampflat.exposure_count
2✔
430

431
        exp_s_duration = exp_count_s*(exp_time_s + self.floyds_readout_time1 + self.floyds_fixed_overhead_per_exposure)
2✔
432
        exp_a_duration = exp_count_a*(exp_time_a + self.floyds_readout_time1 + self.floyds_fixed_overhead_per_exposure)
2✔
433
        exp_l_duration = exp_count_l*(exp_time_l + self.floyds_readout_time1 + self.floyds_fixed_overhead_per_exposure)
2✔
434

435
        num_configurations = 3
2✔
436

437
        self.assertEqual(duration, (exp_s_duration + exp_a_duration + exp_l_duration + num_configurations*(PER_CONFIGURATION_STARTUP_TIME)))
2✔
438

439
    def test_configuration_repeats_single_configuration_request_duration(self):
2✔
440
        self.configuration_expose.request = self.request
2✔
441
        self.configuration_expose.save()
2✔
442
        self.request.configuration_repeats = 3
2✔
443
        self.request.save()
2✔
444

445
        self.instrument_config_expose.configuration = self.configuration_expose
2✔
446
        self.instrument_config_expose.save()
2✔
447

448
        duration = self.request.duration
2✔
449
        exp_count = self.instrument_config_expose.exposure_count
2✔
450
        exp_time = self.instrument_config_expose.exposure_time
2✔
451

452
        configuration_duration = exp_count*(exp_time + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure) + self.sbig_filter_optical_element_change_overhead + PER_CONFIGURATION_STARTUP_TIME + self.instrument_change_overhead_1m + self.minimum_slew_time
2✔
453
        self.assertEqual(duration, math.ceil( self.request.configuration_repeats * configuration_duration + self.sbig_front_padding))
2✔
454

455
    def test_configuration_repeats_multi_configuration_request_duration(self):
2✔
456
        self.configuration_expose.request = self.request
2✔
457
        self.configuration_expose.save()
2✔
458
        self.instrument_config_expose_1.configuration = self.configuration_expose
2✔
459
        self.instrument_config_expose_1.save()
2✔
460
        self.instrument_config_expose_2.configuration = self.configuration_expose
2✔
461
        self.instrument_config_expose_2.save()
2✔
462
        self.instrument_config_expose_3.configuration = self.configuration_expose
2✔
463
        self.instrument_config_expose_3.save()
2✔
464

465
        self.request.configuration_repeats = 3
2✔
466
        self.request.save()
2✔
467

468
        duration = self.request.duration
2✔
469

470
        exp_time1 = self.instrument_config_expose_1.exposure_time
2✔
471
        exp_count1 = self.instrument_config_expose_1.exposure_count
2✔
472
        exp_time2 = self.instrument_config_expose_2.exposure_time
2✔
473
        exp_count2 = self.instrument_config_expose_2.exposure_count
2✔
474
        exp_time3 = self.instrument_config_expose_3.exposure_time
2✔
475
        exp_count3 = self.instrument_config_expose_3.exposure_count
2✔
476

477
        exp_1_duration = exp_count1*(exp_time1 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
478
        exp_2_duration = exp_count2*(exp_time2 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
479
        exp_3_duration = exp_count3*(exp_time3 + self.sbig_readout_time2 + self.sbig_fixed_overhead_per_exposure)
2✔
480

481
        num_filter_changes = 2
2✔
482
        configuration_duration = exp_1_duration + exp_2_duration + exp_3_duration + num_filter_changes*self.sbig_filter_optical_element_change_overhead + (PER_CONFIGURATION_STARTUP_TIME + self.minimum_slew_time)
2✔
483

484
        self.assertEqual(duration, math.ceil(self.request.configuration_repeats * configuration_duration + self.sbig_front_padding))
2✔
485

486
    def test_configuration_repeats_repeat_configuration_request_duration(self):
2✔
487
        self.configuration_repeat_expose.request = self.request
2✔
488
        self.configuration_repeat_expose.save()
2✔
489
        self.request.configuration_repeats = 2
2✔
490
        self.request.save()
2✔
491

492
        configuration_duration = self.configuration_repeat_expose.repeat_duration
2✔
493
        self.assertEqual(configuration_duration, self.configuration_repeat_expose.duration)
2✔
494

495
        # Add the non-repeat parts back to the configuration duration
496
        configuration_duration += + self.sbig_filter_optical_element_change_overhead + self.instrument_change_overhead_1m + self.minimum_slew_time
2✔
497

498
        duration = self.request.duration
2✔
499
        self.assertEqual(duration, math.ceil( self.request.configuration_repeats * configuration_duration + self.sbig_front_padding))
2✔
500

501
    def test_get_duration_from_non_existent_camera(self):
2✔
502
        self.configuration_expose.instrument_type = 'FAKE-CAMERA'
2✔
503
        self.configuration_expose.save()
2✔
504
        self.instrument_config_expose.configuration = self.configuration_expose
2✔
505
        self.instrument_config_expose.save()
2✔
506

507
        with self.assertRaises(ConfigDBException) as context:
2✔
508
            _ = self.configuration_expose.duration
2✔
UNCOV
509
            self.assertTrue('not found in configdb' in context.exception)
×
510

511

512
class TestValidationHelper(TestCase):
2✔
513
    def setUp(self) -> None:
2✔
514
        self.mock_instrument_type = {'code': '1M0-SCICAM-SBIG',
2✔
515
                                     'validation_schema': {
516
                                        "instrument_configs": {
517
                                            "schema": {
518
                                                "schema": {
519
                                                    "extra_params": {
520
                                                    "schema": {
521
                                                        "defocus": {
522
                                                        "max": 5.0,
523
                                                        "min": -5.0,
524
                                                        "type": "float"
525
                                                        }
526
                                                    },
527
                                                    "type": "dict"
528
                                                    },
529
                                                    'exposure_time': {'type': 'integer', 'min': 0}
530
                                                },
531
                                                "type": "dict"
532
                                            },
533
                                            "type": "list"
534
                                        }
535
                                      }
536
                                    }
537

538
        self.mock_configuration_type_properties = {'SKY_FLAT': {
2✔
539
            'validation_schema': {
540
                "instrument_configs": {
541
                    "schema": {
542
                        "schema": {
543
                            'exposure_time': {'type': 'integer', 'default': 2}
544
                        },
545
                        "type": "dict"
546
                    },
547
                    "type": "list"
548
                }
549
            }
550
        }}
551
        self.generic_payload = copy.deepcopy(generic_payload)
2✔
552
        self.request_instrument_type = self.generic_payload['requests'][0]['configurations'][0]['instrument_type']
2✔
553
        self.configuration = self.generic_payload['requests'][0]['configurations'][0]
2✔
554
        self.instrument_config = self.configuration['instrument_configs'][0]
2✔
555
        self.muscat_extra_params = {'exposure_time_g': 60,
2✔
556
                                    'exposure_time_r': 90,
557
                                    'exposure_time_i': 60,
558
                                    'exposure_time_z': 120,
559
                                    'exposure_mode': 'SYNCHRONOUS'}
560

561
    @patch('observation_portal.requestgroups.serializers.configdb.get_instrument_type_by_code')
2✔
562
    def test_validate_instrument_config_and_extra_params_good_config(self, mock_instrument_type):
2✔
563
        configuration = self.configuration.copy()
2✔
564
        instrument_config = configuration['instrument_configs'][0]
2✔
565
        mock_instrument_type.return_value = self.mock_instrument_type
2✔
566
        instrument_config['extra_params'] = {'defocus': 2.0}
2✔
567

568
        validation_helper = InstrumentTypeValidationHelper(self.request_instrument_type)
2✔
569
        validated_config = validation_helper.validate(configuration)
2✔
570

571
        self.assertEqual(configuration, validated_config)
2✔
572

573
    @patch('observation_portal.requestgroups.serializers.configdb.get_instrument_type_by_code')
2✔
574
    def test_validate_instrument_config_and_extra_params_bad_config(self, mock_instrument_type):
2✔
575
        configuration = self.configuration.copy()
2✔
576
        instrument_config = configuration['instrument_configs'][0]
2✔
577
        mock_instrument_type.return_value = self.mock_instrument_type
2✔
578
        instrument_config['extra_params'] = {'defocus': 2.0}
2✔
579
        instrument_config['exposure_time'] = -20
2✔
580

581
        validation_helper = InstrumentTypeValidationHelper(self.request_instrument_type)
2✔
582
        with self.assertRaises(ValidationError) as e:
2✔
583
            validation_helper.validate(configuration)
2✔
584
        self.assertIn('exposure_time', str(e.exception))
2✔
585

586
    def test_validate_mode_config_filled_in_when_missing(self):
2✔
587
        guiding_config = {}
2✔
588
        modes = configdb.get_modes_by_type(self.request_instrument_type)
2✔
589
        validation_helper = ModeValidationHelper('guiding', self.request_instrument_type, modes['guiding'])
2✔
590

591
        validated_config = validation_helper.validate(guiding_config)
2✔
592
        self.assertEqual(validated_config['mode'], 'OFF')
2✔
593

594
    def test_validate_mode_config_good_config(self):
2✔
595
        instrument_config = self.instrument_config.copy()
2✔
596
        instrument_config['mode'] = "1m0_sbig_1"
2✔
597
        instrument_config['extra_params'] = {'bin_x': 1, 'bin_y': 1}
2✔
598
        modes = configdb.get_modes_by_type(self.request_instrument_type)
2✔
599

600
        validation_helper = ModeValidationHelper('readout', self.request_instrument_type, modes['readout'])
2✔
601
        validated_config = validation_helper.validate(instrument_config)
2✔
602

603
        self.assertEqual(validated_config, instrument_config)
2✔
604

605
    def test_validate_mode_config_bad_config(self):
2✔
606
        instrument_config = self.instrument_config.copy()
2✔
607
        instrument_config['mode'] = "1m0_sbig_2"
2✔
608
        instrument_config['extra_params'] = {'bin_x': 1, 'bin_y': 2}
2✔
609
        modes = configdb.get_modes_by_type(self.request_instrument_type)
2✔
610

611
        validation_helper = ModeValidationHelper('readout', self.request_instrument_type, modes['readout'])
2✔
612
        with self.assertRaises(ValidationError) as e:
2✔
613
            validation_helper.validate(instrument_config)
2✔
614
        self.assertIn('bin_x', str(e.exception))
2✔
615

616
    def test_validate_extra_param_mode_good_config(self):
2✔
617
        instrument_config = self.instrument_config.copy()
2✔
618
        instrument_type = "2M0-SCICAM-MUSCAT"
2✔
619
        instrument_config['instrument_type'] = instrument_type
2✔
620
        instrument_config['extra_params'] = self.muscat_extra_params
2✔
621
        modes = configdb.get_modes_by_type(instrument_type)
2✔
622

623
        validation_helper = ModeValidationHelper('exposure', instrument_type, modes['exposure'],
2✔
624
                                                 is_extra_param_mode=True)
625
        validated_instrument_config = validation_helper.validate(instrument_config)
2✔
626

627
        self.assertEqual(instrument_config, validated_instrument_config)
2✔
628

629
    def test_validate_extra_param_mode_bad_config(self):
2✔
630
        instrument_config = self.instrument_config.copy()
2✔
631
        instrument_type = "2M0-SCICAM-MUSCAT"
2✔
632
        instrument_config['instrument_type'] = instrument_type
2✔
633
        instrument_config['extra_params'] = self.muscat_extra_params.copy()
2✔
634
        del instrument_config['extra_params']['exposure_mode']
2✔
635
        modes = configdb.get_modes_by_type(instrument_type)
2✔
636

637
        validation_helper = ModeValidationHelper('exposure', instrument_type, modes['exposure'],
2✔
638
                                                 is_extra_param_mode=True)
639

640
        with self.assertRaises(ValidationError) as e:
2✔
641
            validation_helper.validate(instrument_config)
2✔
642
        self.assertIn('exposure_mode', str(e.exception))
2✔
643

644
    @patch('observation_portal.requestgroups.serializers.configdb.get_configuration_types')
2✔
645
    def test_validate_exposure_time_no_exposure_time_set(self, mock_configuration_type_properties):
2✔
646
        configuration = self.configuration.copy()
2✔
647
        instrument_config = configuration['instrument_configs'][0]
2✔
648
        mock_configuration_type_properties.return_value = self.mock_configuration_type_properties
2✔
649
        del instrument_config['exposure_time']
2✔
650

651
        validation_helper = ConfigurationTypeValidationHelper('FAKE-CAMERA', 'SKY_FLAT')
2✔
652
        validated_configuration = validation_helper.validate(configuration)
2✔
653

654
        self.assertEqual(validated_configuration['instrument_configs'][0]['exposure_time'], 2.0)
2✔
655

656

657
class TestRequestSemester(SetTimeMixin, TestCase):
2✔
658
    def setUp(self) -> None:
2✔
659
        super().setUp()
2✔
660
        self.semester_1 = mixer.blend(Semester, start=datetime(2016, 1, 1, 0, 0, 0, tzinfo=timezone.utc), end=datetime(2016, 1, 31, 23, 59, 59, tzinfo=timezone.utc))
2✔
661
        self.semester_2 = mixer.blend(Semester, start=datetime(2016, 2, 1, 0, 0, 0, tzinfo=timezone.utc), end=datetime(2016, 2, 29, 23, 59, 59, tzinfo=timezone.utc))
2✔
662
        self.request = mixer.blend(Request)
2✔
663

664
    def test_get_semester(self):
2✔
665
        mixer.blend(Window, request=self.request, start=self.semester_1.start + timedelta(days=1), end=self.semester_1.end - timedelta(days=1))
2✔
666
        mixer.blend(Window, request=self.request, start=self.semester_1.start + timedelta(days=2), end=self.semester_1.end - timedelta(days=2))
2✔
667
        semester = self.request.semester
2✔
668
        self.assertEqual(semester.id, self.semester_1.id)
2✔
669

670
    def test_get_semester_for_request_without_observations_when_windows_span_multiple_semesters(self):
2✔
671
        mixer.blend(Window, request=self.request, start=self.semester_1.start + timedelta(days=1), end=self.semester_1.end - timedelta(days=1))
2✔
672
        mixer.blend(Window, request=self.request, start=self.semester_2.start - timedelta(days=1), end=self.semester_2.start + timedelta(days=1))
2✔
673
        semester = self.request.semester
2✔
674
        # Should fall into the semester with the earliest window start time
675
        self.assertEqual(semester.id, self.semester_1.id)
2✔
676

677
    def test_get_semester_for_request_with_observations_when_windows_span_multiple_semesters(self):
2✔
678
        mixer.blend(Window, request=self.request, start=self.semester_1.start + timedelta(days=1), end=self.semester_1.end - timedelta(days=1))
2✔
679
        mixer.blend(Window, request=self.request, start=self.semester_2.start - timedelta(days=1), end=self.semester_2.start + timedelta(days=1))
2✔
680
        mixer.blend(Observation, request=self.request, start=self.semester_2.start + timedelta(hours=1), end=self.semester_2.start + timedelta(hours=2))
2✔
681
        semester = self.request.semester
2✔
682
        # Should fall into the semester that contains any observation
683
        self.assertEqual(semester.id, self.semester_2.id)
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc