actix / actix-extras

@@ -18,9 +18,9 @@
Loading
18 18
//!     // access session data
19 19
//!     if let Some(count) = session.get::<i32>("counter")? {
20 20
//!         println!("SESSION value: {}", count);
21 -
//!         session.set("counter", count + 1)?;
21 +
//!         session.insert("counter", count + 1)?;
22 22
//!     } else {
23 -
//!         session.set("counter", 1)?;
23 +
//!         session.insert("counter", 1)?;
24 24
//!     }
25 25
//!
26 26
//!     Ok("Welcome!")
@@ -39,21 +39,27 @@
Loading
39 39
//! }
40 40
//! ```
41 41
42 -
#![deny(rust_2018_idioms)]
42 +
#![deny(rust_2018_idioms, nonstandard_style)]
43 +
#![warn(missing_docs)]
43 44
44 -
use std::{cell::RefCell, collections::HashMap, rc::Rc};
45 +
use std::{
46 +
    cell::{Ref, RefCell},
47 +
    collections::HashMap,
48 +
    mem,
49 +
    rc::Rc,
50 +
};
45 51
46 -
use actix_web::dev::{
47 -
    Extensions, Payload, RequestHead, ServiceRequest, ServiceResponse,
52 +
use actix_web::{
53 +
    dev::{Extensions, Payload, RequestHead, ServiceRequest, ServiceResponse},
54 +
    Error, FromRequest, HttpMessage, HttpRequest,
48 55
};
49 -
use actix_web::{Error, FromRequest, HttpMessage, HttpRequest};
50 56
use futures_util::future::{ok, Ready};
51 57
use serde::{de::DeserializeOwned, Serialize};
52 58
53 59
#[cfg(feature = "cookie-session")]
54 60
mod cookie;
55 61
#[cfg(feature = "cookie-session")]
56 -
pub use crate::cookie::CookieSession;
62 +
pub use self::cookie::CookieSession;
57 63
58 64
/// The high-level interface you use to modify session data.
59 65
///
@@ -67,9 +73,9 @@
Loading
67 73
/// async fn index(session: Session) -> Result<&'static str> {
68 74
///     // access session data
69 75
///     if let Some(count) = session.get::<i32>("counter")? {
70 -
///         session.set("counter", count + 1)?;
76 +
///         session.insert("counter", count + 1)?;
71 77
///     } else {
72 -
///         session.set("counter", 1)?;
78 +
///         session.insert("counter", 1)?;
73 79
///     }
74 80
///
75 81
///     Ok("Welcome!")
@@ -79,6 +85,7 @@
Loading
79 85
80 86
/// Extraction of a [`Session`] object.
81 87
pub trait UserSession {
88 +
    /// Extract the [`Session`] object
82 89
    fn get_session(&self) -> Session;
83 90
}
84 91
@@ -100,13 +107,31 @@
Loading
100 107
    }
