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

VolumeGraphics / havocompare / 21669188415

04 Feb 2026 11:09AM UTC coverage: 83.7% (+1.9%) from 81.767%
21669188415

Pull #58

github

BBertram-hex
57: implement explicit 'G' variants that use globbing as fall-back

- KeepColumnsByNameG
- DeleteColumnByNameG
- TODO: fix ambiguity
Pull Request #58: 57 new preprocessor step for whitlisting of CSV columns

343 of 348 new or added lines in 1 file covered. (98.56%)

17 existing lines in 1 file now uncovered.

3081 of 3681 relevant lines covered (83.7%)

2580.77 hits per line

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

96.33
/src/csv/preprocessing.rs
1
use crate::csv;
2
use crate::csv::value::Value;
3
use crate::csv::Table;
4
use glob::Pattern;
5
use schemars_derive::JsonSchema;
6
use serde::{Deserialize, Serialize};
7
use std::cmp::Ordering::Equal;
8
use tracing::{debug, info, warn};
9

10
#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
11
/// Preprocessor options
12
pub enum Preprocessor {
13
    /// Try to extract the headers from the first row - fallible if first row contains a number
14
    ExtractHeaders,
15
    /// Replace all fields in column by number by a deleted marker
16
    DeleteColumnByNumber(usize),
17
    /// Replace all fields in column by name by a deleted marker
18
    DeleteColumnByName(String),
19
    /// Replace all fields in column whose header matches a glob pattern by a deleted marker
20
    DeleteColumnByNameG(String),
21
    /// Replace with deleted marker: all fields in all columns that do not match any of the specified name
22
    KeepColumnsByName(Vec<String>),
23
    /// Replace with deleted marker: all fields in all columns that do not match any of the specified glob patterns
24
    KeepColumnsByNameG(Vec<String>),
25
    /// Sort rows by column with given name. Fails if no headers were extracted or column name is not found, or if any row has no numbers there
26
    SortByColumnName(String),
27
    /// Sort rows by column with given number. Fails if any row has no numbers there or if out of bounds.
28
    SortByColumnNumber(usize),
29
    /// Replace all fields in row with given number by a deleted marker
30
    DeleteRowByNumber(usize),
31
    /// Replace all fields in row  where at least a single field matches regex by a deleted marker
32
    DeleteRowByRegex(String),
33
    /// replace found cell using row and column index by a deleted marker
34
    DeleteCellByNumber {
35
        /// column number
36
        column: usize,
37
        /// row number
38
        row: usize,
39
    },
40
    /// replace found cell using column header and row index by a deleted marker
41
    DeleteCellByName {
42
        /// column with given name
43
        column: String,
44
        /// row number
45
        row: usize,
46
    },
47
}
48

49
impl Preprocessor {
50
    pub(crate) fn process(&self, table: &mut Table) -> Result<(), csv::Error> {
146✔
51
        match self {
146✔
52
            Preprocessor::ExtractHeaders => extract_headers(table),
146✔
53
            Preprocessor::DeleteColumnByNumber(id) => delete_column_number(table, *id),
×
54
            Preprocessor::DeleteColumnByName(name) => delete_column_name(table, name.as_str()),
×
NEW
55
            Preprocessor::DeleteColumnByNameG(name) => {
×
NEW
56
                delete_column_name_glob(table, name.as_str())
×
57
            }
NEW
58
            Preprocessor::KeepColumnsByName(names) => keep_columns_matching_any_names(table, names),
×
NEW
59
            Preprocessor::KeepColumnsByNameG(names) => {
×
NEW
60
                keep_columns_matching_any_names_glob(table, names)
×
61
            }
62
            Preprocessor::SortByColumnName(name) => sort_by_column_name(table, name.as_str()),
×
63
            Preprocessor::SortByColumnNumber(id) => sort_by_column_id(table, *id),
×
64
            Preprocessor::DeleteRowByNumber(id) => delete_row_by_number(table, *id),
×
65
            Preprocessor::DeleteRowByRegex(regex) => delete_row_by_regex(table, regex),
×
66
            Preprocessor::DeleteCellByNumber { column, row } => {
×
67
                delete_cell_by_number(table, *column, *row)
×
68
            }
69
            Preprocessor::DeleteCellByName { column, row } => {
×
70
                delete_cell_by_column_name_and_row_number(table, column, *row)
×
71
            }
72
        }
73
    }
146✔
74
}
75

76
fn delete_row_by_regex(table: &mut Table, regex: &str) -> Result<(), csv::Error> {
1✔
77
    let regex = regex::Regex::new(regex)?;
1✔
78
    table
1✔
79
        .rows_mut()
1✔
80
        .filter(|row| row.iter().any(|v| regex.is_match(v.to_string().as_str())))
1,027✔
81
        .for_each(|mut row| row.iter_mut().for_each(|v| **v = Value::deleted()));
2✔
82
    Ok(())
1✔
83
}
1✔
84

85
fn delete_row_by_number(table: &mut Table, id: usize) -> Result<(), csv::Error> {
1✔
86
    if let Some(mut v) = table.rows_mut().nth(id) {
1✔
87
        v.iter_mut().for_each(|v| **v = Value::deleted())
2✔
88
    }
×
89
    Ok(())
1✔
90
}
1✔
91

92
fn delete_cell_by_number(table: &mut Table, column: usize, row: usize) -> Result<(), csv::Error> {
1✔
93
    let value = table
1✔
94
        .columns
1✔
95
        .get_mut(column)
1✔
96
        .ok_or_else(|| {
1✔
97
            csv::Error::InvalidAccess(format!("Cell with column number {} not found.", column))
×
98
        })?
×
99
        .rows
100
        .get_mut(row)
1✔
101
        .ok_or_else(|| {
1✔
102
            csv::Error::InvalidAccess(format!("Cell with row number {} not found.", row))
×
103
        })?;
×
104

105
    *value = Value::deleted();
1✔
106

107
    Ok(())
1✔
108
}
1✔
109

