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