101 108
}
102 109
110 +
/// Status of a [`Session`].
103 111
#[derive(PartialEq, Clone, Debug)]
104 112
pub enum SessionStatus {
113 +
    /// Session has been updated and requires a new persist operation.
105 114
    Changed,
115 +
116 +
    /// Session is flagged for deletion and should be removed from client and server.
117 +
    ///
118 +
    /// Most operations on the session after purge flag is set should have no effect.
106 119
    Purged,
120 +
121 +
    /// Session is flagged for refresh.
122 +
    ///
123 +
    /// For example, when using a backend that has a TTL (time-to-live) expiry on the session entry,
124 +
    /// the session will be refreshed even if no data inside it has changed. The client may also
125 +
    /// be notified of the refresh.
107 126
    Renewed,
127 +
128 +
    /// Session is unchanged from when last seen (if exists).
129 +
    ///
130 +
    /// This state also captures new (previously unissued) sessions such as a user's first
131 +
    /// site visit.
108 132
    Unchanged,
109 133
}
134 +
110 135
impl Default for SessionStatus {
111 136
    fn default() -> SessionStatus {
112 137
        SessionStatus::Unchanged
@@ -116,7 +141,7 @@
Loading
116 141
#[derive(Default)]
117 142
struct SessionInner {
118 143
    state: HashMap<String, String>,
119 -
    pub status: SessionStatus,
144 +
    status: SessionStatus,
120 145
}
121 146
122 147
impl Session {
@@ -129,37 +154,80 @@
Loading
129 154
        }
130 155
    }
131 156
132 -
    /// Set a `value` from the session.
133 -
    pub fn set<T: Serialize>(&self, key: &str, value: T) -> Result<(), Error> {
157 +
    /// Get all raw key-value data from the session.
158 +
    ///
159 +
    /// Note that values are JSON encoded.
160 +
    pub fn entries(&self) -> Ref<'_, HashMap<String, String>> {
161 +
        Ref::map(self.0.borrow(), |inner| &inner.state)
162 +
    }
163 +
164 +
    /// Inserts a key-value pair into the session.
165 +
    ///
166 +
    /// Any serializable value can be used and will be encoded as JSON in session data, hence why
167 +
    /// only a reference to the value is taken.
168 +
    pub fn insert(
169 +
        &self,
170 +
        key: impl Into<String>,
171 +
        value: impl Serialize,
172 +
    ) -> Result<(), Error> {
134 173
        let mut inner = self.0.borrow_mut();
174 +
135 175
        if inner.status != SessionStatus::Purged {
136 176
            inner.status = SessionStatus::Changed;
137 -
            inner
138 -
                .state
139 -
                .insert(key.to_owned(), serde_json::to_string(&value)?);
177 +
            let val = serde_json::to_string(&value)?;
178 +
            inner.state.insert(key.into(), val);
140 179
        }
180 +
141 181
        Ok(())
142 182
    }
143 183
144 184
    /// Remove value from the session.
145 -
    pub fn remove(&self, key: &str) {
185 +
    ///
186 +
    /// If present, the JSON encoded value is returned.
187 +
    pub fn remove(&self, key: &str) -> Option<String> {
146 188
        let mut inner = self.0.borrow_mut();
189 +
147 190
        if inner.status != SessionStatus::Purged {
148 191
            inner.status = SessionStatus::Changed;
149 -
            inner.state.remove(key);
192 +
            return inner.state.remove(key);
150 193
        }
194 +
195 +
        None
196 +
    }
197 +
198 +
    /// Remove value from the session and deserialize.
199 +
    ///
200 +
    /// Returns None if key was not present in session. Returns T if deserialization succeeds,
201 +
    /// otherwise returns un-deserialized JSON string.
202 +
    pub fn remove_as<T: DeserializeOwned>(
203 +
        &self,
204 +
        key: &str,
205 +
    ) -> Option<Result<T, String>> {
206 +
        self.remove(key)
207 +
            .map(|val_str| match serde_json::from_str(&val_str) {
208 +
                Ok(val) => Ok(val),
209 +
                Err(_err) => {
210 +
                    log::debug!(
211 +
                        "removed value (key: {}) could not be deserialized as {}",
212 +
                        key,
213 +
                        std::any::type_name::<T>()
214 +
                    );
215 +
                    Err(val_str)
216 +
                }
217 +
            })
151 218
    }
152 219
153 220
    /// Clear the session.
154 221
    pub fn clear(&self) {
155 222
        let mut inner = self.0.borrow_mut();
223 +
156 224
        if inner.status != SessionStatus::Purged {
157 225
            inner.status = SessionStatus::Changed;
158 226
            inner.state.clear()
159 227
        }
160 228
    }
161 229
162 -
    /// Removes session, both client and server side.
230 +
    /// Removes session both client and server side.
163 231
    pub fn purge(&self) {
164 232
        let mut inner = self.0.borrow_mut();
165 233
        inner.status = SessionStatus::Purged;
@@ -169,6 +237,7 @@
Loading
169 237
    /// Renews the session key, assigning existing session state to new key.
170 238
    pub fn renew(&self) {
171 239
        let mut inner = self.0.borrow_mut();
240 +
172 241
        if inner.status != SessionStatus::Purged {
173 242
            inner.status = SessionStatus::Renewed;
174 243
        }
@@ -186,35 +255,32 @@
Loading
186 255
    /// let mut req = test::TestRequest::default().to_srv_request();
187 256
    ///
188 257
    /// Session::set_session(
189 -
    ///     vec![("counter".to_string(), serde_json::to_string(&0).unwrap())],
190 258
    ///     &mut req,
259 +
    ///     vec![("counter".to_string(), serde_json::to_string(&0).unwrap())],
191 260
    /// );
192 261
    /// ```
193 262
    pub fn set_session(
194 -
        data: impl IntoIterator<Item = (String, String)>,
195 263
        req: &mut ServiceRequest,
264 +
        data: impl IntoIterator<Item = (String, String)>,
196 265
    ) {
197 266
        let session = Session::get_session(&mut *req.extensions_mut());
198 267
        let mut inner = session.0.borrow_mut();
199 268
        inner.state.extend(data);
200 269
    }
201 270
271 +
    /// Returns session status and iterator of key-value pairs of changes.
202 272
    pub fn get_changes<B>(
203 273
        res: &mut ServiceResponse<B>,
204 -
    ) -> (
205 -
        SessionStatus,
206 -
        Option<impl Iterator<Item = (String, String)>>,
207 -
    ) {
274 +
    ) -> (SessionStatus, impl Iterator<Item = (String, String)>) {
208 275
        if let Some(s_impl) = res
209 276
            .request()
210 277
            .extensions()
211 278
            .get::<Rc<RefCell<SessionInner>>>()
212 279
        {
213 -
            let state =
214 -
                std::mem::replace(&mut s_impl.borrow_mut().state, HashMap::new());
215 -
            (s_impl.borrow().status.clone(), Some(state.into_iter()))
280 +
            let state = mem::take(&mut s_impl.borrow_mut().state);
281 +
            (s_impl.borrow().status.clone(), state.into_iter())
216 282
        } else {
217 -
            (SessionStatus::Unchanged, None)
283 +
            (SessionStatus::Unchanged, HashMap::new().into_iter())
218 284
        }
219 285
    }
220 286
@@ -230,21 +296,23 @@
Loading
230 296
231 297
/// Extractor implementation for Session type.
232 298
///
233 -
/// ```rust
299 +
/// # Examples
300 +
/// ```
234 301
/// # use actix_web::*;
235 302
/// use actix_session::Session;
236 303
///
237 -
/// fn index(session: Session) -> Result<&'static str> {
304 +
/// #[get("/")]
305 +
/// async fn index(session: Session) -> Result<impl Responder> {
238 306
///     // access session data
239 307
///     if let Some(count) = session.get::<i32>("counter")? {
240 -
///         session.set("counter", count + 1)?;
308 +
///         session.insert("counter", count + 1)?;
241 309
///     } else {
242 -
///         session.set("counter", 1)?;
310 +
///         session.insert("counter", 1)?;
243 311
///     }
244 312
///
245 -
///     Ok("Welcome!")
313 +
///     let count = session.get::<i32>("counter")?.unwrap();
314 +
///     Ok(format!("Counter: {}", count))
246 315
/// }
247 -
/// # fn main() {}
248 316
/// ```
249 317
impl FromRequest for Session {
250 318
    type Error = Error;
@@ -268,19 +336,19 @@
Loading
268 336
        let mut req = test::TestRequest::default().to_srv_request();
269 337
270 338
        Session::set_session(
271 -
            vec![("key".to_string(), serde_json::to_string("value").unwrap())],
272 339
            &mut req,
340 +
            vec![("key".to_string(), serde_json::to_string("value").unwrap())],
273 341
        );
274 342
        let session = Session::get_session(&mut *req.extensions_mut());
275 343
        let res = session.get::<String>("key").unwrap();
276 344
        assert_eq!(res, Some("value".to_string()));
277 345
278 -
        session.set("key2", "value2".to_string()).unwrap();
346 +
        session.insert("key2", "value2").unwrap();
279 347
        session.remove("key");
280 348
281 349
        let mut res = req.into_response(HttpResponse::Ok().finish());
282 350
        let (_status, state) = Session::get_changes(&mut res);
283 -
        let changes: Vec<_> = state.unwrap().collect();
351 +
        let changes: Vec<_> = state.collect();
284 352
        assert_eq!(changes, [("key2".to_string(), "\"value2\"".to_string())]);
285 353
    }
286 354
@@ -289,8 +357,8 @@
Loading
289 357
        let mut req = test::TestRequest::default().to_srv_request();
290 358
291 359
        Session::set_session(
292 -
            vec![("key".to_string(), serde_json::to_string(&true).unwrap())],
293 360
            &mut req,
361 +
            vec![("key".to_string(), serde_json::to_string(&true).unwrap())],
294 362
        );
295 363
296 364
        let session = req.get_session();
@@ -303,8 +371,8 @@
Loading
303 371
        let mut req = test::TestRequest::default().to_srv_request();
304 372
305 373
        Session::set_session(
306 -
            vec![("key".to_string(), serde_json::to_string(&10).unwrap())],
307 374
            &mut req,
375 +
            vec![("key".to_string(), serde_json::to_string(&10).unwrap())],
308 376
        );
309 377
310 378
        let session = req.head_mut().get_session();
@@ -329,4 +397,15 @@
Loading
329 397
        session.renew();
330 398
        assert_eq!(session.0.borrow().status, SessionStatus::Renewed);
331 399
    }
400 +
401 +
    #[test]
402 +
    fn session_entries() {
403 +
        let session = Session(Rc::new(RefCell::new(SessionInner::default())));
404 +
        session.insert("test_str", "val").unwrap();
405 +
        session.insert("test_num", 1).unwrap();
406 +
407 +
        let map = session.entries();
408 +
        map.contains_key("test_str");
409 +
        map.contains_key("test_num");
410 +
    }
332 411
}

@@ -8,7 +8,7 @@
Loading
8 8
use actix_web::http::{header::SET_COOKIE, HeaderValue};
9 9
use actix_web::{Error, HttpMessage, ResponseError};
10 10
use derive_more::Display;
11 -
use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready};
11 +
use futures_util::future::{ok, LocalBoxFuture, Ready};
12 12
use serde_json::error::Error as JsonError;
13 13
use time::{Duration, OffsetDateTime};
14 14
@@ -77,6 +77,7 @@
Loading
77 77
78 78
        let value =
79 79
            serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?;
80 +
80 81
        if value.len() > 4064 {
81 82
            return Err(CookieSessionError::Overflow.into());
82 83
        }
@@ -144,6 +145,7 @@
Loading
144 145
                            jar.private(&self.key).get(&self.name)
145 146
                        }