110
fn delete_cell_by_column_name_and_row_number(
1✔
111
    table: &mut Table,
1✔
112
    column: &str,
1✔
113
    row: usize,
1✔
114
) -> Result<(), csv::Error> {
1✔
115
    let value = table
1✔
116
        .columns
1✔
117
        .iter_mut()
1✔
118
        .find(|col| col.header.as_deref().unwrap_or_default() == column)
2✔
119
        .ok_or_else(|| {
1✔
120
            csv::Error::InvalidAccess(format!("Cell with column name '{}' not found.", column))
×
121
        })?
×
122
        .rows
123
        .get_mut(row)
1✔
124
        .ok_or_else(|| {
1✔
125
            csv::Error::InvalidAccess(format!("Cell with row number {} not found.", row))
×
126
        })?;
×
127

128
    *value = Value::deleted();
1✔
129

130
    Ok(())
1✔
131
}
1✔
132

133
fn get_permutation(rows_to_sort_by: &Vec<f64>) -> permutation::Permutation {
2✔
134
    permutation::sort_by(rows_to_sort_by, |a, b| b.partial_cmp(a).unwrap_or(Equal))
6,862✔
135
}
2✔
136

137
fn apply_permutation(table: &mut Table, mut permutation: permutation::Permutation) {
2✔
138
    table.columns.iter_mut().for_each(|c| {
4✔
139
        permutation.apply_slice_in_place(&mut c.rows);
4✔
140
    });
4✔
141
}
2✔
142

143
fn sort_by_column_id(table: &mut Table, id: usize) -> Result<(), csv::Error> {
3✔
144
    let sort_master_col = table.columns.get(id).ok_or_else(|| {
3✔
145
        csv::Error::InvalidAccess(format!(
1✔
146
            "Column number sorting by id {id} requested but column not found."
1✔
147
        ))
1✔
148
    })?;
1✔
149
    let col_floats: Result<Vec<_>, csv::Error> = sort_master_col
2✔
150
        .rows
2✔
151
        .iter()
2✔
152
        .map(|v| {
515✔
153
            v.get_quantity().map(|q| q.value).ok_or_else(|| {
515✔
154
                csv::Error::UnexpectedValue(
1✔
155
                    v.clone(),
1✔
156
                    "Expected quantity while trying to sort by column id".to_string(),
1✔
157
                )
1✔
158
            })
1✔
159
        })
515✔
160
        .collect();
2✔
161
    let permutation = get_permutation(&col_floats?);
2✔
162
    apply_permutation(table, permutation);
1✔
163
    Ok(())
1✔
164
}
3✔
165

166
fn sort_by_column_name(table: &mut Table, name: &str) -> Result<(), csv::Error> {
3✔
167
    let sort_master_col = table
3✔
168
        .columns
3✔
169
        .iter()
3✔
170
        .find(|c| c.header.as_deref().unwrap_or_default() == name)
5✔
171
        .ok_or_else(|| {
3✔
172
            csv::Error::InvalidAccess(format!(
1✔
173
                "Requested format sorting by column'{name}' but column not found."
1✔
174
            ))
1✔
175
        })?;
1✔
176
    let col_floats: Result<Vec<_>, csv::Error> = sort_master_col
2✔
177
        .rows
2✔
178
        .iter()
2✔
179
        .map(|v| {
515✔
180
            v.get_quantity().map(|q| q.value).ok_or_else(|| {
515✔
181
                csv::Error::UnexpectedValue(
1✔
182
                    v.clone(),
1✔
183
                    "Expected quantity while trying to sort by column name".to_string(),
1✔
184
                )
1✔
185
            })
1✔
186
        })
515✔
187
        .collect();
2✔
188
    let permutation = get_permutation(&col_floats?);
2✔
189
    apply_permutation(table, permutation);
1✔
190
    Ok(())
1✔
191
}
3✔
192

193
fn delete_column_name(table: &mut Table, name: &str) -> Result<(), csv::Error> {
5✔
194
    if let Some(c) = table
5✔
195
        .columns
5✔
196
        .iter_mut()
5✔
197
        .find(|col| col.header.as_deref().unwrap_or_default() == name)
9✔
198
    {
4✔
199
        c.delete_contents();
4✔
200
    }
4✔
201
    Ok(())
5✔
202
}
5✔
203

204
fn delete_column_name_glob(table: &mut Table, name: &str) -> Result<(), csv::Error> {
5✔
205
    let pattern = Pattern::new(name).map_err(|e| {
5✔
206
        csv::Error::InvalidAccess(format!(
1✔
207
            "Invalid glob pattern in DeleteColumnByNameG '{}': {}",
1✔
208
            name, e
1✔
209
        ))
1✔
210
    })?;
1✔
211

212
    if let Some(c) = table.columns.iter_mut().find(|col| {
5✔
213
        col.header.as_deref().unwrap_or_default() == name
5✔
214
            || pattern.matches(col.header.as_deref().unwrap_or_default())
4✔
215
    }) {
5✔
216
        c.delete_contents();
4✔
217
    }
4✔
218
    Ok(())
4✔
219
}
5✔
220

221
fn delete_column_number(table: &mut Table, id: usize) -> Result<(), csv::Error> {
1✔
222
    if let Some(col) = table.columns.get_mut(id) {
1✔
223
        col.delete_contents();
1✔
224
    }
1✔
225
    Ok(())
1✔
226
}
1✔
227

228
fn keep_columns_matching_any_names(table: &mut Table, names: &[String]) -> Result<(), csv::Error> {
3✔
229
    table.columns.iter_mut().for_each(|col| {
11✔
230
        let header = col.header.as_deref().unwrap_or_default();
11✔
231
        if names.iter().any(|name| header == name) {
13✔
232
            info!("Keep header: \"{}\" (exact match)", header);
4✔
233
        } else {
7✔
234
            col.delete_contents();
7✔
235
        }
7✔
236
    });
11✔
237
    Ok(())
3✔
238
}
3✔
239

240
fn keep_columns_matching_any_names_glob(
7✔
241
    table: &mut Table,
7✔
242
    names: &[String],
7✔
243
) -> Result<(), csv::Error> {
7✔
244
    let patterns: Result<Vec<Pattern>, csv::Error> = names
7✔
245
        .iter()
7✔
246
        .map(|name| {
8✔
247
            Pattern::new(name).map_err(|e| {
8✔
248
                csv::Error::InvalidAccess(format!(
1✔
249
                    "Invalid glob pattern in KeepColumnsByNameG '{}': {}",
1✔
250
                    name, e
1✔
251
                ))
1✔
252
            })
1✔
253
        })
8✔
254
        .collect();
7✔
255
    let patterns = patterns?;
7✔
256

257
    table.columns.iter_mut().for_each(|col| {
21✔
258
        let header = col.header.as_deref().unwrap_or_default();
21✔
259
        if !(names.iter().zip(patterns.iter()).any(|(name, pattern)| {
24✔
260
            if header == name {
24✔
261
                info!("Keep header: \"{}\" (exact match)", header);
3✔
262
                true
3✔
263
            } else if pattern.matches(header) {
21✔
264
                info!("Keep header: \"{}\" (matches: \"{}\")", header, name);
9✔
265
                true
9✔
266
            } else {
267
                false
12✔
268
            }
269
        })) {
24✔
270
            col.delete_contents();
9✔
271
        }
12✔
272
    });
21✔
273
    Ok(())
6✔
274
}
7✔
275

