Skip to main content

lava_api_mock/
state.rs

1use super::{
2    Alias, Architecture, BitWidth, Core, Device, DeviceType, Group, Job, ProcessorFamily, Tag,
3    TestCase, TestSet, TestSuite, User, Worker,
4};
5
6use boulder::{
7    Buildable, Builder, GeneratableWithPersianRug, GeneratorWithPersianRug,
8    GeneratorWithPersianRugIterator, GeneratorWithPersianRugMutIterator, RepeatFromPersianRug,
9    SubsetsFromPersianRug, TryRepeatFromPersianRug,
10};
11use clone_replace::{CloneReplace, MutateGuard};
12use django_query::mock::clone_replace::persian_rug::CloneReplacePersianRugTableSource;
13use django_query::mock::{EndpointWithContext, NestedEndpointParams, NestedEndpointWithContext};
14use persian_rug::{Context, Mutator, Proxy};
15use std::sync::Arc;
16
17/// The data backing a mock Lava instance
18///
19/// This is a [`persian_rug::Context`] containing all of the different
20/// data types that make up the database of a Lava instance.
21#[derive(Clone, Debug, Default)]
22#[persian_rug::persian_rug]
23pub struct State {
24    #[table]
25    aliases: Alias<State>,
26    #[table]
27    architectures: Architecture<State>,
28    #[table]
29    bit_widths: BitWidth<State>,
30    #[table]
31    cores: Core<State>,
32    #[table]
33    devices: Device<State>,
34    #[table]
35    device_types: DeviceType<State>,
36    #[table]
37    groups: Group<State>,
38    #[table]
39    jobs: Job<State>,
40    #[table]
41    processor_family: ProcessorFamily<State>,
42    #[table]
43    tags: Tag<State>,
44    #[table]
45    test_cases: TestCase<State>,
46    #[table]
47    test_sets: TestSet<State>,
48    #[table]
49    test_suites: TestSuite<State>,
50    #[table]
51    users: User<State>,
52    #[table]
53    workers: Worker<State>,
54}
55
56/// A thin wrapper around [`State`] for shared access.
57///
58/// Although a [`State`] can hold all the necessary data, it doesn't
59/// define a strategy for sharing that data so it can be
60/// updated. Owing to limitations in the underlying crates this crate
61/// is based on, there's only really one sensible way to do this at
62/// present, and that's to use a [`CloneReplace`] to hold the data.
63///
64/// This is just a lightweight wrapper with some convenient methods to
65/// allow you to create [`wiremock`] endpoints.  Those are in turn
66/// based on [`EndpointWithContext`] from [`django_query`]
67/// (specifically this is the `WithContext` variant, because the
68/// connections between the different data types are handled using
69/// [`persian-rug`](persian_rug), and in fact a [`State`] is just a
70/// [`persian_rug::Context`].
71pub struct SharedState(CloneReplace<State>);
72
73impl SharedState {
74    /// Create and wrap a new empty [`State`].
75    ///
76    /// Example:
77    /// ```rust
78    /// use lava_api_mock::SharedState;
79    ///
80    /// let p = SharedState::new();
81    /// ```
82    pub fn new() -> Self {
83        Self(CloneReplace::new(State::new()))
84    }
85
86    /// Create, populate and wrap a [`State`].
87    ///
88    /// `pop` is a [`PopulationParams`] instance giving a count for
89    /// each type of object.
90    ///
91    /// Example:
92    /// ```rust
93    /// use lava_api_mock::SharedState;
94    ///
95    /// let p = SharedState::new_populated(Default::default());
96    /// ```
97    pub fn new_populated(pop: PopulationParams) -> Self {
98        Self(CloneReplace::new(State::new_populated(pop)))
99    }
100
101    /// Create a new [`EndpointWithContext`] for type `T` within the
102    /// enclosed [`State`].
103    ///
104    /// The return value is an implementor of [`wiremock::Respond`] and can
105    /// be mounted directly onto a wiremock server instance.
106    ///
107    /// Example:
108    /// ```rust
109    /// use lava_api_mock::{Job, State, SharedState};
110    ///
111    /// # tokio_test::block_on( async {
112    /// let p = SharedState::new();
113    ///
114    /// let server = wiremock::MockServer::start().await;
115    ///
116    /// wiremock::Mock::given(wiremock::matchers::method("GET"))
117    ///     .and(wiremock::matchers::path("/api/v0.2/jobs/"))
118    ///     .respond_with(p.endpoint::<Job<State>>(Some(&server.uri()), None))
119    ///     .mount(&server)
120    ///     .await;
121    /// # });
122    /// ```
123    #[allow(clippy::type_complexity)]
124    pub fn endpoint<T>(
125        &self,
126        uri: Option<&str>,
127        default_limit: Option<usize>,
128    ) -> EndpointWithContext<
129        CloneReplacePersianRugTableSource<
130            impl Fn(&Arc<State>) -> persian_rug::TableIterator<'_, T> + Clone + use<T>,
131            State,
132        >,
133    >
134    where
135        T: persian_rug::Contextual<Context = State> + 'static,
136        State: persian_rug::Owner<T>,
137    {
138        let mut ep = EndpointWithContext::new(
139            CloneReplacePersianRugTableSource::new(
140                self.0.clone(),
141                |s: &Arc<State>| -> persian_rug::TableIterator<'_, T> { s.get_iter() },
142            ),
143            uri,
144        );
145        if let Some(default_limit) = default_limit {
146            ep.default_limit(default_limit);
147        }
148        ep
149    }
150
151    /// Create a new [`NestedEndpointWithContext`] for type `T` within the
152    /// enclosed [`State`].
153    ///
154    /// Nested endpoints objects data that can only be queried by
155    /// providing some related object, like finding [`TestCase`]
156    /// instances that match a given [`Job`] for example: here `tests`
157    /// is nested under `jobs`. See the documentation for
158    /// [`NestedEndpointWithContext`] for more details.
159    ///
160    /// The return value is an implementor of [`wiremock::Respond`] and can
161    /// be mounted directly onto a wiremock server instance.
162    ///
163    /// Example:
164    /// ```rust
165    /// use django_query::mock::{nested_endpoint_matches, NestedEndpointParams};
166    /// use lava_api_mock::{Job, State, SharedState, TestCase};
167    ///
168    /// let p = SharedState::new();
169    ///
170    /// # tokio_test::block_on( async {
171    /// let server = wiremock::MockServer::start().await;
172    ///
173    /// wiremock::Mock::given(wiremock::matchers::method("GET"))
174    ///     .and(nested_endpoint_matches("/api/v0.2", "jobs", "tests"))
175    ///     .respond_with(p.nested_endpoint::<TestCase<State>>(
176    ///         NestedEndpointParams {
177    ///             root: "/api/v0.2",
178    ///             parent: "jobs",
179    ///             child: "tests",
180    ///             parent_query: "suite__job__id",
181    ///             base_uri: Some(&server.uri()),
182    ///         },
183    ///         Some(10),
184    ///     ))
185    ///     .mount(&server)
186    ///     .await;
187    /// # });
188    /// ```
189    #[allow(clippy::type_complexity)]
190    pub fn nested_endpoint<T>(
191        &self,
192        params: NestedEndpointParams<'_>,
193        default_limit: Option<usize>,
194    ) -> NestedEndpointWithContext<
195        CloneReplacePersianRugTableSource<
196            impl Fn(&Arc<State>) -> persian_rug::TableIterator<'_, T> + Clone + use<T>,
197            State,
198        >,
199    >
200    where
201        T: persian_rug::Contextual<Context = State> + 'static,
202        State: persian_rug::Owner<T>,
203    {
204        let mut ep = NestedEndpointWithContext::new(
205            CloneReplacePersianRugTableSource::new(
206                self.0.clone(),
207                |s: &Arc<State>| -> persian_rug::TableIterator<'_, T> { s.get_iter() },
208            ),
209            params,
210        );
211        if let Some(default_limit) = default_limit {
212            ep.default_limit(default_limit);
213        }
214        ep
215    }
216
217    /// Obtain a [`persian_rug::Accessor`] for the enclosed [`State`]
218    ///
219    /// This permits reading the data contained in the [`State`].
220    ///
221    /// Example:
222    /// ```rust
223    /// use lava_api_mock::{Job, SharedState};
224    /// use persian_rug::Accessor;
225    ///
226    /// let p = SharedState::new_populated(Default::default());
227    ///
228    /// for job in p.access().get_proxy_iter::<Job<_>>() {
229    ///     println!("Got job {:?}", p.access().get(&job));
230    /// }
231    /// ```
232    pub fn access(&self) -> Arc<State> {
233        self.0.access()
234    }
235
236    /// Obtain a [`persian_rug::Mutator`] for the enclosed [`State`]
237    ///
238    /// This permits modifying the data contained in the [`State`].
239    ///
240    /// Example:
241    /// ```rust
242    /// use boulder::{BuildableWithPersianRug, BuilderWithPersianRug};
243    /// use lava_api_mock::{Job, SharedState, State};
244    /// use persian_rug::Proxy;
245    ///
246    /// let mut p = SharedState::new_populated(Default::default());
247    ///
248    /// let _ = Proxy::<Job<State>>::builder().build(p.mutate());
249    /// ```
250    pub fn mutate(&mut self) -> MutateGuard<State> {
251        self.0.mutate()
252    }
253}
254
255impl Clone for SharedState {
256    fn clone(&self) -> Self {
257        SharedState(self.0.clone())
258    }
259}
260
261impl Default for SharedState {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267/// Initial population sizes for the data in a [`State`]
268///
269/// This specifies the number of objects of each type to
270/// generate when initializing a [`State`] instance using
271/// [`new_populated`](State::new_populated). It is
272/// [`Buildable`] so you can customise just some fields
273/// from default if you.
274///
275/// The default values are:
276/// - 10 [`Alias`] instances
277/// - 5 [`Architecture`] instances
278/// - 2 [`BitWidth`] instances
279/// - 3 [`Core`] instances
280/// - 50 [`Device`] instances
281/// - 10 [`DeviceType`] instances
282/// - 3 [`Group`] instances
283/// - 200 [`Job`] instances
284/// - 3 [`ProcessorFamily`] instances
285/// - 5 [`Tag`] instances
286/// - 5 [`User`] instances
287/// - 10 [`Worker`] instances
288///
289/// It also asks for:
290/// - 5 [`TestCase`] instances
291/// - 2 [`TestSet`] instances
292/// - 3 [`TestSuite`] instances
293///   to be created for each job that is created.
294#[derive(Buildable, Clone, Debug, Eq, PartialEq)]
295pub struct PopulationParams {
296    #[boulder(default = 10usize)]
297    pub aliases: usize,
298    #[boulder(default = 5usize)]
299    pub architectures: usize,
300    #[boulder(default = 2usize)]
301    pub bit_widths: usize,
302    #[boulder(default = 3usize)]
303    pub cores: usize,
304    #[boulder(default = 50usize)]
305    pub devices: usize,
306    #[boulder(default = 10usize)]
307    pub device_types: usize,
308    #[boulder(default = 3usize)]
309    pub groups: usize,
310    #[boulder(default = 200usize)]
311    pub jobs: usize,
312    #[boulder(default = 3usize)]
313    pub processor_families: usize,
314    #[boulder(default = 5usize)]
315    pub tags: usize,
316    #[boulder(default = 5usize)]
317    pub test_cases: usize,
318    #[boulder(default = 2usize)]
319    pub test_sets: usize,
320    #[boulder(default = 3usize)]
321    pub test_suites: usize,
322    #[boulder(default = 5usize)]
323    pub users: usize,
324    #[boulder(default = 10usize)]
325    pub workers: usize,
326}
327
328impl PopulationParams {
329    /// Create a new default [`PopulationParams`]
330    ///
331    /// This is equivalent to using the [`Builder`] without
332    /// customising it.
333    ///
334    /// ```rust
335    /// use boulder::{Buildable, Builder};
336    /// use lava_api_mock::PopulationParams;
337    ///
338    /// assert_eq!(PopulationParams::new(), PopulationParams::builder().build());
339    /// ```
340    pub fn new() -> Self {
341        Default::default()
342    }
343}
344
345impl Default for PopulationParams {
346    fn default() -> Self {
347        Self::builder().build()
348    }
349}
350
351struct JobGenerator {
352    job: Option<Proxy<Job<State>>>,
353}
354
355impl JobGenerator {
356    pub fn new(job: Option<Proxy<Job<State>>>) -> Self {
357        Self { job }
358    }
359}
360
361impl GeneratorWithPersianRug<State> for JobGenerator {
362    type Output = Proxy<Job<State>>;
363
364    fn generate<'b, B>(&mut self, context: B) -> (Self::Output, B)
365    where
366        B: 'b + Mutator<Context = State>,
367    {
368        (self.job.unwrap(), context)
369    }
370}
371
372struct SuiteGenerator {
373    suite: usize,
374    suites: Vec<Proxy<TestSuite<State>>>,
375}
376
377impl SuiteGenerator {
378    pub fn new(suites: Vec<Proxy<TestSuite<State>>>) -> Self {
379        SuiteGenerator { suite: 0, suites }
380    }
381}
382
383impl GeneratorWithPersianRug<State> for SuiteGenerator {
384    type Output = Proxy<TestSuite<State>>;
385
386    fn generate<'b, B>(&mut self, context: B) -> (Self::Output, B)
387    where
388        B: 'b + Mutator<Context = State>,
389    {
390        let suite = self.suites[self.suite];
391        self.suite = (self.suite + 1) % self.suites.len();
392
393        (suite, context)
394    }
395}
396
397struct SetGenerator {
398    suite: usize,
399    set: usize,
400    suites: Vec<Proxy<TestSuite<State>>>,
401    sets: Vec<Proxy<TestSet<State>>>,
402}
403
404impl SetGenerator {
405    fn new(suites: Vec<Proxy<TestSuite<State>>>, sets: Vec<Proxy<TestSet<State>>>) -> Self {
406        SetGenerator {
407            suite: 0,
408            set: 0,
409            suites,
410            sets,
411        }
412    }
413}
414
415impl GeneratorWithPersianRug<State> for SetGenerator {
416    type Output = Option<Proxy<TestSet<State>>>;
417
418    fn generate<'b, B>(&mut self, context: B) -> (Self::Output, B)
419    where
420        B: 'b + Mutator<Context = State>,
421    {
422        if self.suites.is_empty() || self.sets.is_empty() {
423            return (None, context);
424        }
425
426        let suite = self.suites[self.suite];
427        self.suite = (self.suite + 1) % self.suites.len();
428
429        let mut attempts = 0;
430        let set = loop {
431            let set = self.sets[self.set];
432            self.set = (self.set + 1) % self.sets.len();
433            attempts += 1;
434            if context.get(&set).suite == suite {
435                break Some(set);
436            }
437            if attempts == self.sets.len() {
438                break None;
439            }
440        };
441
442        (set, context)
443    }
444}
445
446impl State {
447    /// Create a new empty [`State`]
448    pub fn new() -> Self {
449        Default::default()
450    }
451
452    /// A [`DeviceType`] [`GeneratorWithPersianRug`] that uses
453    /// dependencies already in the [`State`].
454    ///
455    /// This generator is equivalent to the default, except that it
456    /// draws [`Alias`], [`Architecture`], [`BitWidth`], [`Core`] and
457    /// [`ProcessorFamily`] instances from those already in the
458    /// containing [`State`] at the point of generation.
459    pub fn make_device_type_generator()
460    -> impl GeneratorWithPersianRug<State, Output = Proxy<DeviceType<State>>> {
461        Proxy::<DeviceType<State>>::generator()
462            .aliases(SubsetsFromPersianRug::new())
463            .architecture(TryRepeatFromPersianRug::new())
464            .bits(TryRepeatFromPersianRug::new())
465            .cores(SubsetsFromPersianRug::new())
466            .processor(TryRepeatFromPersianRug::new())
467    }
468
469    /// A [`User`] [`GeneratorWithPersianRug`] that uses
470    /// dependencies already in the [`State`].
471    ///
472    /// This generator is equivalent to the default, except that it
473    /// draws [`Group`] instances from those already in the containing
474    /// [`State`] at the point of generation.
475    pub fn make_user_generator() -> impl GeneratorWithPersianRug<State, Output = Proxy<User<State>>>
476    {
477        Proxy::<User<State>>::generator().group(TryRepeatFromPersianRug::new())
478    }
479
480    /// A [`Device`] [`GeneratorWithPersianRug`] that uses
481    /// dependencies already in the [`State`].
482    ///
483    /// This generator is equivalent to the default, except that it
484    /// draws [`DeviceType`], [`User`], [`Group`],
485    /// [`Tag`] and [`Worker`] instances from those already in
486    /// the containing [`State`] at the point of generation.
487    pub fn make_device_generator()
488    -> impl GeneratorWithPersianRug<State, Output = Proxy<Device<State>>> {
489        Proxy::<Device<State>>::generator()
490            .device_type(RepeatFromPersianRug::new())
491            .physical_owner(TryRepeatFromPersianRug::new())
492            .physical_group(TryRepeatFromPersianRug::new())
493            .tags(SubsetsFromPersianRug::new())
494            .worker_host(RepeatFromPersianRug::new())
495    }
496
497    /// A [`Job`] [`GeneratorWithPersianRug`] that uses
498    /// dependencies already in the [`State`].
499    ///
500    /// This generator is equivalent to the default, except that it
501    /// draws [`User`], [`Group`], [`DeviceType`], [`Tag`] and
502    /// [`Device`] instances from those already in the
503    /// containing [`State`] at the point of generation.
504    pub fn make_job_generator() -> impl GeneratorWithPersianRug<State, Output = Proxy<Job<State>>> {
505        Proxy::<Job<State>>::generator()
506            .submitter(RepeatFromPersianRug::new())
507            .viewing_groups(SubsetsFromPersianRug::new())
508            .requested_device_type(TryRepeatFromPersianRug::new())
509            .tags(SubsetsFromPersianRug::new())
510            .actual_device(TryRepeatFromPersianRug::new())
511    }
512
513    /// Create a new [`State`] with some initial data.
514    ///
515    /// Here, `pop` is a [`PopulationParams`] which gives the initial
516    /// number of each type of object. The object generators are
517    /// customised to draw their references from the other objects in
518    /// the state.
519    ///
520    /// You can obtain new instances of themodified generators from
521    /// [`make_device_generator`](State::make_device_generator),
522    /// [`make_device_type_generator`](State::make_device_type_generator),
523    /// [`make_job_generator`](State::make_job_generator) and
524    /// [`make_user_generator`](State::make_user_generator) if you
525    /// need to create more objects in a similar fashion.
526    ///
527    /// Note that because tests are per-job objects, the counts in
528    /// [`PopulationParams`] for [`TestCase`], [`TestSet`] and
529    /// [`TestSuite`] are used to make custom objects for each job.
530    /// The tests are not provided automatically when jobs are generated
531    /// by the underlying [`GeneratorWithPersianRug`] provided by
532    /// [`make_job_generator`](State::make_job_generator).
533    pub fn new_populated(pop: PopulationParams) -> Self {
534        let mut s: State = Default::default();
535
536        let aliases = Proxy::<Alias<State>>::generator();
537        let _ = GeneratorWithPersianRugIterator::new(aliases, &mut s)
538            .take(pop.aliases)
539            .collect::<Vec<_>>();
540
541        let architectures = Proxy::<Architecture<State>>::generator();
542        let _ = GeneratorWithPersianRugIterator::new(architectures, &mut s)
543            .take(pop.architectures)
544            .collect::<Vec<_>>();
545
546        let bit_widths = Proxy::<BitWidth<State>>::generator();
547        let _ = GeneratorWithPersianRugIterator::new(bit_widths, &mut s)
548            .take(pop.bit_widths)
549            .collect::<Vec<_>>();
550
551        let cores = Proxy::<Core<State>>::generator();
552        let _ = GeneratorWithPersianRugIterator::new(cores, &mut s)
553            .take(pop.cores)
554            .collect::<Vec<_>>();
555
556        let processor_families = Proxy::<ProcessorFamily<State>>::generator();
557        let _ = GeneratorWithPersianRugIterator::new(processor_families, &mut s)
558            .take(pop.processor_families)
559            .collect::<Vec<_>>();
560
561        let device_types = Self::make_device_type_generator();
562        let _ = GeneratorWithPersianRugIterator::new(device_types, &mut s)
563            .take(pop.device_types)
564            .collect::<Vec<_>>();
565
566        let groups = Proxy::<Group<State>>::generator();
567        let _ = GeneratorWithPersianRugIterator::new(groups, &mut s)
568            .take(pop.groups)
569            .collect::<Vec<_>>();
570
571        let users = Self::make_user_generator();
572        let _ = GeneratorWithPersianRugIterator::new(users, &mut s)
573            .take(pop.users)
574            .collect::<Vec<_>>();
575
576        let workers = Proxy::<Worker<State>>::generator();
577        let _ = GeneratorWithPersianRugIterator::new(workers, &mut s)
578            .take(pop.workers)
579            .collect::<Vec<_>>();
580
581        let tags = Proxy::<Tag<State>>::generator();
582        let _ = GeneratorWithPersianRugIterator::new(tags, &mut s)
583            .take(pop.tags)
584            .collect::<Vec<_>>();
585
586        let devices = Self::make_device_generator();
587        let _ = GeneratorWithPersianRugIterator::new(devices, &mut s)
588            .take(pop.devices)
589            .collect::<Vec<_>>();
590
591        let jobs = Self::make_job_generator();
592        let jobs = GeneratorWithPersianRugIterator::new(jobs, &mut s)
593            .take(pop.jobs)
594            .collect::<Vec<_>>();
595
596        let mut suites = Proxy::<TestSuite<State>>::generator().job(JobGenerator::new(None));
597        let mut sets = Proxy::<TestSet<State>>::generator().suite(SuiteGenerator::new(Vec::new()));
598        let mut cases = Proxy::<TestCase<State>>::generator()
599            .suite(SuiteGenerator::new(Vec::new()))
600            .test_set(SetGenerator::new(Vec::new(), Vec::new()));
601
602        for job in jobs {
603            suites = suites.job(JobGenerator::new(Some(job)));
604            let suites = GeneratorWithPersianRugMutIterator::new(&mut suites, &mut s)
605                .take(pop.test_suites)
606                .collect::<Vec<_>>();
607
608            sets = sets.suite(SuiteGenerator::new(suites.clone()));
609            let sets = GeneratorWithPersianRugMutIterator::new(&mut sets, &mut s)
610                .take(pop.test_sets)
611                .collect::<Vec<_>>();
612
613            cases = cases
614                .suite(SuiteGenerator::new(suites.clone()))
615                .test_set(SetGenerator::new(suites.clone(), sets.clone()));
616            let _ = GeneratorWithPersianRugMutIterator::new(&mut cases, &mut s)
617                .take(pop.test_cases)
618                .collect::<Vec<_>>();
619        }
620
621        s
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use crate::{JobState, SharedState};
629
630    use anyhow::Result;
631    use boulder::{BuildableWithPersianRug, BuilderWithPersianRug};
632    use persian_rug::Proxy;
633    use serde_json::{Value, json};
634
635    async fn make_request<T, U>(server_uri: T, endpoint: U) -> Result<Value>
636    where
637        T: AsRef<str>,
638        U: AsRef<str>,
639    {
640        let url = format!("{}/api/v0.2/{}", server_uri.as_ref(), endpoint.as_ref());
641        Ok(reqwest::get(&url).await?.json().await?)
642    }
643
644    #[tokio::test]
645    async fn test_state() {
646        let mut p = SharedState::new();
647        {
648            let m = p.mutate();
649            let (_, m) = Proxy::<Job<State>>::builder().id(100).build(m);
650            let (_, _m) = Proxy::<Job<State>>::builder().id(101).build(m);
651        }
652
653        let server = wiremock::MockServer::start().await;
654
655        wiremock::Mock::given(wiremock::matchers::method("GET"))
656            .and(wiremock::matchers::path("/api/v0.2/jobs/"))
657            .respond_with(p.endpoint::<Job<State>>(Some(&server.uri()), None))
658            .mount(&server)
659            .await;
660
661        let jobs = make_request(server.uri(), "jobs/")
662            .await
663            .expect("failed to query jobs");
664
665        assert_eq!(jobs["results"][0]["id"], json!(100));
666        assert_eq!(jobs["results"][1]["id"], json!(101));
667        assert_eq!(jobs["results"].as_array().unwrap().len(), 2);
668
669        {
670            let m = p.mutate();
671            let (_, _m) = Proxy::<Job<State>>::builder()
672                .id(102)
673                .state(JobState::Submitted)
674                .build(m);
675        }
676
677        let jobs = make_request(server.uri(), "jobs/")
678            .await
679            .expect("failed to query jobs");
680
681        assert_eq!(jobs["results"][0]["id"], json!(100));
682        assert_eq!(jobs["results"][1]["id"], json!(101));
683        assert_eq!(jobs["results"][2]["id"], json!(102));
684        assert_eq!(jobs["results"].as_array().unwrap().len(), 3);
685
686        {
687            let mut m = p.mutate();
688            for j in m.get_iter_mut::<Job<State>>() {
689                if j.id == 102 {
690                    j.state = JobState::Finished
691                }
692            }
693        }
694
695        let jobs = make_request(server.uri(), "jobs/")
696            .await
697            .expect("failed to query jobs");
698
699        assert_eq!(jobs["results"][0]["id"], json!(100));
700        assert_eq!(jobs["results"][1]["id"], json!(101));
701        assert_eq!(jobs["results"][2]["id"], json!(102));
702        assert_eq!(jobs["results"][2]["state"], json!("Finished"));
703        assert_eq!(jobs["results"].as_array().unwrap().len(), 3);
704    }
705}