146 147
                    };
148 +
147 149
                    if let Some(cookie) = cookie_opt {
148 150
                        if let Ok(val) = serde_json::from_str(cookie.value()) {
149 151
                            return (false, val);
@@ -152,6 +154,7 @@
Loading
152 154
                }
153 155
            }
154 156
        }
157 +
155 158
        (true, HashMap::new())
156 159
    }
157 160
}
@@ -309,7 +312,7 @@
Loading
309 312
    }
310 313
}
311 314
312 -
/// Cookie session middleware
315 +
/// Cookie based session middleware.
313 316
pub struct CookieSessionMiddleware<S> {
314 317
    service: S,
315 318
    inner: Rc<CookieSessionInner>,
@@ -336,41 +339,40 @@
Loading
336 339
        let inner = self.inner.clone();
337 340
        let (is_new, state) = self.inner.load(&req);
338 341
        let prolong_expiration = self.inner.expires_in.is_some();
339 -
        Session::set_session(state, &mut req);
342 +
        Session::set_session(&mut req, state);
340 343
341 344
        let fut = self.service.call(req);
342 345
343 -
        async move {
344 -
            fut.await.map(|mut res| {
345 -
                match Session::get_changes(&mut res) {
346 -
                    (SessionStatus::Changed, Some(state))
347 -
                    | (SessionStatus::Renewed, Some(state)) => {
348 -
                        res.checked_expr(|res| inner.set_cookie(res, state))
349 -
                    }
350 -
                    (SessionStatus::Unchanged, Some(state)) if prolong_expiration => {
351 -
                        res.checked_expr(|res| inner.set_cookie(res, state))
352 -
                    }
353 -
                    (SessionStatus::Unchanged, _) =>
354 -
                    // set a new session cookie upon first request (new client)
355 -
                    {
356 -
                        if is_new {
357 -
                            let state: HashMap<String, String> = HashMap::new();
358 -
                            res.checked_expr(|res| {
359 -
                                inner.set_cookie(res, state.into_iter())
360 -
                            })
361 -
                        } else {
362 -
                            res
363 -
                        }
364 -
                    }
365 -
                    (SessionStatus::Purged, _) => {
366 -
                        let _ = inner.remove_cookie(&mut res);
346 +
        Box::pin(async move {
347 +
            let mut res = fut.await?;
348 +
349 +
            let res = match Session::get_changes(&mut res) {
350 +
                (SessionStatus::Changed, state) | (SessionStatus::Renewed, state) => {
351 +
                    res.checked_expr(|res| inner.set_cookie(res, state))
352 +
                }
353 +
354 +
                (SessionStatus::Unchanged, state) if prolong_expiration => {
355 +
                    res.checked_expr(|res| inner.set_cookie(res, state))
356 +
                }
357 +
358 +
                // set a new session cookie upon first request (new client)
359 +
                (SessionStatus::Unchanged, _) => {
360 +
                    if is_new {
361 +
                        let state: HashMap<String, String> = HashMap::new();
362 +
                        res.checked_expr(|res| inner.set_cookie(res, state.into_iter()))
363 +
                    } else {
367 364
                        res
368 365
                    }
369 -
                    _ => res,
370 366
                }
371 -
            })
372 -
        }
373 -
        .boxed_local()
367 +
368 +
                (SessionStatus::Purged, _) => {
369 +
                    let _ = inner.remove_cookie(&mut res);
370 +
                    res
371 +
                }
372 +
            };
373 +
374 +
            Ok(res)
375 +
        })
374 376
    }
375 377
}
376 378
@@ -386,7 +388,7 @@
Loading
386 388
            App::new()
387 389
                .wrap(CookieSession::signed(&[0; 32]).secure(false))
388 390
                .service(web::resource("/").to(|ses: Session| async move {
389 -
                    let _ = ses.set("counter", 100);
391 +
                    let _ = ses.insert("counter", 100);
390 392
                    "test"
391 393
                })),
392 394
        )