276
fn extract_headers(table: &mut Table) -> Result<(), csv::Error> {
173✔
277
    debug!("Extracting headers...");
173✔
278
    let can_extract = table
173✔
279
        .columns
173✔
280
        .iter()
173✔
281
        .all(|c| matches!(c.rows.first(), Some(Value::String(_))));
456✔
282
    if !can_extract {
173✔
283
        warn!("Cannot extract header for this csv!");
1✔
284
        return Ok(());
1✔
285
    }
172✔
286

287
    for col in table.columns.iter_mut() {
455✔
288
        let title = col.rows.drain(0..1).next().ok_or_else(|| {
455✔
289
            csv::Error::InvalidAccess("Tried to extract header of empty column!".to_string())
×
290
        })?;
×
291
        if let Value::String(title) = title {
455✔
292
            col.header = Some(title);
455✔
293
        }
455✔
294
    }
295
    Ok(())
172✔
296
}
173✔
297

298
#[cfg(test)]
299
mod tests {
300
    use super::*;
301
    use crate::csv::{Column, Delimiters, Error};
302
    use std::{fs::File, io::Cursor};
303

304
    macro_rules! string_vec {
305
        ($($x:expr),*) => (vec![$($x.to_string()),*]);
306
    }
307

308
    fn setup_table(delimiters: Option<Delimiters>) -> Table {
13✔
309
        let delimiters = delimiters.unwrap_or_default();
13✔
310
        Table::from_reader(
13✔
311
            File::open("tests/csv/data/DeviationHistogram.csv").unwrap(),
13✔
312
            &delimiters,
13✔
313
        )
314
        .unwrap()
13✔
315
    }
13✔
316

317
    /// Helper function to create a Table from CSV string content for testing
318
    fn table_from_string(content: &str) -> Table {
17✔
319
        let cursor = Cursor::new(content.as_bytes());
17✔
320

321
        Table::from_reader(
17✔
322
            cursor,
17✔
323
            &Delimiters {
17✔
324
                field_delimiter: Some(';'),
17✔
325
                decimal_separator: Some('.'),
17✔
326
            },
17✔
327
        )
328
        .unwrap()
17✔
329
    }
17✔
330

331
    fn setup_table_two(delimiters: Option<Delimiters>) -> Table {
1✔
332
        let delimiters = delimiters.unwrap_or_default();
1✔
333
        Table::from_reader(
1✔
334
            File::open("tests/csv/data/defects_headers.csv").unwrap(),
1✔
335
            &delimiters,
1✔
336
        )
337
        .unwrap()
1✔
338
    }
1✔
339

340
    #[test]
341
    fn test_extract_headers_two() {
1✔
342
        let mut table = setup_table_two(None);
1✔
343
        extract_headers(&mut table).unwrap();
1✔
344
        assert_eq!(
1✔
345
            table.columns.first().unwrap().header.as_deref().unwrap(),
1✔
346
            "Entry"
347
        );
348
        assert_eq!(
1✔
349
            table.columns.last().unwrap().header.as_deref().unwrap(),
1✔
350
            "Radius"
351
        );
352
    }
1✔
353

354
    #[test]
355
    fn test_extract_headers_from_string() {
1✔
356
        let content = "Header1;Header2;Header3\nValue1;Value2;Value3";
1✔
357
        let mut table = table_from_string(content);
1✔
358

359
        assert_eq!(table.columns.len(), 3);
1✔
360
        assert!(table.columns[0].header.is_none());
1✔
361

362
        extract_headers(&mut table).unwrap();
1✔
363

364
        assert_eq!(table.columns[0].header.as_deref().unwrap(), "Header1");
1✔
365
        assert_eq!(table.columns[1].header.as_deref().unwrap(), "Header2");
1✔
366
        assert_eq!(table.columns[2].header.as_deref().unwrap(), "Header3");
1✔
367
        assert_eq!(table.columns[0].rows.len(), 1); // One row remaining after header extraction
1✔
368
    }
1✔
369

370
    #[test]
371
    fn test_extract_headers_fails_with_numbers() {
1✔
372
        let content = "1.5;2.5;3.5\nValue1;Value2;Value3";
1✔
373
        let mut table = table_from_string(content);
1✔
374

375
        // Should not extract headers when first row contains numbers
376
        extract_headers(&mut table).unwrap();
1✔
377

378
        assert!(table.columns[0].header.is_none());
1✔
379
        assert_eq!(table.columns[0].rows.len(), 2); // Both rows still present
1✔
380
    }
1✔
381

382
    #[test]
383
    fn test_extract_headers() {
1✔
384
        let mut table = setup_table(None);
1✔
385
        extract_headers(&mut table).unwrap();
1✔
386
        assert_eq!(
1✔
387
            table.columns.first().unwrap().header.as_deref().unwrap(),
1✔
388
            "Deviation [mm]"
389
        );
390
        assert_eq!(
1✔
391
            table.columns.last().unwrap().header.as_deref().unwrap(),
1✔
392
            "Surface [mm²]"
393
        );
394
    }
1✔
395

396
    #[test]
397
    fn test_delete_column_by_id() {
1✔
398
        let mut table = setup_table(None);
1✔
399
        extract_headers(&mut table).unwrap();
1✔
400
        delete_column_number(&mut table, 0).unwrap();
1✔
401
        assert_eq!(
1✔
402
            table.columns.first().unwrap().header.as_deref().unwrap(),
1✔
403
            "DELETED"
404
        );
405
        assert!(table
1✔
406
            .columns
1✔
407
            .first()
1✔
408
            .unwrap()
1✔
409
            .rows
1✔
410
            .iter()
1✔
411
            .all(|v| *v == Value::deleted()));
513✔
412
    }
1✔
413

414
    #[test]
415
    fn test_delete_column_by_name() {
1✔
416
        let mut table = setup_table(None);
1✔
417
        extract_headers(&mut table).unwrap();
1✔
418
        delete_column_name(&mut table, "Surface [mm²]").unwrap();
1✔
419
        assert_eq!(
1✔
420
            table.columns.last().unwrap().header.as_deref().unwrap(),
1✔
421
            "DELETED"
422
        );
423
        assert!(table
1✔
424
            .columns
1✔
425
            .last()
1✔
426
            .unwrap()
1✔
427
            .rows
1✔
428
            .iter()
1✔
429
            .all(|v| *v == Value::deleted()));
513✔
430
    }
1✔
431

432
    #[test]
433
    fn test_delete_column_by_name_glob() {
1✔
434
        let mut table = setup_table(None);
1✔
435
        extract_headers(&mut table).unwrap();
1✔
436
        delete_column_name_glob(&mut table, "Surface*").unwrap();
1✔
437
        assert_eq!(
1✔
438
            table.columns.last().unwrap().header.as_deref().unwrap(),
1✔
439
            "DELETED"
440
        );
441
        assert!(table
1✔
442
            .columns
1✔
443
            .last()
1✔
444
            .unwrap()
1✔
445
            .rows
1✔
446
            .iter()
1✔
447
            .all(|v| *v == Value::deleted()));
513✔
448
    }
1✔
449

450
    #[test]
451
    fn test_keep_columns_matching_any_names() {
1✔
452
        let mut table = setup_table(None);
1✔
453
        // column headers in test table: "Deviation [mm]", "Surface [mm²]"
454
        extract_headers(&mut table).unwrap();
1✔
455

456
        let keep_names = string_vec![
1✔
457
            "Deviation [mm]",
1✔
458
            "Any name to be kept, which is no table header -> no effect"
1✔
459
        ];
460
        keep_columns_matching_any_names(&mut table, &keep_names).unwrap();
1✔
461
        assert_eq!(
1✔
462
            table.columns.first().unwrap().header.as_deref().unwrap(),
1✔
463
            "Deviation [mm]",
464
            "First column was deleted, although it is in the list of names to be kept!"
465
        );
466
        assert_eq!(
1✔
467
            table.columns.last().unwrap().header.as_deref().unwrap(),
1✔
468
            "DELETED",
469
            "Second column was not deleted, although it is not in the list of names to be kept!",
470
        );
471
        assert!(table
1✔
472
            .columns
1✔
473
            .last()
1✔
474
            .unwrap()
1✔
475
            .rows
1✔
476
            .iter()
1✔
477
            .all(|v| *v == Value::deleted()));
513✔
478
    }
1✔
479

480
    #[test]
481
    fn test_keep_columns_matching_glob_patterns() {
1✔
482
        let mut table = setup_table(None);
1✔
483
        // column headers in test table: "Deviation [mm]", "Surface [mm²]"
484
        extract_headers(&mut table).unwrap();
1✔
485

486
        let keep_names = string_vec![
1✔
487
            "Surface*"  // Should match "Surface [mm²]" via glob pattern
1✔
488
        ];
489
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
490
        assert_eq!(
1✔
491
            table.columns.first().unwrap().header.as_deref().unwrap(),
1✔
492
            "DELETED",
493
            "First column (Deviation) should be deleted as it doesn't match pattern!"
494
        );
495
        assert_eq!(
1✔
496
            table.columns.last().unwrap().header.as_deref().unwrap(),
1✔
497
            "Surface [mm²]",
498
            "Second column (Surface) was deleted, although it matches the glob pattern!",
499
        );
500
        assert!(table
1✔
501
            .columns
1✔
502
            .first()
1✔
503
            .unwrap()
1✔
504
            .rows
1✔
505
            .iter()
1✔
506
            .all(|v| *v == Value::deleted()));
513✔
507
    }
1✔
508

509
    #[test]
510
    fn test_delete_row_by_id() {
1✔
511
        let mut table = setup_table(None);
1✔
512
        delete_row_by_number(&mut table, 0).unwrap();
1✔
513
        assert_eq!(
1✔
514
            table
1✔
515
                .columns
1✔
516
                .first()
1✔
517
                .unwrap()
1✔
518
                .rows
1✔
519
                .first()
1✔
520
                .unwrap()
1✔
521
                .get_string()
1✔
522
                .as_deref()
1✔
523
                .unwrap(),
1✔
524
            "DELETED"
525
        );
526
    }
1✔
527

528
    #[test]
529
    fn test_delete_row_by_regex() {
1✔
530
        let mut table = setup_table(None);
1✔
531
        delete_row_by_regex(&mut table, "mm").unwrap();
1✔
532
        assert_eq!(
1✔
533
            table
1✔
534
                .columns
1✔
535
                .first()
1✔
536
                .unwrap()
1✔
537
                .rows
1✔
538
                .first()
1✔
539
                .unwrap()
1✔
540
                .get_string()
1✔
541
                .as_deref()
1✔
542
                .unwrap(),
1✔
543
            "DELETED"
544
        );
545
    }
1✔
546

547
    #[test]
548
    fn test_sort_by_name() {
1✔
549
        let mut table = setup_table(None);
1✔
550
        extract_headers(&mut table).unwrap();
1✔
551
        sort_by_column_name(&mut table, "Surface [mm²]").unwrap();
1✔
552
        let mut peekable_rows = table.rows().peekable();
1✔
553
        while let Some(row) = peekable_rows.next() {
514✔
554
            if let Some(next_row) = peekable_rows.peek() {
513✔
555
                assert!(
512✔
556
                    row.get(1).unwrap().get_quantity().unwrap().value
512✔
557
                        >= next_row.get(1).unwrap().get_quantity().unwrap().value
512✔
558
                );
559
            }
1✔
560
        }
561
    }
1✔
562

563
    #[test]
564
    fn test_sort_by_id() {
1✔
565
        let mut table = setup_table(None);
1✔
566
        extract_headers(&mut table).unwrap();
1✔
567
        let column = 1;
1✔
568
        sort_by_column_id(&mut table, column).unwrap();
1✔
569
        let mut peekable_rows = table.rows().peekable();
1✔
570
        while let Some(row) = peekable_rows.next() {
514✔
571
            if let Some(next_row) = peekable_rows.peek() {
513✔
572
                assert!(
512✔
573
                    row.get(column).unwrap().get_quantity().unwrap().value
512✔
574
                        >= next_row.get(column).unwrap().get_quantity().unwrap().value
512✔
575
                );
576
            }
1✔
577
        }
578
    }
1✔
579

580
    #[test]
581
    fn sorting_by_mixed_column_fails() {
1✔
582
        let column = Column {
1✔
583
            header: Some("Field".to_string()),
1✔
584
            rows: vec![
1✔
585
                Value::from_str("1.0", &None),
1✔
586
                Value::String("String-Value".to_string()),
1✔
587
            ],
1✔
588
        };
1✔
589
        let mut table = Table {
1✔
590
            columns: vec![column],
1✔
591
        };
1✔
592
        let order_by_name = sort_by_column_name(&mut table, "Field");
1✔
593
        assert!(matches!(
1✔
594
            order_by_name.unwrap_err(),
1✔
595
            Error::UnexpectedValue(_, _)
596
        ));
597

598
        let order_by_id = sort_by_column_id(&mut table, 0);
1✔
599
        assert!(matches!(
1✔
600
            order_by_id.unwrap_err(),
1✔
601
            Error::UnexpectedValue(_, _)
602
        ));
603
    }
1✔
604

605
    #[test]
606
    fn non_existing_table_fails() {
1✔
607
        let mut table = setup_table(None);
1✔
608
        let order_by_name = sort_by_column_name(&mut table, "Non-Existing-Field");
1✔
609
        assert!(matches!(
1✔
610
            order_by_name.unwrap_err(),
1✔
611
            Error::InvalidAccess(_)
612
        ));
613

614
        let order_by_id = sort_by_column_id(&mut table, 999);
1✔
615
        assert!(matches!(order_by_id.unwrap_err(), Error::InvalidAccess(_)));
1✔
616
    }
1✔
617

618
    #[test]
619
    fn test_delete_cell_by_numb() {
1✔
620
        let mut table = setup_table(None);
1✔
621
        delete_cell_by_number(&mut table, 1, 2).unwrap();
1✔
622

623
        assert_eq!(
1✔
624
            table
1✔
625
                .columns
1✔
626
                .get(1)
1✔
627
                .unwrap()
1✔
628
                .rows
1✔
629
                .get(2)
1✔
630
                .unwrap()
1✔
631
                .get_string()
1✔
632
                .as_deref()
1✔
633
                .unwrap(),
1✔
634
            "DELETED"
635
        );
636

637
        assert_ne!(
1✔
638
            table
1✔
639
                .columns
1✔
640
                .get(1)
1✔
641
                .unwrap()
1✔
642
                .rows
1✔
643
                .first()
1✔
644
                .unwrap()
1✔
645
                .get_string()
1✔
646
                .as_deref()
1✔
647
                .unwrap(),
1✔
648
            "DELETED"
649
        );
650

651
        assert_eq!(
1✔
652
            table
1✔
653
                .columns
1✔
654
                .first()
1✔
655
                .unwrap()
1✔
656
                .rows
1✔
657
                .get(1)
1✔
658
                .unwrap()
1✔
659
                .get_string(),
1✔
660
            None
661
        );
662
    }
1✔
663

664
    #[test]
665
    fn test_delete_cell_by_name() {
1✔
666
        let mut table = setup_table(None);
1✔
667
        extract_headers(&mut table).unwrap();
1✔
668
        delete_cell_by_column_name_and_row_number(&mut table, "Surface [mm²]", 1).unwrap();
1✔
669

670
        assert_eq!(
1✔
671
            table
1✔
672
                .columns
1✔
673
                .get(1)
1✔
674
                .unwrap()
1✔
675
                .rows
1✔
676
                .get(1)
1✔
677
                .unwrap()
1✔
678
                .get_string()
1✔
679
                .as_deref()
1✔
680
                .unwrap(),
1✔
681
            "DELETED"
682
        );
683

684
        assert_eq!(
1✔
685
            table
1✔
686
                .columns
1✔
687
                .get(1)
1✔
688
                .unwrap()
1✔
689
                .rows
1✔
690
                .get(3)
1✔
691
                .unwrap()
1✔
692
                .get_string(),
1✔
693
            None
694
        );
695

696
        assert_eq!(
1✔
697
            table
1✔
698
                .columns
1✔
699
                .get(0)
1✔
700
                .unwrap()
1✔
701
                .rows
1✔
702
                .get(1)
1✔
703
                .unwrap()
1✔
704
                .get_string(),
1✔
705
            None
706
        );
707
    }
1✔
708

709
    // Tests for PR #58 - Column filtering with exact match and glob patterns
710
    //
711
    // BEHAVIOR DOCUMENTATION & AMBIGUITIES FOUND:
712
    //
713
    // 1. DeleteColumnByName and DeleteColumnByNameG use find() which deletes only the FIRST matching column.
714
    //    - If multiple columns have the same name, only the first is deleted
715
    //    - PR reviewer (TheAdiWijaya) suggested using filter() instead to delete ALL matches
716
    //    - Why not "just fix it"?
717
    //      i.e. changing the behavior DeleteColumnByName* so that it removes ALL columns:
718
    //      - Workaround (feature?) exists: if 2,3,... identical columns "foo" in the CSV are actually
719
    //        intended behavior of the AUT, then the tester can add 2,3,... identical
720
    //        `DeleteColumnByName: "foo"` steps.
721
    //      - debatable behavior (feature or bug) is already released
722
    //      - singular in "DeleteColumnByName" implies deleting only one column
723
    //      - any existing test out there, using "DeleteColumnByName", would in general delete more columns
724
    //        and become less sensitive in a very implicit way when just upgrading havocompare.
725
    //    - Current tests document the FIRST-MATCH-ONLY behavior
726
    //
727
    // 2. DeleteColumnByNameG "exact match priority" is ambiguous:
728
    //    - PR says "first try exact match, then glob pattern"
729
    //    - Current implementation: find() returns first column matching (exact OR glob) by position
730
    //    - Example: pattern "Center *" with columns ["Center x [mm]", "Center y [mm]", "Center *"]
731
    //      deletes "Center x [mm]" (first glob match), NOT "Center *" (exact match at position 3)
732
    //    - Alternative interpretation: search for exact match first, if none found, then search by glob
733
    //    - This needs clarification from the PR author
734
    //
735
    // 3. KeepColumnsByName and KeepColumnsByNameG work correctly:
736
    //    - They iterate ALL columns and keep those matching any pattern (exact or glob)
737
    //    - Multiple columns with same name are all kept
738
    //    - This behavior matches the PR description examples
739

740
    #[test]
741
    fn test_delete_column_by_name_deletes_first_match_only() {
1✔
742
        // According to PR description: DeleteColumnByName deletes the first column that exactly matches
743
        let content = "Center x [mm];Center y [mm];Center z [mm];Center x [mm];Center x\n1;2;3;4;5";
1✔
744
        let mut table = table_from_string(content);
1✔
745
        extract_headers(&mut table).unwrap();
1✔
746

747
        delete_column_name(&mut table, "Center x [mm]").unwrap();
1✔
748

749
        // First "Center x [mm]" should be deleted
750
        assert_eq!(
1✔
751
            table.columns[0].header.as_deref().unwrap(),
1✔
752
            "DELETED",
753
            "First 'Center x [mm]' should be deleted"
754
        );
755
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
756

757
        // Second "Center x [mm]" should still exist
758
        assert_eq!(
1✔
759
            table.columns[3].header.as_deref().unwrap(),
1✔
760
            "Center x [mm]",
761
            "Second 'Center x [mm]' should NOT be deleted (only first match)"
762
        );
763
        assert!(!table.columns[3].rows.iter().all(|v| *v == Value::deleted()));
1✔
764
    }
1✔
765

766
    #[test]
767
    fn test_delete_column_by_name_glob_deletes_first_match() {
1✔
768
        // AMBIGUITY DETECTED: PR says "first try exact match, then glob pattern"
769
        // Current implementation uses find() which returns FIRST column matching (exact OR glob)
770
        // When pattern "Center *" is used, it matches "Center x [mm]" (glob) before "Center *" (exact)
771
        // This test documents CURRENT behavior - may need clarification if prioritization is intended
772
        let content = "Center x [mm];Center y [mm];Center z [mm];Center *\n1;2;3;4";
1✔
773
        let mut table = table_from_string(content);
1✔
774
        extract_headers(&mut table).unwrap();
1✔
775

776
        // Pattern "Center *" as glob matches "Center x [mm]" first
777
        delete_column_name_glob(&mut table, "Center *").unwrap();
1✔
778

779
        // Current behavior: deletes first match by position (glob match on column 0)
780
        assert_eq!(
1✔
781
            table.columns[0].header.as_deref().unwrap(),
1✔
782
            "DELETED",
783
            "First column matching glob 'Center *' should be deleted"
784
        );
785
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
786

787
        // The exact match "Center *" at column 3 is NOT deleted (only first match)
788
        assert_eq!(
1✔
789
            table.columns[3].header.as_deref().unwrap(),
1✔
790
            "Center *",
791
            "Exact match is not deleted because first glob match was found first"
792
        );
793
        assert!(!table.columns[3].rows.iter().all(|v| *v == Value::deleted()));
1✔
794
    }
1✔
795

796
    #[test]
797
    fn test_delete_column_by_name_glob_pattern_match() {
1✔
798
        let content = "Center x [mm];Center y [mm];Center z [mm];Other column\n1;2;3;4";
1✔
799
        let mut table = table_from_string(content);
1✔
800
        extract_headers(&mut table).unwrap();
1✔
801

802
        delete_column_name_glob(&mut table, "Center *").unwrap();
1✔
803

804
        // First match by glob should be deleted
805
        assert_eq!(
1✔
806
            table.columns[0].header.as_deref().unwrap(),
1✔
807
            "DELETED",
808
            "First column matching 'Center *' glob should be deleted"
809
        );
810
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
811

812
        // Other columns that match should NOT be deleted (only first match with find())
813
        assert_eq!(
1✔
814
            table.columns[1].header.as_deref().unwrap(),
1✔
815
            "Center y [mm]",
816
            "Second matching column should NOT be deleted"
817
        );
818
    }
1✔
819

820
    #[test]
821
    fn test_delete_column_by_name_glob_square_brackets() {
1✔
822
        // According to PR: Square brackets in glob are wildcards (match 'u' OR 'm')
823
        // So exact match should be tried first to handle "[mm]" correctly
824
        let content = "Value [mm];Value [um];Value m;Value u\n1;2;3;4";
1✔
825
        let mut table = table_from_string(content);
1✔
826
        extract_headers(&mut table).unwrap();
1✔
827

828
        // Without exact match fallback, "[um]" would match "Value u" or "Value m"
829
        delete_column_name_glob(&mut table, "Value [mm]").unwrap();
1✔
830

831
        // Should delete exact match "Value [mm]"
832
        assert_eq!(
1✔
833
            table.columns[0].header.as_deref().unwrap(),
1✔
834
            "DELETED",
835
            "Exact match 'Value [mm]' should be deleted"
836
        );
837
    }
1✔
838

839
    #[test]
840
    fn test_keep_columns_by_name_example_from_pr() {
1✔
841
        // Example from PR #58
842
        let content = "Center x [mm];Center y [mm];Center z [mm];any other string;Center x [mm];Center x\n1;2;3;4;5;6";
1✔
843
        let mut table = table_from_string(content);
1✔
844
        extract_headers(&mut table).unwrap();
1✔
845

846
        let keep_names = string_vec!["Center x [mm]", "Center y [mm]"];
1✔
847
        keep_columns_matching_any_names(&mut table, &keep_names).unwrap();
1✔
848

849
        // First two columns should be kept (exact matches)
850
        assert_eq!(
1✔
851
            table.columns[0].header.as_deref().unwrap(),
1✔
852
            "Center x [mm]",
853
            "First column should be kept"
854
        );
855
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
856

857
        assert_eq!(
1✔
858
            table.columns[1].header.as_deref().unwrap(),
1✔
859
            "Center y [mm]",
860
            "Second column should be kept"
861
        );
862
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
863

864
        // Third column should be deleted
865
        assert_eq!(
1✔
866
            table.columns[2].header.as_deref().unwrap(),
1✔
867
            "DELETED",
868
            "Center z [mm] should be deleted"
869
        );
870
        assert!(table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
871

872
        // Fourth column should be deleted
873
        assert_eq!(
1✔
874
            table.columns[3].header.as_deref().unwrap(),
1✔
875
            "DELETED",
876
            "any other string should be deleted"
877
        );
878
        assert!(table.columns[3].rows.iter().all(|v| *v == Value::deleted()));
1✔
879

880
        // Fifth column (duplicate "Center x [mm]") should be kept
881
        assert_eq!(
1✔
882
            table.columns[4].header.as_deref().unwrap(),
1✔
883
            "Center x [mm]",
884
            "Second 'Center x [mm]' should also be kept"
885
        );
886
        assert!(!table.columns[4].rows.iter().all(|v| *v == Value::deleted()));
1✔
887

888
        // Sixth column should be deleted
889
        assert_eq!(
1✔
890
            table.columns[5].header.as_deref().unwrap(),
1✔
891
            "DELETED",
892
            "Center x (without [mm]) should be deleted"
893
        );
894
        assert!(table.columns[5].rows.iter().all(|v| *v == Value::deleted()));
1✔
895
    }
1✔
896

897
    #[test]
898
    fn test_keep_columns_by_name_glob_with_wildcard() {
1✔
899
        // Example from PR: "Center *" should match "Center x [mm]", "Center y [mm]", "Center _"
900
        let content = "Center x [mm];Center y [mm];Center _;Other column\n1;2;3;4";
1✔
901
        let mut table = table_from_string(content);
1✔
902
        extract_headers(&mut table).unwrap();
1✔
903

904
        let keep_names = string_vec!["Center *"];
1✔
905
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
906

907
        // All "Center *" columns should be kept
908
        assert_eq!(
1✔
909
            table.columns[0].header.as_deref().unwrap(),
1✔
910
            "Center x [mm]",
911
            "Center x [mm] should match glob"
912
        );
913
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
914

915
        assert_eq!(
1✔
916
            table.columns[1].header.as_deref().unwrap(),
1✔
917
            "Center y [mm]",
918
            "Center y [mm] should match glob"
919
        );
920
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
921

922
        assert_eq!(
1✔
923
            table.columns[2].header.as_deref().unwrap(),
1✔
924
            "Center _",
925
            "Center _ should match glob"
926
        );
927
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
928

929
        // "Other column" should be deleted
930
        assert_eq!(
1✔
931
            table.columns[3].header.as_deref().unwrap(),
1✔
932
            "DELETED",
933
            "Other column should be deleted"
934
        );
935
        assert!(table.columns[3].rows.iter().all(|v| *v == Value::deleted()));
1✔
936
    }
1✔
937

938
    #[test]
939
    fn test_keep_columns_by_name_glob_question_mark_wildcard() {
1✔
940
        // Example from PR: "Center ? [mm]" does NOT match "Center x [mm]"
941
        // because ? matches single char and space is a char
942
        let content = "Center x [mm];Center  [mm];Centerx[mm]\n1;2;3";
1✔
943
        let mut table = table_from_string(content);
1✔
944
        extract_headers(&mut table).unwrap();
1✔
945

946
        let keep_names = string_vec!["Center ? [mm]"];
1✔
947
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
948

949
        // "Center x [mm]" has space+x before [mm], so doesn't match single char '?'
950
        assert_eq!(
1✔
951
            table.columns[0].header.as_deref().unwrap(),
1✔
952
            "DELETED",
953
            "Center x [mm] should NOT match 'Center ? [mm]' pattern"
954
        );
955
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
956

957
        // "Center  [mm]" has space+space before [mm], so doesn't match single char '?'
958
        assert_eq!(
1✔
959
            table.columns[1].header.as_deref().unwrap(),
1✔
960
            "DELETED",
961
            "Center  [mm] should NOT match 'Center ? [mm]' pattern"
962
        );
963

964
        // "Centerx[mm]" has no space, so doesn't match
965
        assert_eq!(
1✔
966
            table.columns[2].header.as_deref().unwrap(),
1✔
967
            "DELETED",
968
            "Centerx[mm] should NOT match 'Center ? [mm]' pattern"
969
        );
970
    }
1✔
971

972
    #[test]
973
    fn test_keep_columns_by_name_glob_exact_match_takes_priority() {
1✔
974
        // Verify that exact match is tried before glob pattern
975
        let content = "Center *;Center x;Center y\n1;2;3";
1✔
976
        let mut table = table_from_string(content);
1✔
977
        extract_headers(&mut table).unwrap();
1✔
978

979
        let keep_names = string_vec!["Center *"];
1✔
980
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
981

982
        // "Center *" should be kept (exact match)
983
        assert_eq!(
1✔
984
            table.columns[0].header.as_deref().unwrap(),
1✔
985
            "Center *",
986
            "Exact match 'Center *' should be kept"
987
        );
988
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
989

990
        // "Center x" should also be kept (glob match)
991
        assert_eq!(
1✔
992
            table.columns[1].header.as_deref().unwrap(),
1✔
993
            "Center x",
994
            "Center x should match glob pattern"
995
        );
996
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
997

998
        // "Center y" should also be kept (glob match)
999
        assert_eq!(
1✔
1000
            table.columns[2].header.as_deref().unwrap(),
1✔
1001
            "Center y",
1002
            "Center y should match glob pattern"
1003
        );
1004
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
1005
    }
1✔
1006

1007
    #[test]
1008
    fn test_keep_columns_by_name_glob_square_brackets_as_wildcard() {
1✔
1009
        // According to PR: [um] matches 'u' OR 'm'
1010
        // But exact match should be tried first
1011
        let content = "Value [mm];Value u;Value m;Value [um]\n1;2;3;4";
1✔
1012
        let mut table = table_from_string(content);
1✔
1013
        extract_headers(&mut table).unwrap();
1✔
1014

1015
        let keep_names = string_vec!["Value [mm]"];
1✔
1016
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
1017

1018
        // "Value [mm]" should be kept (exact match takes priority)
1019
        assert_eq!(
1✔
1020
            table.columns[0].header.as_deref().unwrap(),
1✔
1021
            "Value [mm]",
1022
            "Exact match 'Value [mm]' should be kept"
1023
        );
1024
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
1025

1026
        // "Value u" should also be kept (glob [mm] matches 'm' at that position)
1027
        // Wait - this would only work if the glob was "Value [um]" not "Value [mm]"
1028
        // Let's check if "Value u" matches glob pattern "Value [mm]"
1029
        // [mm] means 'm' OR 'm', so it matches single 'm'
1030
        // "Value u" doesn't have 'm' at position after "Value ", so it shouldn't match
1031
        assert_eq!(
1✔
1032
            table.columns[1].header.as_deref().unwrap(),
1✔
1033
            "DELETED",
1034
            "Value u should NOT match 'Value [mm]' glob"
1035
        );
1036

1037
        // "Value m" should be kept if glob [mm] at that position matches 'm'
1038
        assert_eq!(
1✔
1039
            table.columns[2].header.as_deref().unwrap(),
1✔
1040
            "Value m",
1041
            "Value m should match glob 'Value [mm]' (first 'm' in brackets)"
1042
        );
1043
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
1044
    }
1✔
1045

1046
    #[test]
1047
    fn test_keep_columns_multiple_patterns() {
1✔
1048
        // Test with multiple patterns in the list
1049
        let content = "Column A;Column B;Data X;Data Y;Other\n1;2;3;4;5";
1✔
1050
        let mut table = table_from_string(content);
1✔
1051
        extract_headers(&mut table).unwrap();
1✔
1052

1053
        let keep_names = string_vec!["Column *", "Data X"];
1✔
1054
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
1055

1056
        // "Column A" and "Column B" should match first pattern
1057
        assert_eq!(table.columns[0].header.as_deref().unwrap(), "Column A");
1✔
1058
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
1059

1060
        assert_eq!(table.columns[1].header.as_deref().unwrap(), "Column B");
1✔
1061
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
1062

1063
        // "Data X" should match second pattern (exact)
1064
        assert_eq!(table.columns[2].header.as_deref().unwrap(), "Data X");
1✔
1065
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
1066

1067
        // "Data Y" should be deleted (doesn't match any pattern)
1068
        assert_eq!(table.columns[3].header.as_deref().unwrap(), "DELETED");
1✔
1069
        assert!(table.columns[3].rows.iter().all(|v| *v == Value::deleted()));
1✔
1070

1071
        // "Other" should be deleted
1072
        assert_eq!(table.columns[4].header.as_deref().unwrap(), "DELETED");
1✔
1073
        assert!(table.columns[4].rows.iter().all(|v| *v == Value::deleted()));
1✔
1074
    }
1✔
1075

1076
    #[test]
1077
    fn test_keep_columns_by_name_empty_list() {
1✔
1078
        // Empty list should delete all columns
1079
        let content = "Column A;Column B;Column C\n1;2;3";
1✔
1080
        let mut table = table_from_string(content);
1✔
1081
        extract_headers(&mut table).unwrap();
1✔
1082

1083
        let keep_names: Vec<String> = vec![];
1✔
1084
        keep_columns_matching_any_names(&mut table, &keep_names).unwrap();
1✔
1085

1086
        // All columns should be deleted
1087
        assert!(table.columns.iter().all(|col| {
3✔
1088
            col.header.as_deref().unwrap() == "DELETED"
3✔
1089
                && col.rows.iter().all(|v| *v == Value::deleted())
3✔
1090
        }));
3✔
1091
    }
1✔
1092

1093
    #[test]
1094
    fn test_delete_column_by_name_nonexistent() {
1✔
1095
        // Deleting non-existent column should succeed (no-op)
1096
        let content = "Column A;Column B\n1;2";
1✔
1097
        let mut table = table_from_string(content);
1✔
1098
        extract_headers(&mut table).unwrap();
1✔
1099

1100
        delete_column_name(&mut table, "Nonexistent").unwrap();
1✔
1101

1102
        // Both columns should still exist
1103
        assert_eq!(table.columns[0].header.as_deref().unwrap(), "Column A");
1✔
1104
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
1105
        assert_eq!(table.columns[1].header.as_deref().unwrap(), "Column B");
1✔
1106
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
1107
    }
1✔
1108

1109
    #[test]
1110
    fn test_delete_column_by_name_header_occurs_twice_only_first_column_deleted() {
1✔
1111
        // Deleting non-existent column should succeed (no-op)
1112
        let content = "Column A;Column B;Column A\n1;2;3";
1✔
1113
        let mut table = table_from_string(content);
1✔
1114
        extract_headers(&mut table).unwrap();
1✔
1115

1116
        delete_column_name(&mut table, "Column A").unwrap();
1✔
1117

1118
        // Only first column should be deleted
1119
        assert_eq!(table.columns[0].header.as_deref().unwrap(), "DELETED");
1✔
1120
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
1121
        assert_eq!(table.columns[1].header.as_deref().unwrap(), "Column B");
1✔
1122
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
1123
        assert_eq!(table.columns[2].header.as_deref().unwrap(), "Column A");
1✔
1124
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
1125

1126
        delete_column_name(&mut table, "Column A").unwrap();
1✔
1127
        // Now the 2nd "Column A" should be deleted as well.
1128
        assert_eq!(table.columns[0].header.as_deref().unwrap(), "DELETED");
1✔
1129
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
1130
        assert_eq!(table.columns[1].header.as_deref().unwrap(), "Column B");
1✔
1131
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
1132
        assert_eq!(table.columns[2].header.as_deref().unwrap(), "DELETED");
1✔
1133
        assert!(table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
1134
    }
1✔
1135

1136
    #[test]
1137
    fn test_delete_column_by_name_glob_invalid_pattern() {
1✔
1138
        // Invalid glob pattern should return error
1139
        let content = "Column A;Column B\n1;2";
1✔
1140
        let mut table = table_from_string(content);
1✔
1141
        extract_headers(&mut table).unwrap();
1✔
1142

1143
        let result = delete_column_name_glob(&mut table, "[invalid");
1✔
1144
        assert!(result.is_err());
1✔
1145
        assert!(matches!(result.unwrap_err(), Error::InvalidAccess(_)));
1✔
1146
    }
1✔
1147

1148
    #[test]
1149
    fn test_keep_columns_by_name_glob_invalid_pattern() {
1✔
1150
        // Invalid glob pattern should return error
1151
        let content = "Column A;Column B\n1;2";
1✔
1152
        let mut table = table_from_string(content);
1✔
1153
        extract_headers(&mut table).unwrap();
1✔
1154

1155
        let keep_names = string_vec!["[invalid"];
1✔
1156
        let result = keep_columns_matching_any_names_glob(&mut table, &keep_names);
1✔
1157
        assert!(result.is_err());
1✔
1158
        assert!(matches!(result.unwrap_err(), Error::InvalidAccess(_)));
1✔
1159
    }
1✔
1160
}
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