Repository: aeye-lab/pymovements
Pull request #286 (2023-03-15 13:46:35)

Feature: add `pymovements.utils.asc2csv`
convert ascii file to csv

tests both created dataframes and sent messages

codecov-commenter: ## [Codecov](https://codecov.io/gh/aeye-lab/pymovements/pull/286?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) Report
Patch coverage: **`100.00`**% and no project coverage change.
> Comparison is base [(`137a5af`)](https://codecov.io/gh/aeye-lab/pymovements/commit/137a5af0742935c30f63b7539fcf8e92fc10893c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00% compared to head [(`f4335bc`)](https://codecov.io/gh/aeye-lab/pymovements/pull/286?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) 100.00%.

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


```diff
@@            Coverage Diff             @@
##              main      #286    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           32        34     +2     
  Lines         1310      1428   +118     
  Branches       307       327    +20     
==========================================
+ Hits          1310      1428   +118     
```


| [Impacted Files](https://codecov.io/gh/aeye-lab/pymovements/pull/286?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab) | Coverage Δ | |
|---|---|---|
| [src/pymovements/utils/\_\_init\_\_.py](https://codecov.io/gh/aeye-lab/pymovements/pull/286?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL3V0aWxzL19faW5pdF9fLnB5) | `100.00% <ø> (ø)` | |
| [src/pymovements/utils/asc2csv.py](https://codecov.io/gh/aeye-lab/pymovements/pull/286?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab#diff-c3JjL3B5bW92ZW1lbnRzL3V0aWxzL2FzYzJjc3YucHk=) | `100.00% <100.00%> (ø)` | |

... and [14 files with indirect coverage changes](https://codecov.io/gh/aeye-lab/pymovements/pull/286/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab)

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/aeye-lab/pymovements/pull/286?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aeye-lab).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](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).


Patch:
diff --git a/src/pymovements/utils/__init__.py b/src/pymovements/utils/__init__.py
index be224cf..267882d 100644
--- a/src/pymovements/utils/__init__.py
+++ b/src/pymovements/utils/__init__.py
@@ -30,10 +30,12 @@
     pymovements.utils.decorators
     pymovements.utils.downloads
     pymovements.utils.filters
+    pymovements.utils.parsing
     pymovements.utils.paths
 """
 from pymovements.utils import archives  # noqa: F401
 from pymovements.utils import checks  # noqa: F401
 from pymovements.utils import decorators  # noqa: F401
 from pymovements.utils import downloads  # noqa: F401
+from pymovements.utils import parsing  # noqa: F401
 from pymovements.utils import paths  # noqa: F401
diff --git a/src/pymovements/utils/parsing.py b/src/pymovements/utils/parsing.py
new file mode 100755
index 0000000..56cba9e
--- /dev/null
+++ b/src/pymovements/utils/parsing.py
@@ -0,0 +1,197 @@
+# Copyright (c) 2023 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+""" Converts ascii to csv files."""
+from __future__ import annotations
+
+import os
+import re
+from pathlib import Path
+
+import numpy as np
+import polars as pl
+
+EYE_TRACKING_SAMPLE = re.compile(
+    r'(?P<time>(\d{7}|\d{8}|\d{9}|\d{10}))\s+'
+    r'(?P<x_eye>[-]?\d*[.]\d*)\s+'
+    r'(?P<y_eye>[-]?\d*[.]\d*)\s+'
+    r'(?P<pupil_eye>\d*[.]\d*)\s+'
+    r'(?P<dummy>\d*[.]\d*)\s+'
+    r'((?P<dots>[A-Za-z.]{5}))?\s*',
+)
+
+
+class scolor:
+    """Implements colors for conversion"""
+    created: str = '\x1b[1;37;42m'
+    skipped: str = '\x1b[1;37;44m'
+    rewritten: str = '\x1b[1;37;43m'
+    end: str = '\x1b[0m'
+
+
+def check_nan(sample_location: str) -> float:
+    """Returns position as float or np.nan depending on validity of sample
+
+    Parameters
+    ----------
+    sample_location: str
+        Sample location as extracted from ascii file.
+    """
+
+    try:
+        ret = float(sample_location)
+    except ValueError:
+        ret = np.nan
+    return ret
+
+
+def get_eye_tracking_sample_feature(eye_tracking_sample_match: re.Match, feature: str) -> str:
+    """Extract sample information from line.
+
+    Parameters
+    ----------
+    eye_tracking_sample_match: re.Match
+        Matched eye tracking sample pattern.
+    feature: str
+        feature to extract from matched eye tracking sample pattern.
+    """
+    return eye_tracking_sample_match.group(feature)
+
+
+def process_asc2csv(
+    file_name: Path,
+    sync_msg_start_pattern: None | str = None,
+    sync_msg_stop_pattern: None | str = None,
+    overwrite_existing: bool = False,
+) -> pl.DataFrame:
+    """Processes ascii files to csv.
+
+    Parameters
+    ----------
+    file_name: Path
+        file name of ascii file to convert
+    sync_msg_start_pattern: str,
+        Optional starting pattern of trial as sync message,
+        if None is given reverts back to TRIALID
+    sync_msg_stop_pattern: str,
+        Optional stopping pattern of trial as sync message,
+        if None is given reverts back to previous TRIALID
+    overwrite_existing: bool,
+        Decide whether to overwrite an existing csv file.
+    """
+    if sync_msg_start_pattern is None:
+        # if no specific type pattern revert to trial ids
+        _sync_msg_start_pattern = re.compile('MSG\t\\d+ TRIALID (?P<startpattern>[0-9]+)\n')
+    else:
+        _sync_msg_start_pattern = re.compile(
+            f'MSG\t\\d+ .\\d+ {sync_msg_start_pattern}(?P<startpattern>[0-9]+)',
+        )
+    if sync_msg_stop_pattern is None:
+        # if no stop pattern, stop when "next" occurance of start pattern
+        _sync_msg_stop_pattern = _sync_msg_start_pattern
+    else:
+        _sync_msg_stop_pattern = re.compile(
+            fr'MSG\t\d+\ \d+\ \S+(?P<stoppattern>{sync_msg_stop_pattern})\n',
+        )
+    csv_file_name = f"{Path(*file_name.parts[:-1])}/{file_name.parts[-1].split('.')[0]}.csv"
+    df = pl.DataFrame(
+        schema=[
+            ('time', pl.Int64),
+            ('x_eye', pl.Float64),
+            ('y_eye', pl.Float64),
+            ('pupil_eye', pl.Float64),
+            ('task', pl.Utf8),
+        ],
+    )
+    if not os.path.exists(csv_file_name):
+        status_msg = 'Created'
+        status_clr = scolor.created
+    elif os.path.exists(csv_file_name) and not overwrite_existing:
+        status_msg = 'Skipped'
+        status_clr = scolor.skipped
+        written_msg = f'Processing csv for {file_name}'.ljust(105 - len(status_msg), '.')
+        print(written_msg, end='', flush=True)
+        print(f'{status_clr}{status_msg}{scolor.end}', flush=True)
+        return df
+    else:
+        status_msg = 'Rewritten'
+        status_clr = scolor.rewritten
+
+    written_msg = f'Processing csv for {file_name}'.ljust(105 - len(status_msg), '.')
+    print(written_msg, end='', flush=True)
+    with open(file_name, encoding='ascii') as asc_file:
+        lines = asc_file.readlines()
+    cur_task_type = None
+    during_task_recording = False
+    for line in lines:
+
+        if _sync_msg_stop_pattern.match(line):
+            during_task_recording = False
+        if _sync_msg_start_pattern.match(line):
+            start_matched = _sync_msg_start_pattern.match(line)
+            # mypy is unaware that 'start_matched' can never be None (l.146)
+            assert start_matched is not None
+            start_id = start_matched.group('startpattern')
+            if sync_msg_start_pattern is not None:
+                cur_task_type = str(sync_msg_start_pattern) + start_id
+            else:
+                cur_task_type = f'TRIALID_{start_id}'
+            during_task_recording = True
+        if during_task_recording:
+            if EYE_TRACKING_SAMPLE.match(line):
+                eye_tracking_sample_match = EYE_TRACKING_SAMPLE.match(line)
+                # mypy is unaware that 'eye_tracking_sample_match' can never be None (l.156)
+                assert eye_tracking_sample_match is not None
+
+                timestamp_s = get_eye_tracking_sample_feature(
+                    eye_tracking_sample_match,
+                    'time',
+                )
+                x_eye_s = get_eye_tracking_sample_feature(
+                    eye_tracking_sample_match,
+                    'x_eye',
+                )
+                y_eye_s = get_eye_tracking_sample_feature(
+                    eye_tracking_sample_match,
+                    'y_eye',
+                )
+                pupil_eye_s = get_eye_tracking_sample_feature(
+                    eye_tracking_sample_match,
+                    'pupil_eye',
+                )
+
+                timestamp = int(timestamp_s)
+                x_eye = check_nan(x_eye_s)
+                y_eye = check_nan(y_eye_s)
+                pupil_eye = check_nan(pupil_eye_s)
+                df = df.extend(
+                    pl.DataFrame(
+                        {
+                            'time': [timestamp],
+                            'x_eye': [x_eye],
+                            'y_eye': [y_eye],
+                            'pupil_eye': [pupil_eye],
+                            'task': [cur_task_type],
+                        },
+                    ),
+                )
+
+    df.write_csv(csv_file_name, sep='\t')
+    print(f'{status_clr}{status_msg}{scolor.end}', flush=True)
+    return df


Test patch:
diff --git a/tests/utils/parsing_test.py b/tests/utils/parsing_test.py
new file mode 100644
index 0000000..0a7928b
--- /dev/null
+++ b/tests/utils/parsing_test.py
@@ -0,0 +1,307 @@
+# Copyright (c) 2023 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+# flake8: noqa: E101, W191, E501
+""" Tests pymovements asc to csv processing"""
+import os
+
+import numpy as np
+import polars as pl
+import pytest
+from polars.testing import assert_frame_equal
+
+from pymovements.utils.parsing import process_asc2csv
+from pymovements.utils.parsing import scolor
+
+
+ASC_TEXT = r"""\
+** CONVERTED FROM D:\SamplePymovements\results\sub_1\sub_1.edf using edfapi 4.2.1 Win32  EyeLink Dataviewer Sub ComponentApr 01 1990 on Wed Sep 20 13:47:57 1989
+** DATE: Wed Sep  20 13:47:20 1989
+** TYPE: EDF_FILE BINARY EVENT SAMPLE TAGGED
+** VERSION: EYELINK II 1
+** SOURCE: EYELINK CL
+** EYELINK II CL v6.12 Feb  1 2018 (EyeLink Portable Duo)
+** CAMERA: EyeLink USBCAM Version 1.01
+** SERIAL NUMBER: CLU-DAB50
+** CAMERA_CONFIG: DAB50200.SCD
+** RECORDED BY SleepAlc
+** SREB2.2.299 WIN32 LID:20A87A96 Mod:2023.03.08 11:03 MEZ
+**
+
+MSG	2091650 !CMD 1 select_parser_configuration 0
+MSG	2091659 !CMD 0 fixation_update_interval = 50
+MSG	2091659 !CMD 0 fixation_update_accumulate = 50
+MSG	2091681 !CMD 1 auto_calibration_messages = YES
+MSG	2095865 DISPLAY_COORDS 0 0 1279 1023
+MSG	2095865 RETRACE_INTERVAL  16.646125144
+MSG	2095865 ENVIRONMENT   OpenGL on Windows (6, 2, 9200, 2, '')
+MSG	2095980 TRACKER_TIME 0 2095980.470
+MSG	2096367 -4 SYNCTIME 766 0
+MSG	2100624 SYNCTIME_READING
+INPUT	2117909	0
+INPUT	2126823	0
+MSG	2135819 !CAL
+>>>>>>> CALIBRATION (HV9,P-CR) FOR LEFT: <<<<<<<<<
+MSG	2135819 !CAL Calibration points:
+MSG	2135819 !CAL -32.6, -47.7        -0,    227
+MSG	2135819 !CAL -32.2, -63.6        -0,  -2267
+MSG	2135819 !CAL -32.6, -31.7        -0,   2624
+MSG	2135819 !CAL -58.0, -47.3     -3291,    227
+MSG	2135819 !CAL -7.7, -46.8      3291,    227
+MSG	2135820 !CAL -58.8, -65.2     -3358,  -2267
+MSG	2135820 !CAL -7.9, -61.4      3358,  -2267
+MSG	2135820 !CAL -55.5, -31.2     -3227,   2624
+MSG	2135820 !CAL -9.0, -31.3      3227,   2624
+MSG	2135820 !CAL  0.0,  0.0         0,      0
+MSG	2135820 !CAL eye check box: (L,R,T,B)
+	  -65     6   -72     7
+MSG	2135820 !CAL href cal range: (L,R,T,B)
+	-5037  5037 -3489  3847
+MSG	2135820 !CAL Cal coeff:(X=a+bx+cy+dxx+eyy,Y=f+gx+goaly+ixx+jyy)
+  -0.00043008  131.07  1.437  0.051949 -0.1007
+   227.35 -1.5024  153.47 -0.1679 -0.22845
+MSG	2135820 !CAL Prenormalize: offx, offy = -32.583 -47.715
+MSG	2135820 !CAL Quadrant center: centx, centy =
+  -0.00043025  227.35
+MSG	2135820 !CAL Corner correction:
+   9.5364e-06,  3.4194e-05
+  -1.6932e-05,  2.9132e-05
+   3.3933e-05,  3.5e-06
+   1.5902e-05,  8.6479e-06
+MSG	2135820 !CAL Gains: cx:152.074 lx:170.107 rx:152.936
+MSG	2135820 !CAL Gains: cy:128.550 ty:155.848 by:116.611
+MSG	2135820 !CAL Resolution (upd) at screen center: X=1.7, Y=2.0
+MSG	2135820 !CAL Gain Change Proportion: X: 0.112 Y: 0.336
+MSG	2135821 !CAL Gain Ratio (Gy/Gx) = 0.845
+MSG	2135821 !CAL Bad Y/X gain ratio: 0.845
+MSG	2135821 !CAL PCR gain ratio(x,y) = 2.507, 2.179
+MSG	2135821 !CAL CR gain match(x,y) = 1.010, 1.010
+MSG	2135821 !CAL Slip rotation correction OFF
+MSG	2135821 !CAL CALIBRATION HV9 L LEFT    GOOD
+INPUT	2137650	0
+MSG	2148587 !CAL VALIDATION HV9 L LEFT  GOOD ERROR 0.27 avg. 0.83 max  OFFSET 0.11 deg. 3.7,2.4 pix.
+MSG	2148587 VALIDATE L POINT 0  LEFT  at 640,512  OFFSET 0.19 deg.  7.2,1.0 pix.
+MSG	2148587 VALIDATE L POINT 1  LEFT  at 640,159  OFFSET 0.12 deg.  3.9,-2.2 pix.
+MSG	2148587 VALIDATE L POINT 2  LEFT  at 640,864  OFFSET 0.42 deg.  -15.8,0.9 pix.
+MSG	2148587 VALIDATE L POINT 3  LEFT  at 172,512  OFFSET 0.83 deg.  26.3,17.5 pix.
+MSG	2148587 VALIDATE L POINT 4  LEFT  at 1107,512  OFFSET 0.19 deg.  -4.3,5.7 pix.
+MSG	2148587 VALIDATE L POINT 5  LEFT  at 228,201  OFFSET 0.06 deg.  1.3,1.9 pix.
+MSG	2148587 VALIDATE L POINT 6  LEFT  at 1051,201  OFFSET 0.33 deg.  -3.0,-12.5 pix.
+MSG	2148587 VALIDATE L POINT 7  LEFT  at 228,822  OFFSET 0.18 deg.  -6.8,0.2 pix.
+MSG	2148587 VALIDATE L POINT 8  LEFT  at 1051,822  OFFSET 0.18 deg.  3.8,5.5 pix.
+INPUT	2153108	0
+MSG	2154447 DRIFTCORRECT L LEFT  at 133,133  OFFSET 0.38 deg.  12.5,7.9 pix.
+MSG	2154540 TRIALID 0
+MSG	2154555 RECCFG CR 1000 2 1 L
+MSG	2154555 ELCLCFG BTABLER
+MSG	2154555 GAZE_COORDS 0.00 0.00 1279.00 1023.00
+MSG	2154555 THRESHOLDS L 102 242
+MSG	2154555 ELCL_WINDOW_SIZES 176 188 0 0
+MSG	2154555 CAMERA_LENS_FOCAL_LENGTH 27.00
+MSG	2154555 PUPIL_DATA_TYPE RAW_AUTOSLIP
+MSG	2154555 ELCL_PROC CENTROID (3)
+MSG	2154555 ELCL_PCR_PARAM 5 3.0
+START	2154556 	LEFT	SAMPLES	EVENTS
+PRESCALER	1
+VPRESCALER	1
+PUPIL	AREA
+EVENTS	GAZE	LEFT	RATE	1000.00	TRACKING	CR	FILTER	2
+SAMPLES	GAZE	LEFT	RATE	1000.00	TRACKING	CR	FILTER	2	INPUT
+INPUT	2154556	0
+MSG	2154557 !MODE RECORD CR 1000 2 1 L
+2154557	  139.6	  132.1	  784.0	    0.0	...
+MSG	2154558 -11 SYNCTIME_READING_SCREEN_0
+2154558	  139.5	  131.9	  784.0	    0.0	...
+MSG	2154559 -11 !V DRAW_LIST ../../runtime/dataviewer/sub_1/graphics/VC_1.vcl
+2154560	   .	   .	    0.0	    0.0	...
+2154561	  850.7	  717.5	  714.0	    0.0	...
+MSG	2154562 TRACKER_TIME 1 2222195.987
+MSG	2154563 0 READING_SCREEN_0.STOP
+MSG	2154447 DRIFTCORRECT L LEFT  at 133,133  OFFSET 0.38 deg.  12.5,7.9 pix.
+MSG	2154540 TRIALID 1
+MSG	2154555 RECCFG CR 1000 2 1 L
+MSG	2154555 ELCLCFG BTABLER
+MSG	2154555 GAZE_COORDS 0.00 0.00 1279.00 1023.00
+MSG	2154555 THRESHOLDS L 102 242
+MSG	2154555 ELCL_WINDOW_SIZES 176 188 0 0
+MSG	2154555 CAMERA_LENS_FOCAL_LENGTH 27.00
+MSG	2154555 PUPIL_DATA_TYPE RAW_AUTOSLIP
+MSG	2154555 ELCL_PROC CENTROID (3)
+MSG	2154555 ELCL_PCR_PARAM 5 3.0
+MSG	2154564 -11 SYNCTIME_READING_SCREEN_1
+2154565	  139.5	  131.9	  784.0	    0.0	...
+MSG	2154566 -11 !V DRAW_LIST ../../runtime/dataviewer/sub_1/graphics/VC_2.vcl
+2154567	   .	   .	    0.0	    0.0	...
+2154568	  850.7	  717.5	  714.0	    0.0	...
+MSG	2154569 TRACKER_TIME 1 2222195.987
+MSG	2154570 0 READING_SCREEN_1.STOP
+"""
+
+EXPECTED_DF_NO_SYNC = pl.from_dict(
+    {
+        'time': [
+            2154557,
+            2154558,
+            2154560,
+            2154561,
+            2154565,
+            2154567,
+            2154568,
+        ], 'x_eye': [
+            139.6,
+            139.5,
+            np.nan,
+            850.7,
+            139.5,
+            np.nan,
+            850.7,
+        ], 'y_eye': [
+            132.1,
+            131.9,
+            np.nan,
+            717.5,
+            131.9,
+            np.nan,
+            717.5,
+        ], 'pupil_eye': [
+            784.0,
+            784.0,
+            0.0,
+            714.0,
+            784.0,
+            0.0,
+            714.0,
+        ], 'task': [
+            'TRIALID_0',
+            'TRIALID_0',
+            'TRIALID_0',
+            'TRIALID_0',
+            'TRIALID_1',
+            'TRIALID_1',
+            'TRIALID_1',
+        ],
+    },
+)
+
+EXPECTED_DF_SYNC = pl.from_dict(
+    {
+        'time': [
+            2154558,
+            2154560,
+            2154561,
+            2154565,
+            2154567,
+            2154568,
+        ], 'x_eye': [
+            139.5,
+            np.nan,
+            850.7,
+            139.5,
+            np.nan,
+            850.7,
+        ], 'y_eye': [
+            131.9,
+            np.nan,
+            717.5,
+            131.9,
+            np.nan,
+            717.5,
+        ], 'pupil_eye': [
+            784.0,
+            0.0,
+            714.0,
+            784.0,
+            0.0,
+            714.0,
+        ], 'task': [
+            'SYNCTIME_READING_SCREEN_0',
+            'SYNCTIME_READING_SCREEN_0',
+            'SYNCTIME_READING_SCREEN_0',
+            'SYNCTIME_READING_SCREEN_1',
+            'SYNCTIME_READING_SCREEN_1',
+            'SYNCTIME_READING_SCREEN_1',
+        ],
+    },
+)
+
+
+@pytest.mark.parametrize(
+    'sync_msg_start_pattern',
+    [None, 'SYNCTIME_READING_SCREEN_'],
+)
+@pytest.mark.parametrize(
+    'sync_msg_stop_pattern',
+    [None, 'STOP'],
+)
+@pytest.mark.parametrize(
+    'overwrite_existing',
+    [False, True],
+)
+@pytest.mark.parametrize(
+    'create_overwrite_existing',
+    [False, True],
+)
+def test_process_asc2csv(
+    tmp_path, sync_msg_start_pattern, sync_msg_stop_pattern,
+    overwrite_existing, create_overwrite_existing, capsys,
+):
+    filepath = tmp_path / 'sub.asc'
+    filepath.write_text(ASC_TEXT)
+    if create_overwrite_existing:
+        status_msg = 'Rewritten'
+        start_color = scolor.rewritten
+        csv_filepath = tmp_path / 'sub.csv'
+        csv_filepath.write_text('test')
+    else:
+        status_msg = 'Created'
+        start_color = scolor.created
+    df = process_asc2csv(
+        filepath,
+        sync_msg_start_pattern,
+        sync_msg_stop_pattern,
+        overwrite_existing,
+    )
+    captured = capsys.readouterr()
+    if sync_msg_start_pattern:
+        expected_df = EXPECTED_DF_SYNC
+    else:
+        expected_df = EXPECTED_DF_NO_SYNC
+    if create_overwrite_existing and not overwrite_existing:
+        status_msg = 'Skipped'
+        msg_begin = f"Processing csv for {os.path.join(tmp_path, 'sub.asc')}".ljust(
+            105 - len(status_msg), '.',
+        )
+        expected_out = msg_begin + f'{scolor.skipped}{status_msg}{scolor.end}\n'
+        expected_df = pl.DataFrame(
+            schema=[
+                ('time', pl.Int64),
+                ('x_eye', pl.Float64),
+                ('y_eye', pl.Float64),
+                ('pupil_eye', pl.Float64),
+                ('task', pl.Utf8),
+            ],
+        )
+        assert_frame_equal(df, expected_df)
+        assert captured.out == expected_out
+    else:
+        msg_begin = f"Processing csv for {os.path.join(tmp_path, 'sub.asc')}".ljust(
+            105 - len(status_msg), '.',
+        )
+        expected_out = msg_begin + f'{start_color}{status_msg}{scolor.end}\n'
+        assert captured.out == expected_out