@@ -406,7 +408,7 @@
Loading
406 408
            App::new()
407 409
                .wrap(CookieSession::private(&[0; 32]).secure(false))
408 410
                .service(web::resource("/").to(|ses: Session| async move {
409 -
                    let _ = ses.set("counter", 100);
411 +
                    let _ = ses.insert("counter", 100);
410 412
                    "test"
411 413
                })),
412 414
        )
@@ -426,7 +428,7 @@
Loading
426 428
            App::new()
427 429
                .wrap(CookieSession::signed(&[0; 32]).secure(false).lazy(true))
428 430
                .service(web::resource("/count").to(|ses: Session| async move {
429 -
                    let _ = ses.set("counter", 100);
431 +
                    let _ = ses.insert("counter", 100);
430 432
                    "counting"
431 433
                }))
432 434
                .service(web::resource("/").to(|_ses: Session| async move { "test" })),
@@ -452,7 +454,7 @@
Loading
452 454
            App::new()
453 455
                .wrap(CookieSession::signed(&[0; 32]).secure(false))
454 456
                .service(web::resource("/").to(|ses: Session| async move {
455 -
                    let _ = ses.set("counter", 100);
457 +
                    let _ = ses.insert("counter", 100);
456 458
                    "test"
457 459
                })),
458 460
        )
