Repository: aeye-lab/pymovements
Pull request #887 (2024-10-25 10:49:02)

feat: add support for multiple recording specs per file to gaze.from_asc()
## Description

Record all the tracked eye sides in the metadata to fix the issue  #875

## Implemented changes

Insert a description of the changes implemented in the pull request.
I modified the code in parsing.py to record every time it can be matched in the message line of the asci files

- Added Regex for Tracking Eye Information in the asci file
- added a condition to match the pattern multiple times and add it to a list
- created an additional  pre_processed_metadata key : tracked_eyes, where the list is stored

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change is or requires a documentation update

## How Has This Been Tested?
 I ran it on the to ascii converted ch1hr007.edf of the pilot-hr-1-zh data to verify the output, but no further testing has been done so far

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
- [x] I have checked my code and corrected any misspellings


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/aeye-lab/pymovements/pull/887?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.58%. Comparing base [(`2d48f2f`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/2d48f2f619557b0af797acedbfd3f475980815a9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) to head [(`a3f70c1`)](https://app.codecov.io/gh/aeye-lab/pymovements/commit/a3f70c19fb51526030193041e963210da166afaf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##              main     #887      +/-   ##
===========================================
- Coverage   100.00%   99.58%   -0.42%     
===========================================
  Files           74       74              
  Lines         3372     3377       +5     
  Branches       594      595       +1     
===========================================
- Hits          3372     3363       -9     
- Misses           0       11      +11     
- Partials         0        3       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aeye-lab/pymovements/pull/887?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).

dkrako: Hi @saphjra I just noticed that you're probably waiting on some further feedback on this PR right?
I won't be able to review this week but I try to have a go at it next week so we can finish this up in the next two weeks. Thank you for your patience.
saeub: @dkrako I resolved the merge conflicts and am now continuing this (with @saphjra's consent :slightly_smiling_face:)

Some updates:
- My recent changes also fix #944.
- I tried to remove unnecessary metadata (like dates) from parsing unit tests and instead used `@pytest.mark.filterwarnings` to ignore the warnings about missing metadata.
- You mentioned at some point that the tracked eye could be added as a column in the gaze dataframe, just like the distance column. However, it looks like there is already a way of specifying the tracked eye using [nested (multi-component) columns](https://github.com/aeye-lab/pymovements/blob/09b1ee6184e346dda52be485dd4922277bab1d58/src/pymovements/gaze/gaze_dataframe.py#L1320), so it seems unnecessary to also add a separate column for the tracked eye. Or am I missing something?
saeub: > Just so that we don't forget, can you create an issue with your ideas before merging?

I created #1072. I agree that #453 would also make this much more usable.

Just out of curiosity, I was wondering if you also considered the following option when implementing the multi-components feature: Instead of allowing multiple components in the same column, adding a column `eye` in `GazeDataFrame`; in the binocular case, we would have two rows for each timestamp, one for the left and one for the right eye, like this:

| time | pixel | eye |
| --- | --- | --- |
| 1 | [123, 456] | L |
| 1 | [123, 456] | R |
| 2 | [123, 456] | L |
| 3 | [123, 456] | R |

This would make filtering for a specific eye or applying event detection to a specific eye very easy. (Although it might make it difficult to notice whether the tracked eye changed between trials.)
dkrako: > > Just so that we don't forget, can you create an issue with your ideas before merging?
> 
> I created #1072. I agree that #453 would also make this much more usable.
> 
> Just out of curiosity, I was wondering if you also considered the following option when implementing the multi-components feature: Instead of allowing multiple components in the same column, adding a column `eye` in `GazeDataFrame`; in the binocular case, we would have two rows for each timestamp, one for the left and one for the right eye, like this:
> time 	pixel 	eye
> 1 	[123, 456] 	L
> 1 	[123, 456] 	R
> 2 	[123, 456] 	L
> 3 	[123, 456] 	R
> 
> This would make filtering for a specific eye or applying event detection to a specific eye very easy. (Although it might make it difficult to notice whether the tracked eye changed between trials.)

A dedicated column that encodes the eye was my original thought for this PR as it's the easiest to implement.
I'm ok with this PR not fully integrating changes of recording specs by writing them into columns. That can be implemented later on.

But generally, instead of having a dedicated column, it would be much nicer to have an expression. A column would need to be checked or recalculated to maintain consistency. An expression that returns the tracked eye from a specific column would be dynamic and computed on-demand. // see https://github.com/aeye-lab/pymovements/issues/453#issuecomment-2787339168



Patch:
diff --git a/src/pymovements/gaze/_utils/parsing.py b/src/pymovements/gaze/_utils/parsing.py
index 45f786b..8153446 100755
--- a/src/pymovements/gaze/_utils/parsing.py
+++ b/src/pymovements/gaze/_utils/parsing.py
@@ -23,6 +23,7 @@ from __future__ import annotations
 import calendar
 import datetime
 import re
+import warnings
 from collections import defaultdict
 from pathlib import Path
 from typing import Any
@@ -48,13 +49,6 @@ EYELINK_META_REGEXES = [
         ),
         r'\*\*\s+(?P<version_2>EYELINK.*)',
         r'MSG\s+\d+[.]?\d*\s+DISPLAY_COORDS\s*=?\s*(?P<resolution>.*)',
-        (
-            r'MSG\s+\d+[.]?\d*\s+RECCFG\s+(?P<tracking_mode>[A-Z,a-z]+)\s+'
-            r'(?P<sampling_rate>\d+)\s+'
-            r'(?P<file_sample_filter>(0|1|2))\s+'
-            r'(?P<link_sample_filter>(0|1|2))\s+'
-            r'(?P<tracked_eye>(L|R|LR))\s*'
-        ),
         r'PUPIL\s+(?P<pupil_data_type>(AREA|DIAMETER))\s*',
         r'MSG\s+\d+[.]?\d*\s+ELCLCFG\s+(?P<mount_configuration>.*)',
     )
@@ -93,6 +87,14 @@ STOP_RECORDING_REGEX = re.compile(
     r'END\s+(?P<timestamp>(\d+[.]?\d*))\s+\s+(?P<types>.*)\s+RES\s+'
     r'(?P<xres>[\d\.]*)\s+(?P<yres>[\d\.]*)\s*',
 )
+RECORDING_CONFIG = re.compile(
+    r'MSG\s+(?P<timestamp>\d+[.]?\d*)\s+'
+    r'RECCFG\s+(?P<tracking_mode>[A-Z,a-z]+)\s+'
+    r'(?P<sampling_rate>\d+)\s+'
+    r'(?P<file_sample_filter>0|1|2)\s+'
+    r'(?P<link_sample_filter>0|1|2)\s+'
+    r'(?P<tracked_eye>LR|[LR])\s*',
+)
 
 
 def check_nan(sample_location: str) -> float:
@@ -250,7 +252,7 @@ def parse_eyelink(
     calibrations = []
     blinks = []
     invalid_samples = []
-
+    recording_config = []
     blink = False
 
     start_recording_timestamp = ''
@@ -258,7 +260,6 @@ def parse_eyelink(
     num_blink_samples = 0
 
     for line in lines:
-
         for pattern_dict in compiled_patterns:
 
             if match := pattern_dict['pattern'].match(line):
@@ -299,6 +300,9 @@ def parse_eyelink(
             num_blink_samples = 0
             blinks.append(blink_info)
 
+        elif eye_side_match := RECORDING_CONFIG.match(line):
+            recording_config.append(eye_side_match.groupdict())
+
         elif match := START_RECORDING_REGEX.match(line):
             start_recording_timestamp = match.groupdict()['timestamp']
 
@@ -353,10 +357,12 @@ def parse_eyelink(
                     compiled_metadata_patterns.remove(pattern_dict)
 
     if not metadata:
-        raise Warning('No metadata found. Please check the file for errors.')
+        warnings.warn('No metadata found. Please check the file for errors.')
 
     # if the sampling rate is not found, we cannot calculate the data loss
     actual_number_of_samples = len(samples['time'])
+    # if we don't have any recording config, we cannot calculate the data loss
+    metadata['sampling_rate'] = _check_reccfg_key(recording_config, 'sampling_rate', float)
 
     data_loss_ratio, data_loss_ratio_blinks = _calculate_data_loss(
         blinks=blinks,
@@ -366,6 +372,8 @@ def parse_eyelink(
         sampling_rate=metadata['sampling_rate'],
     )
 
+    metadata['tracked_eye'] = _check_reccfg_key(recording_config, 'tracked_eye')
+
     pre_processed_metadata: dict[str, Any] = _pre_process_metadata(metadata)
     # is not yet pre-processed but should be
     pre_processed_metadata['calibrations'] = calibrations
@@ -374,6 +382,7 @@ def parse_eyelink(
     pre_processed_metadata['data_loss_ratio'] = data_loss_ratio
     pre_processed_metadata['data_loss_ratio_blinks'] = data_loss_ratio_blinks
     pre_processed_metadata['total_recording_duration_ms'] = total_recording_duration
+    pre_processed_metadata['recording_config'] = recording_config
 
     schema_overrides = {
         'time': pl.Float64,
@@ -412,11 +421,6 @@ def _pre_process_metadata(metadata: defaultdict[str, Any]) -> dict[str, Any]:
         resolution = (coordinates[2] - coordinates[0] + 1, coordinates[3] - coordinates[1] + 1)
         metadata['resolution'] = resolution
 
-    if metadata['sampling_rate']:
-        metadata['sampling_rate'] = float(metadata['sampling_rate'])
-    else:
-        metadata['sampling_rate'] = 'unknown'
-
     # if the date has been parsed fully, convert the date to a datetime object
     if 'day' in metadata and 'year' in metadata and 'month' in metadata and 'time' in metadata:
         metadata['day'] = int(metadata['day'])
@@ -436,12 +440,51 @@ def _pre_process_metadata(metadata: defaultdict[str, Any]) -> dict[str, Any]:
     return return_metadata
 
 
+def _check_reccfg_key(
+        recording_config: list[dict[str, Any]],
+        key: str,
+        astype: type | None = None,
+) -> Any:
+    """Check if the recording configs contain consistent values for the specified key and return it.
+
+    Prints a warning if no recording config is found or if the value is inconsistent across entries.
+
+    Parameters
+    ----------
+    recording_config: list[dict[str, Any]]
+        List of dictionaries containing recording config details.
+    key: str
+        The key in the recording configs to check for consistency.
+    astype: type | None
+        The type to cast the value to.
+
+    Returns
+    -------
+    Any
+        The value of the specified key if available, otherwise None.
+    """
+    if not recording_config:
+        warnings.warn('No recording configuration found.')
+        return None
+
+    values = {d.get(key) for d in recording_config}
+    if len(values) != 1:
+        sorted_values: list = sorted(values)
+        warnings.warn(f"Found inconsistent values for '{key}': {sorted_values}")
+        return None
+
+    value = values.pop()
+    if astype is not None:
+        value = astype(value)
+    return value
+
+
 def _calculate_data_loss(
         blinks: list[dict[str, Any]],
         invalid_samples: list[str],
         actual_num_samples: int,
-        total_rec_duration: float,
-        sampling_rate: float,
+        total_rec_duration: float | None = None,
+        sampling_rate: float | None = None,
 ) -> tuple[float | str, float | str]:
     """Calculate data loss and blink loss.
 
@@ -453,9 +496,9 @@ def _calculate_data_loss(
         List of invalid samples.
     actual_num_samples: int
         Number of actual samples recorded.
-    total_rec_duration: float
+    total_rec_duration: float | None
         Total duration of the recording.
-    sampling_rate: float
+    sampling_rate: float | None
         Sampling rate of the eye tracker.
 
     Returns
diff --git a/src/pymovements/gaze/io.py b/src/pymovements/gaze/io.py
index 107eb68..8138fc0 100644
--- a/src/pymovements/gaze/io.py
+++ b/src/pymovements/gaze/io.py
@@ -394,8 +394,8 @@ def from_asc(
         )
 
     # Tracked eye
-    asc_left_eye = 'L' in metadata['tracked_eye']
-    asc_right_eye = 'R' in metadata['tracked_eye']
+    asc_left_eye = 'L' in (metadata['tracked_eye'] or '')
+    asc_right_eye = 'R' in (metadata['tracked_eye'] or '')
     if experiment.eyetracker.left is None:
         experiment.eyetracker.left = asc_left_eye
     elif experiment.eyetracker.left != asc_left_eye:


Test patch:
diff --git a/tests/unit/gaze/_utils/_parsing_test.py b/tests/unit/gaze/_utils/_parsing_test.py
index 675f899..892c114 100644
--- a/tests/unit/gaze/_utils/_parsing_test.py
+++ b/tests/unit/gaze/_utils/_parsing_test.py
@@ -160,18 +160,15 @@ EXPECTED_METADATA = {
     'version_2': 'EYELINK II CL v6.12 Feb  1 2018 (EyeLink Portable Duo)',
     'model': 'EyeLink Portable Duo',
     'version_number': '6.12',
-    'sampling_rate': 1000.00,
-    'file_sample_filter': '2',
-    'link_sample_filter': '1',
-    'pupil_data_type': 'AREA',
-    'tracking_mode': 'CR',
+    'sampling_rate': 1000.0,
     'tracked_eye': 'L',
+    'pupil_data_type': 'AREA',
     'calibrations': [],
     'validations': [],
     'resolution': (1280, 1024),
     'data_loss_ratio_blinks': 0.18181818181818182,
     'data_loss_ratio': 0.2727272727272727,
-    'total_recording_duration_ms': 11,
+    'total_recording_duration_ms': 11.0,
     'datetime': datetime.datetime(2023, 3, 8, 9, 25, 20),
     'blinks': [{
         'duration_ms': 2,
@@ -189,6 +186,16 @@ EXPECTED_METADATA = {
     'metadata_2': 'abc',
     'metadata_3': True,
     'metadata_4': None,
+    'recording_config': [
+        {
+            'sampling_rate': '1000',
+            'file_sample_filter': '2',
+            'link_sample_filter': '1',
+            'timestamp': '2154555',
+            'tracked_eye': 'L',
+            'tracking_mode': 'CR',
+        },
+    ],
 }
 
 
@@ -270,7 +277,6 @@ def test_parse_eyelink_raises_value_error(tmp_path, patterns):
     ('metadata', 'expected_version', 'expected_model'),
     [
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             '** VERSION: EYELINK II 1\n'
             '** EYELINK II CL v6.12 Feb  1 2018 (EyeLink Portable Duo)',
             '6.12',
@@ -278,7 +284,6 @@ def test_parse_eyelink_raises_value_error(tmp_path, patterns):
             id='eye_link_portable_duo',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             '** VERSION: EYELINK II 1\n'
             '** EYELINK II CL v5.12 Feb  1 2018\n',
             '5.12',
@@ -286,7 +291,6 @@ def test_parse_eyelink_raises_value_error(tmp_path, patterns):
             id='eye_link_1000_plus',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             '** VERSION: EYELINK II 1\n'
             '** EYELINK II CL v4.12 Feb  1 2018',
             '4.12',
@@ -294,7 +298,6 @@ def test_parse_eyelink_raises_value_error(tmp_path, patterns):
             id='eye_link_1000_1',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             '** VERSION: EYELINK II 1\n'
             '** EYELINK II CL v3.12 Feb  1 2018',
             '3.12',
@@ -302,7 +305,6 @@ def test_parse_eyelink_raises_value_error(tmp_path, patterns):
             id='eye_link_1000_2',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             '** VERSION: EYELINK II 1\n'
             '** EYELINK II CL v2.12 Feb  1 2018',
             '2.12',
@@ -310,21 +312,18 @@ def test_parse_eyelink_raises_value_error(tmp_path, patterns):
             id='eye_link_II',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             '** VERSION: EYELINK REVISION 2.00 (Aug 12 1997)',
             '2.00',
             'EyeLink I',
             id='eye_link_I',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             '** VERSION: nothing\n',
             'unknown',
             'unknown',
             id='unknown_version_1',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             '** VERSION: EYELINK II 1\n'
             '** EYELINK II CL Feb  1 2018 (EyeLink Portable Duo)',
             'unknown',
@@ -332,7 +331,6 @@ def test_parse_eyelink_raises_value_error(tmp_path, patterns):
             id='unknown_version_2',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             '** TYPE: EDF_FILE BINARY EVENT SAMPLE TAGGED',
             'unknown',
             'unknown',
@@ -340,6 +338,8 @@ def test_parse_eyelink_raises_value_error(tmp_path, patterns):
         ),
     ],
 )
+@pytest.mark.filterwarnings('ignore:No metadata found.')
+@pytest.mark.filterwarnings('ignore:No recording configuration found.')
 def test_parse_eyelink_version(tmp_path, metadata, expected_version, expected_model):
     filepath = tmp_path / 'sub.asc'
     filepath.write_text(metadata)
@@ -356,25 +356,34 @@ def test_parse_eyelink_version(tmp_path, metadata, expected_version, expected_mo
     ('metadata', 'expected_msg'),
     [
         pytest.param(
-            '',
+            'MSG	2154555 RECCFG CR 1000 2 1 L\n',
             'No metadata found. Please check the file for errors.',
-            id='eye_link_no_metadata',
+            id='no_metadata',
+        ),
+        pytest.param(
+            '** DATE: Wed Mar  8 09:25:20 2023\n',
+            'No recording configuration found.',
+            id='no_reccfg',
+        ),
+        pytest.param(
+            'MSG	2154555 RECCFG CR 1000 2 1 L\n'
+            'MSG	2154556 RECCFG CR 2000 2 1 L\n',
+            r"Found inconsistent values for 'sampling_rate': \['1000', '2000'\]",
+            id='inconsistent_reccfg',
         ),
     ],
 )
-def test_no_metadata_warning(tmp_path, metadata, expected_msg):
+@pytest.mark.filterwarnings('ignore:No metadata found.')
+@pytest.mark.filterwarnings('ignore:No recording configuration found.')
+def test_metadata_warnings(tmp_path, metadata, expected_msg):
     filepath = tmp_path / 'sub.asc'
     filepath.write_text(metadata)
 
-    with pytest.raises(Warning) as info:
+    with pytest.warns(Warning, match=expected_msg):
         _, metadata = pm.gaze._utils.parsing.parse_eyelink(
             filepath,
         )
 
-    msg = info.value.args[0]
-
-    assert msg == expected_msg
-
 
 @pytest.mark.parametrize(
     ('metadata', 'expected_validation', 'expected_calibration'),
@@ -403,7 +412,6 @@ def test_no_metadata_warning(tmp_path, metadata, expected_msg):
             id='cal_timestamp_with_space',
         ),
         pytest.param(
-            '** DATE: Wed Feb  1 04:38:54 2017\n'
             'MSG	7045618 !CAL\n'
             '>>>>>>> CALIBRATION (HV9,P-CR) FOR LEFT: <<<<<<<<<\n',
             [],
@@ -416,7 +424,6 @@ def test_no_metadata_warning(tmp_path, metadata, expected_msg):
             id='cal_timestamp_no_space_no_val',
         ),
         pytest.param(
-            '** DATE: Wed Feb  1 04:38:54 2017\n'
             'MSG	7045618 !CAL\n'
             'MSG	7045618 !CAL\n',
             [],
@@ -425,6 +432,8 @@ def test_no_metadata_warning(tmp_path, metadata, expected_msg):
         ),
     ],
 )
+@pytest.mark.filterwarnings('ignore:No metadata found.')
+@pytest.mark.filterwarnings('ignore:No recording configuration found.')
 def test_val_cal_eyelink(tmp_path, metadata, expected_validation, expected_calibration):
     filepath = tmp_path / 'sub.asc'
     filepath.write_text(metadata)
@@ -460,7 +469,6 @@ def test_parse_val_cal_eyelink_monocular_file():
     ('metadata', 'expected_blinks'),
     [
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             'EVENTS	GAZE	LEFT	RATE	1000.00	TRACKING	CR	FILTER	2\n'
             'SBLINK R 10000018\n'
             '10000019	   .	   .	    0.0	    0.0	...\n'
@@ -475,7 +483,6 @@ def test_parse_val_cal_eyelink_monocular_file():
             id='blink',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             'EVENTS	GAZE	LEFT	RATE	1000.00	TRACKING	CR	FILTER	2\n'
             'SBLINK R 10000018\n'
             '10000019	   .	   .	    0.0	    0.0	...\n'
@@ -504,7 +511,6 @@ def test_parse_val_cal_eyelink_monocular_file():
             id='multiple_blinks',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             'EVENTS	GAZE	LEFT	RATE	1000.00	TRACKING	CR	FILTER	2\n'
             'SBLINK R 10000018\n'
             '10000019	   .	   .	    0.0	...\n'
@@ -533,7 +539,6 @@ def test_parse_val_cal_eyelink_monocular_file():
             id='multiple_blinks_no_dummy',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             'SBLINK R 10000018\n'
             '10000019	   .	   .	    0.0	    0.0	...\n'
             '10000020	   .	   .	    0.0	    0.0	...\n'
@@ -548,6 +553,8 @@ def test_parse_val_cal_eyelink_monocular_file():
         ),
     ],
 )
+@pytest.mark.filterwarnings('ignore:No metadata found.')
+@pytest.mark.filterwarnings('ignore:No recording configuration found.')
 def test_parse_eyelink_blinks(tmp_path, metadata, expected_blinks):
     filepath = tmp_path / 'sub.asc'
     filepath.write_text(metadata)
@@ -585,7 +592,6 @@ def test_parse_eyelink_blinks(tmp_path, metadata, expected_blinks):
             id='only_blinks_no_dummy',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             'START	10000018 	RIGHT	SAMPLES	EVENTS\n'
             'SBLINK R 10000018\n'
             '10000019	   .	   .	    0.0	    0.0	...\n'
@@ -618,7 +624,6 @@ def test_parse_eyelink_blinks(tmp_path, metadata, expected_blinks):
             id='blinks_and_lost_samples',
         ),
         pytest.param(
-            '** DATE: Wed Mar  8 09:25:20 2023\n'
             'START	10000021 	RIGHT	SAMPLES	EVENTS\n'
             '10000021	   .	   .	    0.0	    0.0	...\n'
             '10000022	   .	   .	    0.0	    0.0	...\n'
@@ -647,7 +652,7 @@ def test_parse_eyelink_blinks(tmp_path, metadata, expected_blinks):
             'END	10000022 	SAMPLES	EVENTS	RES	  38.54	  31.12\n',
             0,
             0.5,
-            id='missing_timestamps_lost_samples',
+            id='missing_timestamps_lost_samples1',
         ),
         pytest.param(
             'MSG	2154555 RECCFG CR 1000 2 1 L\n'
@@ -660,7 +665,7 @@ def test_parse_eyelink_blinks(tmp_path, metadata, expected_blinks):
             'END	10000024 	SAMPLES	EVENTS	RES	  38.54	  31.12\n',
             0.25,
             0.75,
-            id='missing_timestamps_lost_samples',
+            id='missing_timestamps_lost_samples_blink',
         ),
         pytest.param(
             'MSG	2154555 RECCFG CR 1000 2 1 L\n'
@@ -680,10 +685,12 @@ def test_parse_eyelink_blinks(tmp_path, metadata, expected_blinks):
             'END	10000021 	SAMPLES	EVENTS	RES	  38.54	  31.12\n',
             0,
             1,
-            id='missing_timestamps_lost_samples',
+            id='no_samples',
         ),
     ],
 )
+@pytest.mark.filterwarnings('ignore:No metadata found.')
+@pytest.mark.filterwarnings('ignore:No recording configuration found.')
 def test_parse_eyelink_data_loss_ratio(
         tmp_path, metadata, expected_blink_ratio, expected_overall_ratio,
 ):
@@ -696,6 +703,8 @@ def test_parse_eyelink_data_loss_ratio(
     assert parsed_metadata['data_loss_ratio'] == expected_overall_ratio
 
 
+@pytest.mark.filterwarnings('ignore:No metadata found.')
+@pytest.mark.filterwarnings('ignore:No recording configuration found.')
 def test_parse_eyelink_datetime(tmp_path):
     metadata = '** DATE: Wed Mar  8 09:25:20 2023\n'
     expected_datetime = datetime.datetime(2023, 3, 8, 9, 25, 20)
@@ -907,6 +916,7 @@ def test_parse_eyelink_mount_config(tmp_path, metadata, expected_mount_config):
         ),
     ],
 )
+@pytest.mark.filterwarnings('ignore:No recording configuration found.')
 def test_parse_eyelink_encoding(tmp_path, bytestring, encoding, expected_text):
     filepath = tmp_path / 'sub.asc'
     filepath.write_bytes(bytestring)