lava_api_mock/
devices.rs

1use boulder::{BuildableWithPersianRug, GeneratableWithPersianRug};
2use boulder::{Inc, Pattern, Some as GSome};
3use django_query::{
4    filtering::FilterableWithPersianRug, row::IntoRowWithPersianRug,
5    sorting::SortableWithPersianRug,
6};
7use persian_rug::{contextual, Context, Proxy};
8use strum::{Display, EnumString};
9
10use crate::{
11    Alias, Architecture, BitWidth, Core, DeviceType, Group, Job, ProcessorFamily, Tag, User, Worker,
12};
13
14/// A device from the LAVA API.
15#[derive(
16    Clone,
17    Debug,
18    FilterableWithPersianRug,
19    SortableWithPersianRug,
20    IntoRowWithPersianRug,
21    BuildableWithPersianRug,
22    GeneratableWithPersianRug,
23)]
24#[boulder(
25    persian_rug(
26        context=C,
27        access(
28            Device<C>,
29            DeviceType<C>,
30            Alias<C>,
31            Architecture<C>,
32            BitWidth<C>,
33            Core<C>,
34            ProcessorFamily<C>,
35            User<C>,
36            Group<C>,
37            Worker<C>,
38            Job<C>,
39            Tag<C>
40        )
41    )
42)]
43#[django(
44    persian_rug(
45        context=C,
46        access(
47            Device<C>,
48            DeviceType<C>,
49            Alias<C>,
50            Architecture<C>,
51            BitWidth<C>,
52            Core<C>,
53            ProcessorFamily<C>,
54            User<C>,
55            Group<C>,
56            Worker<C>,
57            Job<C>,
58            Tag<C>
59        )
60    )
61)]
62#[contextual(C)]
63pub struct Device<C: Context + 'static> {
64    #[boulder(default="test-device",
65              generator=Pattern!("test-device-{}", Inc(0)))]
66    #[django(sort, op(in, contains, icontains, startswith, endswith))]
67    pub hostname: String,
68    #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
69    #[django(sort("name"), traverse, foreign_key = "name")]
70    pub device_type: Proxy<DeviceType<C>>,
71    #[boulder(default=Some("1.0".to_string()))]
72    #[django(sort, op(in, contains, icontains, startswith, endswith))]
73    pub device_version: Option<String>,
74    #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
75    #[django(sort("id"), traverse, foreign_key = "id")]
76    pub physical_owner: Option<Proxy<User<C>>>,
77    #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
78    #[django(sort("id"), traverse, foreign_key = "id")]
79    pub physical_group: Option<Proxy<Group<C>>>,
80    #[boulder(default=Some("Test description for device.".to_string()),
81              generator=GSome(Pattern!("Test description {}", Inc(0))))]
82    #[django(sort, op(in, contains, icontains, startswith, endswith))]
83    pub description: Option<String>,
84
85    /* The API docs claim `tags` is a sort field, but testing this
86    reveals it's simply ignored. There's no obvious way to sort by
87    a vector of key ID, and the output is definitely not sorted.
88    */
89    #[boulder(generatable_with_persian_rug, sequence = 3usize)]
90    #[django(traverse, foreign_key = "id")]
91    pub tags: Vec<Proxy<Tag<C>>>,
92
93    #[django(sort)]
94    #[boulder(default=State::Idle)]
95    pub state: State,
96    #[django(sort)]
97    #[boulder(default=Health::Good)]
98    pub health: Health,
99    #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
100    #[django(sort("hostname"), traverse, foreign_key = "hostname")]
101    pub worker_host: Proxy<Worker<C>>,
102
103    #[django(unfilterable)]
104    pub is_synced: bool,
105    #[django(unfilterable, foreign_key = "id")]
106    pub last_health_report_job: Option<Proxy<Job<C>>>,
107}
108
109/// The health status of a [`Device`] from the LAVA API.
110#[derive(Clone, Debug, PartialEq, Eq, EnumString, PartialOrd, Ord, Display)]
111pub enum Health {
112    Unknown,
113    Maintenance,
114    Good,
115    Bad,
116    Looping,
117    Retired,
118}
119
120impl django_query::filtering::ops::Scalar for Health {}
121impl django_query::row::StringCellValue for Health {}
122
123/// The state of a [`Device`] from the LAVA API.
124#[derive(Clone, Debug, PartialEq, Eq, EnumString, PartialOrd, Ord, Display)]
125pub enum State {
126    Idle,
127    Reserved,
128    Running,
129}
130
131impl django_query::filtering::ops::Scalar for State {}
132impl django_query::row::StringCellValue for State {}
133
134#[cfg(test)]
135mod test {
136    use super::*;
137    use crate::state::{self, SharedState};
138
139    use anyhow::Result;
140    use boulder::BuilderWithPersianRug;
141    use boulder::GeneratorWithPersianRugIterator;
142    use boulder::Repeat;
143    use serde_json::{json, Value};
144    use test_log::test;
145
146    async fn make_request<T, U>(server_uri: T, endpoint: U) -> Result<Value>
147    where
148        T: AsRef<str>,
149        U: AsRef<str>,
150    {
151        let url = format!("{}/api/v0.2/{}", server_uri.as_ref(), endpoint.as_ref());
152        Ok(reqwest::get(&url).await?.json().await?)
153    }
154
155    #[tokio::test]
156    async fn test_devices() {
157        let mut p = SharedState::new();
158        {
159            let m = p.mutate();
160
161            let (worker, m) = Proxy::<Worker<_>>::builder().hostname("worker1").build(m);
162            let (device_type, m) = Proxy::<DeviceType<_>>::builder().name("type1").build(m);
163            let (_, m) = Proxy::<Device<_>>::builder()
164                .hostname("test1")
165                .worker_host(worker)
166                .device_type(device_type)
167                .description(Some("description of device".to_string()))
168                .health(Health::Good)
169                .build(m);
170
171            let (worker, m) = Proxy::<Worker<_>>::builder().hostname("worker2").build(m);
172            let (device_type, m) = Proxy::<DeviceType<_>>::builder().name("type2").build(m);
173            let _ = Proxy::<Device<state::State>>::builder()
174                .hostname("test2")
175                .worker_host(worker)
176                .device_type(device_type)
177                .description(Some("description of device".to_string()))
178                .health(Health::Bad)
179                .build(m);
180        }
181
182        let server = wiremock::MockServer::start().await;
183
184        wiremock::Mock::given(wiremock::matchers::method("GET"))
185            .and(wiremock::matchers::path("/api/v0.2/devices/"))
186            .respond_with(p.endpoint::<Device<state::State>>(Some(&server.uri()), None))
187            .mount(&server)
188            .await;
189
190        let devices = make_request(server.uri(), "devices/")
191            .await
192            .expect("failed to query devices");
193
194        assert_eq!(devices["results"][0]["hostname"], json!("test1"));
195        assert_eq!(devices["results"][1]["hostname"], json!("test2"));
196        assert_eq!(devices["results"].as_array().unwrap().len(), 2);
197    }
198
199    #[tokio::test]
200    async fn test_device_builder() {
201        let mut p = SharedState::new();
202        {
203            let m = p.mutate();
204            let (worker, m) = Proxy::<Worker<_>>::builder().hostname("worker1").build(m);
205            let (_, m) = Proxy::<Device<state::State>>::builder()
206                .hostname("test1")
207                .worker_host(worker)
208                .build(m);
209            let (worker, m) = Proxy::<Worker<_>>::builder().hostname("worker2").build(m);
210            let _ = Proxy::<Device<state::State>>::builder()
211                .hostname("test2")
212                .worker_host(worker)
213                .build(m);
214        }
215
216        let server = wiremock::MockServer::start().await;
217
218        wiremock::Mock::given(wiremock::matchers::method("GET"))
219            .and(wiremock::matchers::path("/api/v0.2/devices/"))
220            .respond_with(p.endpoint::<Device<state::State>>(Some(&server.uri()), None))
221            .mount(&server)
222            .await;
223
224        let devices = make_request(server.uri(), "devices/")
225            .await
226            .expect("failed to query devices");
227
228        assert_eq!(devices["results"][0]["hostname"], json!("test1"));
229        assert_eq!(devices["results"][1]["hostname"], json!("test2"));
230        assert_eq!(devices["results"].as_array().unwrap().len(), 2);
231    }
232
233    #[tokio::test]
234    async fn test_device_stream() {
235        let mut p = SharedState::new();
236        {
237            let m = p.mutate();
238            let devices = Proxy::<Device<_>>::generator().hostname(Repeat!("w1", "w2"));
239            let _ = GeneratorWithPersianRugIterator::new(devices, m)
240                .take(2)
241                .collect::<Vec<_>>();
242        }
243
244        let server = wiremock::MockServer::start().await;
245
246        wiremock::Mock::given(wiremock::matchers::method("GET"))
247            .and(wiremock::matchers::path("/api/v0.2/devices/"))
248            .respond_with(p.endpoint::<Device<state::State>>(Some(&server.uri()), None))
249            .mount(&server)
250            .await;
251
252        let devices = make_request(server.uri(), "devices/")
253            .await
254            .expect("failed to query devices");
255
256        assert_eq!(devices["results"][0]["hostname"], json!("w1"));
257        assert_eq!(devices["results"][1]["hostname"], json!("w2"));
258        assert_eq!(devices["results"].as_array().unwrap().len(), 2);
259    }
260
261    #[test(tokio::test)]
262    async fn test_output() {
263        let mut p = SharedState::new();
264        {
265            let m = p.mutate();
266            // let mut tag_gen = Proxy::<Tag<_>>::generator();
267
268            let gen = Proxy::<Device<_>>::generator()
269                .health(Repeat!(Health::Maintenance, Health::Good))
270                .description(|| None)
271                .device_version(|| None)
272                .physical_owner(|| None)
273                .physical_group(|| None)
274                .last_health_report_job(|| None); // GSome(Proxy::<Job<state::State>>::generator())
275                                                  //gen.tags(move || (&mut tag_gen).into_iter().take(2).collect::<Vec<_>>());
276
277            let _ = GeneratorWithPersianRugIterator::new(gen, m)
278                .take(4)
279                .collect::<Vec<_>>();
280        }
281
282        let server = wiremock::MockServer::start().await;
283        let ep = p.endpoint::<Device<_>>(Some(&server.uri()), Some(2));
284
285        wiremock::Mock::given(wiremock::matchers::method("GET"))
286            .and(wiremock::matchers::path("/api/v0.2/devices/"))
287            .respond_with(ep)
288            .mount(&server)
289            .await;
290
291        let body: serde_json::Value =
292            reqwest::get(&format!("{}/api/v0.2/devices/?limit=2", server.uri()))
293                .await
294                .expect("error getting devices")
295                .json()
296                .await
297                .expect("error parsing devices");
298
299        let next = format!("{}/api/v0.2/devices/?limit=2&offset=2", server.uri());
300
301        assert_eq!(
302            body,
303            serde_json::json! {
304                {
305                    "count": 4,
306                    "next": next,
307                    "previous": null,
308                    "results": [
309                        {
310                            "hostname": "test-device-0",
311                            "device_type": "test-device-type-0",
312                            "device_version": null,
313                            "physical_owner": null,
314                            "physical_group": null,
315                            "description": null,
316                            "tags": [
317                                0,
318                                1,
319                                2
320                            ],
321                            "state": "Idle",
322                            "health": "Maintenance",
323                            "last_health_report_job": null,
324                            "worker_host": "a-test-worker-1",
325                            "is_synced": false
326                        },
327                        {
328                            "hostname": "test-device-1",
329                            "device_type": "test-device-type-1",
330                            "device_version": null,
331                            "physical_owner": null,
332                            "physical_group": null,
333                            "description": null,
334                            "tags": [
335                                3,
336                                4,
337                                5
338                            ],
339                            "state": "Idle",
340                            "health": "Good",
341                            "last_health_report_job": null,
342                            "worker_host": "a-test-worker-2",
343                            "is_synced": false
344                        },
345                    ]
346                }
347            }
348        );
349    }
350}