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

VolumeGraphics / havocompare / 21915916823

11 Feb 2026 05:33PM UTC coverage: 83.87% (+1.7%) from 82.168%
21915916823

Pull #58

github

BBertram-hex
57: fix DeleteColumnByNameG case

- two columns matching the glob should be removed
- but the DELETED marker matches the glob pattern.
Pull Request #58: 57 new preprocessor step for whitlisting of CSV columns

359 of 364 new or added lines in 1 file covered. (98.63%)

17 existing lines in 1 file now uncovered.

3099 of 3695 relevant lines covered (83.87%)

2571.16 hits per line

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

96.41
/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> {
7✔
205
    let pattern = Pattern::new(name).map_err(|e| {
7✔
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| {
8✔
213
        let header = col.header.as_deref().unwrap_or_default();
8✔
214
        header != "DELETED" && (header == name || pattern.matches(header))
8✔
215
    }) {
8✔
216
        c.delete_contents();
6✔
217
    }
6✔
218
    Ok(())
6✔
219
}
7✔
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> {
174✔
277
    debug!("Extracting headers...");
174✔
278
    let can_extract = table
174✔
279
        .columns
174✔
280
        .iter()
174✔
281
        .all(|c| matches!(c.rows.first(), Some(Value::String(_))));
459✔
282
    if !can_extract {
174✔
283
        warn!("Cannot extract header for this csv!");
1✔
284
        return Ok(());
1✔
285
    }
173✔
286

287
    for col in table.columns.iter_mut() {
458✔
288
        let title = col.rows.drain(0..1).next().ok_or_else(|| {
458✔
289
            csv::Error::InvalidAccess("Tried to extract header of empty column!".to_string())
×
290
        })?;
×
291
        if let Value::String(title) = title {
458✔
292
            col.header = Some(title);
458✔
293
        }
458✔
294
    }
295
    Ok(())
173✔
296
}
174✔
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 {
18✔
319
        let cursor = Cursor::new(content.as_bytes());
18✔
320

321
        Table::from_reader(
18✔
322
            cursor,
18✔
323
            &Delimiters {
18✔
324
                field_delimiter: Some(';'),
18✔
325
                decimal_separator: Some('.'),
18✔
326
            },
18✔
327
        )
328
        .unwrap()
18✔
329
    }
18✔
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_twice_does_not_rematch_deleted() {
1✔
798
        // Scenario: columns ["Data A", "Data B", "Other"], call DeleteColumnByNameG("D*") twice.
799
        // Step 1 should delete "Data A", step 2 should delete "Data B".
800
        // Bug: "D*" also matches "DELETED", so step 2 re-matches the already-deleted column.
801
        let content = "Data A;Data B;Other\n1;2;3";
1✔
802
        let mut table = table_from_string(content);
1✔
803
        extract_headers(&mut table).unwrap();
1✔
804

805
        // First call: should delete "Data A" (first match left-to-right)
806
        delete_column_name_glob(&mut table, "D*").unwrap();
1✔
807
        assert_eq!(
1✔
808
            table.columns[0].header.as_deref().unwrap(),
1✔
809
            "DELETED",
810
            "First call should delete 'Data A'"
811
        );
812
        assert_eq!(
1✔
813
            table.columns[1].header.as_deref().unwrap(),
1✔
814
            "Data B",
815
            "'Data B' should survive the first call"
816
        );
817

818
        // Second call: should delete "Data B" (next match), NOT re-match "DELETED"
819
        delete_column_name_glob(&mut table, "D*").unwrap();
1✔
820
        assert_eq!(
1✔
821
            table.columns[1].header.as_deref().unwrap(),
1✔
822
            "DELETED",
823
            "Second call should delete 'Data B', not re-match the already-deleted column"
824
        );
825

826
        // "Other" should be untouched
827
        assert_eq!(
1✔
828
            table.columns[2].header.as_deref().unwrap(),
1✔
829
            "Other",
830
            "'Other' should not be affected"
831
        );
832
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
833
    }
1✔
834

835
    #[test]
836
    fn test_delete_column_by_name_glob_pattern_match() {
1✔
837
        let content = "Center x [mm];Center y [mm];Center z [mm];Other column\n1;2;3;4";
1✔
838
        let mut table = table_from_string(content);
1✔
839
        extract_headers(&mut table).unwrap();
1✔
840

841
        delete_column_name_glob(&mut table, "Center *").unwrap();
1✔
842

843
        // First match by glob should be deleted
844
        assert_eq!(
1✔
845
            table.columns[0].header.as_deref().unwrap(),
1✔
846
            "DELETED",
847
            "First column matching 'Center *' glob should be deleted"
848
        );
849
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
850

851
        // Other columns that match should NOT be deleted (only first match with find())
852
        assert_eq!(
1✔
853
            table.columns[1].header.as_deref().unwrap(),
1✔
854
            "Center y [mm]",
855
            "Second matching column should NOT be deleted"
856
        );
857
    }
1✔
858

859
    #[test]
860
    fn test_delete_column_by_name_glob_square_brackets() {
1✔
861
        // According to PR: Square brackets in glob are wildcards (match 'u' OR 'm')
862
        // So exact match should be tried first to handle "[mm]" correctly
863
        let content = "Value [mm];Value [um];Value m;Value u\n1;2;3;4";
1✔
864
        let mut table = table_from_string(content);
1✔
865
        extract_headers(&mut table).unwrap();
1✔
866

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

870
        // Should delete exact match "Value [mm]"
871
        assert_eq!(
1✔
872
            table.columns[0].header.as_deref().unwrap(),
1✔
873
            "DELETED",
874
            "Exact match 'Value [mm]' should be deleted"
875
        );
876
    }
1✔
877

878
    #[test]
879
    fn test_keep_columns_by_name_example_from_pr() {
1✔
880
        // Example from PR #58
881
        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✔
882
        let mut table = table_from_string(content);
1✔
883
        extract_headers(&mut table).unwrap();
1✔
884

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

888
        // First two columns should be kept (exact matches)
889
        assert_eq!(
1✔
890
            table.columns[0].header.as_deref().unwrap(),
1✔
891
            "Center x [mm]",
892
            "First column should be kept"
893
        );
894
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
895

896
        assert_eq!(
1✔
897
            table.columns[1].header.as_deref().unwrap(),
1✔
898
            "Center y [mm]",
899
            "Second column should be kept"
900
        );
901
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
902

903
        // Third column should be deleted
904
        assert_eq!(
1✔
905
            table.columns[2].header.as_deref().unwrap(),
1✔
906
            "DELETED",
907
            "Center z [mm] should be deleted"
908
        );
909
        assert!(table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
910

911
        // Fourth column should be deleted
912
        assert_eq!(
1✔
913
            table.columns[3].header.as_deref().unwrap(),
1✔
914
            "DELETED",
915
            "any other string should be deleted"
916
        );
917
        assert!(table.columns[3].rows.iter().all(|v| *v == Value::deleted()));
1✔
918

919
        // Fifth column (duplicate "Center x [mm]") should be kept
920
        assert_eq!(
1✔
921
            table.columns[4].header.as_deref().unwrap(),
1✔
922
            "Center x [mm]",
923
            "Second 'Center x [mm]' should also be kept"
924
        );
925
        assert!(!table.columns[4].rows.iter().all(|v| *v == Value::deleted()));
1✔
926

927
        // Sixth column should be deleted
928
        assert_eq!(
1✔
929
            table.columns[5].header.as_deref().unwrap(),
1✔
930
            "DELETED",
931
            "Center x (without [mm]) should be deleted"
932
        );
933
        assert!(table.columns[5].rows.iter().all(|v| *v == Value::deleted()));
1✔
934
    }
1✔
935

936
    #[test]
937
    fn test_keep_columns_by_name_glob_with_wildcard() {
1✔
938
        // Example from PR: "Center *" should match "Center x [mm]", "Center y [mm]", "Center _"
939
        let content = "Center x [mm];Center y [mm];Center _;Other column\n1;2;3;4";
1✔
940
        let mut table = table_from_string(content);
1✔
941
        extract_headers(&mut table).unwrap();
1✔
942

943
        let keep_names = string_vec!["Center *"];
1✔
944
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
945

946
        // All "Center *" columns should be kept
947
        assert_eq!(
1✔
948
            table.columns[0].header.as_deref().unwrap(),
1✔
949
            "Center x [mm]",
950
            "Center x [mm] should match glob"
951
        );
952
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
953

954
        assert_eq!(
1✔
955
            table.columns[1].header.as_deref().unwrap(),
1✔
956
            "Center y [mm]",
957
            "Center y [mm] should match glob"
958
        );
959
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
960

961
        assert_eq!(
1✔
962
            table.columns[2].header.as_deref().unwrap(),
1✔
963
            "Center _",
964
            "Center _ should match glob"
965
        );
966
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
967

968
        // "Other column" should be deleted
969
        assert_eq!(
1✔
970
            table.columns[3].header.as_deref().unwrap(),
1✔
971
            "DELETED",
972
            "Other column should be deleted"
973
        );
974
        assert!(table.columns[3].rows.iter().all(|v| *v == Value::deleted()));
1✔
975
    }
1✔
976

977
    #[test]
978
    fn test_keep_columns_by_name_glob_question_mark_wildcard() {
1✔
979
        // Example from PR: "Center ? [mm]" does NOT match "Center x [mm]"
980
        // because ? matches single char and space is a char
981
        let content = "Center x [mm];Center  [mm];Centerx[mm]\n1;2;3";
1✔
982
        let mut table = table_from_string(content);
1✔
983
        extract_headers(&mut table).unwrap();
1✔
984

985
        let keep_names = string_vec!["Center ? [mm]"];
1✔
986
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
987

988
        // "Center x [mm]" has space+x before [mm], so doesn't match single char '?'
989
        assert_eq!(
1✔
990
            table.columns[0].header.as_deref().unwrap(),
1✔
991
            "DELETED",
992
            "Center x [mm] should NOT match 'Center ? [mm]' pattern"
993
        );
994
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
995

996
        // "Center  [mm]" has space+space before [mm], so doesn't match single char '?'
997
        assert_eq!(
1✔
998
            table.columns[1].header.as_deref().unwrap(),
1✔
999
            "DELETED",
1000
            "Center  [mm] should NOT match 'Center ? [mm]' pattern"
1001
        );
1002

1003
        // "Centerx[mm]" has no space, so doesn't match
1004
        assert_eq!(
1✔
1005
            table.columns[2].header.as_deref().unwrap(),
1✔
1006
            "DELETED",
1007
            "Centerx[mm] should NOT match 'Center ? [mm]' pattern"
1008
        );
1009
    }
1✔
1010

1011
    #[test]
1012
    fn test_keep_columns_by_name_glob_exact_match_takes_priority() {
1✔
1013
        // Verify that exact match is tried before glob pattern
1014
        let content = "Center *;Center x;Center y\n1;2;3";
1✔
1015
        let mut table = table_from_string(content);
1✔
1016
        extract_headers(&mut table).unwrap();
1✔
1017

1018
        let keep_names = string_vec!["Center *"];
1✔
1019
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
1020

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

1029
        // "Center x" should also be kept (glob match)
1030
        assert_eq!(
1✔
1031
            table.columns[1].header.as_deref().unwrap(),
1✔
1032
            "Center x",
1033
            "Center x should match glob pattern"
1034
        );
1035
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
1036

1037
        // "Center y" should also be kept (glob match)
1038
        assert_eq!(
1✔
1039
            table.columns[2].header.as_deref().unwrap(),
1✔
1040
            "Center y",
1041
            "Center y should match glob pattern"
1042
        );
1043
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
1044
    }
1✔
1045

1046
    #[test]
1047
    fn test_keep_columns_by_name_glob_square_brackets_as_wildcard() {
1✔
1048
        // According to PR: [um] matches 'u' OR 'm'
1049
        // But exact match should be tried first
1050
        let content = "Value [mm];Value u;Value m;Value [um]\n1;2;3;4";
1✔
1051
        let mut table = table_from_string(content);
1✔
1052
        extract_headers(&mut table).unwrap();
1✔
1053

1054
        let keep_names = string_vec!["Value [mm]"];
1✔
1055
        keep_columns_matching_any_names_glob(&mut table, &keep_names).unwrap();
1✔
1056

1057
        // "Value [mm]" should be kept (exact match takes priority)
1058
        assert_eq!(
1✔
1059
            table.columns[0].header.as_deref().unwrap(),
1✔
1060
            "Value [mm]",
1061
            "Exact match 'Value [mm]' should be kept"
1062
        );
1063
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
1064

1065
        // "Value u" should also be kept (glob [mm] matches 'm' at that position)
1066
        // Wait - this would only work if the glob was "Value [um]" not "Value [mm]"
1067
        // Let's check if "Value u" matches glob pattern "Value [mm]"
1068
        // [mm] means 'm' OR 'm', so it matches single 'm'
1069
        // "Value u" doesn't have 'm' at position after "Value ", so it shouldn't match
1070
        assert_eq!(
1✔
1071
            table.columns[1].header.as_deref().unwrap(),
1✔
1072
            "DELETED",
1073
            "Value u should NOT match 'Value [mm]' glob"
1074
        );
1075

1076
        // "Value m" should be kept if glob [mm] at that position matches 'm'
1077
        assert_eq!(
1✔
1078
            table.columns[2].header.as_deref().unwrap(),
1✔
1079
            "Value m",
1080
            "Value m should match glob 'Value [mm]' (first 'm' in brackets)"
1081
        );
1082
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
1083
    }
1✔
1084

1085
    #[test]
1086
    fn test_keep_columns_multiple_patterns() {
1✔
1087
        // Test with multiple patterns in the list
1088
        let content = "Column A;Column B;Data X;Data Y;Other\n1;2;3;4;5";
1✔
1089
        let mut table = table_from_string(content);
1✔
1090
        extract_headers(&mut table).unwrap();
1✔
1091

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

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

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

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

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

1110
        // "Other" should be deleted
1111
        assert_eq!(table.columns[4].header.as_deref().unwrap(), "DELETED");
1✔
1112
        assert!(table.columns[4].rows.iter().all(|v| *v == Value::deleted()));
1✔
1113
    }
1✔
1114

1115
    #[test]
1116
    fn test_keep_columns_by_name_empty_list() {
1✔
1117
        // Empty list should delete all columns
1118
        let content = "Column A;Column B;Column C\n1;2;3";
1✔
1119
        let mut table = table_from_string(content);
1✔
1120
        extract_headers(&mut table).unwrap();
1✔
1121

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

1125
        // All columns should be deleted
1126
        assert!(table.columns.iter().all(|col| {
3✔
1127
            col.header.as_deref().unwrap() == "DELETED"
3✔
1128
                && col.rows.iter().all(|v| *v == Value::deleted())
3✔
1129
        }));
3✔
1130
    }
1✔
1131

1132
    #[test]
1133
    fn test_delete_column_by_name_nonexistent() {
1✔
1134
        // Deleting non-existent column should succeed (no-op)
1135
        let content = "Column A;Column B\n1;2";
1✔
1136
        let mut table = table_from_string(content);
1✔
1137
        extract_headers(&mut table).unwrap();
1✔
1138

1139
        delete_column_name(&mut table, "Nonexistent").unwrap();
1✔
1140

1141
        // Both columns should still exist
1142
        assert_eq!(table.columns[0].header.as_deref().unwrap(), "Column A");
1✔
1143
        assert!(!table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
1144
        assert_eq!(table.columns[1].header.as_deref().unwrap(), "Column B");
1✔
1145
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
1146
    }
1✔
1147

1148
    #[test]
1149
    fn test_delete_column_by_name_header_occurs_twice_only_first_column_deleted() {
1✔
1150
        // Deleting non-existent column should succeed (no-op)
1151
        let content = "Column A;Column B;Column A\n1;2;3";
1✔
1152
        let mut table = table_from_string(content);
1✔
1153
        extract_headers(&mut table).unwrap();
1✔
1154

1155
        delete_column_name(&mut table, "Column A").unwrap();
1✔
1156

1157
        // Only first column should be deleted
1158
        assert_eq!(table.columns[0].header.as_deref().unwrap(), "DELETED");
1✔
1159
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
1160
        assert_eq!(table.columns[1].header.as_deref().unwrap(), "Column B");
1✔
1161
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
1162
        assert_eq!(table.columns[2].header.as_deref().unwrap(), "Column A");
1✔
1163
        assert!(!table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
1164

1165
        delete_column_name(&mut table, "Column A").unwrap();
1✔
1166
        // Now the 2nd "Column A" should be deleted as well.
1167
        assert_eq!(table.columns[0].header.as_deref().unwrap(), "DELETED");
1✔
1168
        assert!(table.columns[0].rows.iter().all(|v| *v == Value::deleted()));
1✔
1169
        assert_eq!(table.columns[1].header.as_deref().unwrap(), "Column B");
1✔
1170
        assert!(!table.columns[1].rows.iter().all(|v| *v == Value::deleted()));
1✔
1171
        assert_eq!(table.columns[2].header.as_deref().unwrap(), "DELETED");
1✔
1172
        assert!(table.columns[2].rows.iter().all(|v| *v == Value::deleted()));
1✔
1173
    }
1✔
1174

1175
    #[test]
1176
    fn test_delete_column_by_name_glob_invalid_pattern() {
1✔
1177
        // Invalid glob pattern should return error
1178
        let content = "Column A;Column B\n1;2";
1✔
1179
        let mut table = table_from_string(content);
1✔
1180
        extract_headers(&mut table).unwrap();
1✔
1181

1182
        let result = delete_column_name_glob(&mut table, "[invalid");
1✔
1183
        assert!(result.is_err());
1✔
1184
        assert!(matches!(result.unwrap_err(), Error::InvalidAccess(_)));
1✔
1185
    }
1✔
1186

1187
    #[test]
1188
    fn test_keep_columns_by_name_glob_invalid_pattern() {
1✔
1189
        // Invalid glob pattern should return error
1190
        let content = "Column A;Column B\n1;2";
1✔
1191
        let mut table = table_from_string(content);
1✔
1192
        extract_headers(&mut table).unwrap();
1✔
1193

1194
        let keep_names = string_vec!["[invalid"];
1✔
1195
        let result = keep_columns_matching_any_names_glob(&mut table, &keep_names);
1✔
1196
        assert!(result.is_err());
1✔
1197
        assert!(matches!(result.unwrap_err(), Error::InvalidAccess(_)));
1✔
1198
    }
1✔
1199
}
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