@@ -480,7 +482,7 @@
Loading
480 482
                        .max_age(100),
481 483
                )
482 484
                .service(web::resource("/").to(|ses: Session| async move {
483 -
                    let _ = ses.set("counter", 100);
485 +
                    let _ = ses.insert("counter", 100);
484 486
                    "test"
485 487
                }))
486 488
                .service(web::resource("/test/").to(|ses: Session| async move {
@@ -513,7 +515,7 @@
Loading
513 515
            App::new()
514 516
                .wrap(CookieSession::signed(&[0; 32]).secure(false).expires_in(60))
515 517
                .service(web::resource("/").to(|ses: Session| async move {
516 -
                    let _ = ses.set("counter", 100);
518 +
                    let _ = ses.insert("counter", 100);
517 519
                    "test"
518 520
                }))
519 521
                .service(

@@ -157,8 +157,9 @@
Loading
157 157
158 158
        Box::pin(async move {
159 159
            let state = inner.load(&req).await?;
160 +
160 161
            let value = if let Some((state, value)) = state {
161 -
                Session::set_session(state, &mut req);
162 +
                Session::set_session(&mut req, state);
162 163
                Some(value)
163 164
            } else {
164 165
                None
@@ -167,8 +168,7 @@
Loading
167 168
            let mut res = srv.call(req).await?;
168 169
169 170
            match Session::get_changes(&mut res) {
170 -
                (SessionStatus::Unchanged, None) => Ok(res),
171 -
                (SessionStatus::Unchanged, Some(state)) => {
171 +
                (SessionStatus::Unchanged, state) => {
172 172
                    if value.is_none() {
173 173
                        // implies the session is new
174 174
                        inner.update(res, state, value).await
@@ -176,10 +176,10 @@
Loading
176 176
                        Ok(res)
177 177
                    }
178 178
                }
179 -
                (SessionStatus::Changed, Some(state)) => {
180 -
                    inner.update(res, state, value).await
181 -
                }
182 -
                (SessionStatus::Purged, Some(_)) => {
179 +
180 +
                (SessionStatus::Changed, state) => inner.update(res, state, value).await,
181 +
182 +
                (SessionStatus::Purged, _) => {
183 183
                    if let Some(val) = value {
184 184
                        inner.clear_cache(val).await?;
185 185
                        match inner.remove_cookie(&mut res) {
@@ -190,7 +190,8 @@
Loading
190 190
                        Err(error::ErrorInternalServerError("unexpected"))
191 191
                    }
192 192
                }
193 -
                (SessionStatus::Renewed, Some(state)) => {
193 +
194 +
                (SessionStatus::Renewed, state) => {
194 195
                    if let Some(val) = value {
195 196
                        inner.clear_cache(val).await?;
196 197
                        inner.update(res, state, None).await
@@ -198,7 +199,6 @@
Loading
198 199
                        inner.update(res, state, None).await
199 200
                    }
200 201
                }
201 -
                (_, None) => unreachable!(),
202 202
            }
203 203
        })
204 204
    }
@@ -343,7 +343,7 @@
Loading
343 343
        Ok(res)
344 344
    }
345 345
346 -
    /// removes cache entry
346 +
    /// Removes cache entry.
347 347
    async fn clear_cache(&self, key: String) -> Result<(), Error> {
348 348
        let cache_key = (self.cache_keygen)(&key);
349 349
@@ -362,7 +362,7 @@
Loading
362 362
        }
363 363
    }
364 364
365 -
    /// invalidates session cookie
365 +
    /// Invalidates session cookie.
366 366
    fn remove_cookie<B>(&self, res: &mut ServiceResponse<B>) -> Result<(), Error> {
367 367
        let mut cookie = Cookie::named(self.name.clone());
368 368
        cookie.set_value("");
@@ -411,7 +411,7 @@
Loading
411 411
            .get::<i32>("counter")
412 412
            .unwrap_or(Some(0))
413 413
            .map_or(1, |inner| inner + 1);
414 -
        session.set("counter", counter)?;
414 +
        session.insert("counter", &counter)?;
415 415
416 416
        Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
417 417
    }
@@ -426,7 +426,7 @@
Loading
426 426
        session: Session,
427 427
    ) -> Result<HttpResponse> {
428 428
        let id = user_id.into_inner().user_id;
429 -
        session.set("user_id", &id)?;
429 +
        session.insert("user_id", &id)?;
430 430
        session.renew();
431 431
432 432
        let counter: i32 = session
Files Coverage
actix-cors/src 87.59%
actix-identity/src 95.96%
actix-redis/src 82.10%
actix-session/src 87.97%
actix-web-httpauth/src 60.59%
actix-protobuf/src/lib.rs 50.00%
Project Totals (29 files) 80.43%
1
comment: false
2

3
coverage:
4
  status:
5
    project:
6
      default:
7
        threshold: 100% # make CI green
8
    patch:
9
      default:
10
        threshold: 100% # make CI green
11

12
# ignore code coverage on following paths
13
ignore:
14
  - "**/tests"
15
  - "**/benches"
16
  - "**/examples"
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading