Repository: M0r13n/pyais

Issue:
Non-deterministic results from 'decode_msg'
I am seeing some non-determinism in `decode_msg` output, e.g.:

```
>>> raw = b'!AIVDM,1,1,,A,D02=ag0flffp,0*11'
>>> pyais.decode_msg(raw)['offset2']
2
>>> pyais.decode_msg(raw)['offset2']
0
```

Memory overflow? This AIS message itself seems short but I would expect an error if it was unexpected length?

python: 3.9
pyais: 1.6.2
bitarray: 2.3.4

Comments:

Patch:
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 38e9c26..019cc19 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,6 +1,15 @@
 ====================
  pyais CHANGELOG
 ====================
+-------------------------------------------------------------------------------
+ Version 1.6.3  5 Dec 2021
+-------------------------------------------------------------------------------
+
+* Correctly handles variable length messages (https://github.com/M0r13n/pyais/issues/40)
+    - prior versions were missing required length checks
+    - therefore some messages were not correctly decoded
+    - this affects Type 7, 15, 16, 20, 22, 24, 25 and 26
+
 -------------------------------------------------------------------------------
  Version 1.6.2  2 May 2021
 -------------------------------------------------------------------------------
diff --git a/pyais/decode.py b/pyais/decode.py
index c067c2b..f8e8357 100644
--- a/pyais/decode.py
+++ b/pyais/decode.py
@@ -15,7 +15,7 @@ from pyais.constants import (
     NavAid
 )
 from pyais.exceptions import UnknownMessageException, MissingMultipartMessageException, TooManyMessagesException
-from pyais.util import get_int, encode_bin_as_ascii6, get_mmsi
+from pyais.util import get_int, encode_bin_as_ascii6, get_mmsi, binary_data
 
 
 def decode_msg_1(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
@@ -128,7 +128,7 @@ def decode_msg_6(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
         'retransmit': bit_arr[70],
         'dac': get_int_from_data(72, 82),
         'fid': get_int_from_data(82, 88),
-        'data': bit_arr[88:].to01()
+        'data': binary_data(bit_arr, 88)
     }
 
 
@@ -138,19 +138,39 @@ def decode_msg_7(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
     Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_7_binary_acknowledge
     """
     get_int_from_data = partial(get_int, bit_arr)
-    return {
+    length = len(bit_arr)
+    # Total length varies between 72 and 168 bits depending on the number of addressed ships
+    # Each address requires 32 bit
+    data = {
         'type': get_int_from_data(0, 6),
         'repeat': get_int_from_data(6, 8),
         'mmsi': get_mmsi(bit_arr, 8, 38),
         'mmsi1': get_mmsi(bit_arr, 40, 70),
         'mmsiseq1': get_int_from_data(70, 72),
-        'mmsi2': get_mmsi(bit_arr, 72, 102),
-        'mmsiseq2': get_int_from_data(102, 104),
-        'mmsi3': get_mmsi(bit_arr, 104, 134),
-        'mmsiseq3': get_int_from_data(134, 136),
-        'mmsi4': get_mmsi(bit_arr, 136, 166),
-        'mmsiseq4': get_int_from_data(166, 168)
+        'mmsi2': None,
+        'mmsiseq2': None,
+        'mmsi3': None,
+        'mmsiseq3': None,
+        'mmsi4': None,
+        'mmsiseq4': None,
     }
+    if 72 < length <= 104:
+        data['mmsi2'] = get_mmsi(bit_arr, 72, 102)
+        data['mmsiseq2'] = get_int_from_data(102, 104)
+    elif 104 < length <= 136:
+        data['mmsi2'] = get_mmsi(bit_arr, 72, 102)
+        data['mmsiseq2'] = get_int_from_data(102, 104)
+        data['mmsi3'] = get_mmsi(bit_arr, 104, 134)
+        data['mmsiseq3'] = get_int_from_data(134, 136)
+    if 136 < length:
+        data['mmsi2'] = get_mmsi(bit_arr, 72, 102)
+        data['mmsiseq2'] = get_int_from_data(102, 104)
+        data['mmsi3'] = get_mmsi(bit_arr, 104, 134)
+        data['mmsiseq3'] = get_int_from_data(134, 136)
+        data['mmsi4'] = get_mmsi(bit_arr, 136, 166)
+        data['mmsiseq4'] = get_int_from_data(166, 168)
+
+    return data
 
 
 def decode_msg_8(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
@@ -165,7 +185,7 @@ def decode_msg_8(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
         'mmsi': get_mmsi(bit_arr, 8, 38),
         'dac': get_int_from_data(40, 50),
         'fid': get_int_from_data(50, 56),
-        'data': bit_arr[56:].to01()
+        'data': binary_data(bit_arr, 56)
     }
 
 
@@ -259,7 +279,7 @@ def decode_msg_15(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
     Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_15_interrogation
     """
     get_int_from_data = partial(get_int, bit_arr)
-    return {
+    data = {
         'type': get_int_from_data(0, 6),
         'repeat': get_int_from_data(6, 8),
         'mmsi': get_mmsi(bit_arr, 8, 38),
@@ -268,30 +288,46 @@ def decode_msg_15(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
         'offset1_1': get_int_from_data(76, 88),
         'type1_2': get_int_from_data(90, 96),
         'offset1_2': get_int_from_data(96, 108),
-        'mmsi2': get_mmsi(bit_arr, 110, 140),
-        'type2_1': get_int_from_data(140, 146),
-        'offset2_1': get_int_from_data(146, 157),
+        'mmsi2': None,
+        'type2_1': None,
+        'offset2_1': None,
     }
 
+    if len(bit_arr) > 88:
+        # TODO (richter): there are more edge cases
+        data['mmsi2'] = get_mmsi(bit_arr, 110, 140)
+        data['type2_1'] = get_int_from_data(140, 146)
+        data['offset2_1'] = get_int_from_data(146, 157)
+
+    return data
+
 
 def decode_msg_16(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
     """
     Assignment Mode Command
-    Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_16_assignment_mode_command
+        Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_16_assignment_mode_command
     """
     get_int_from_data = partial(get_int, bit_arr)
-    return {
+    data = {
         'type': get_int_from_data(0, 6),
         'repeat': get_int_from_data(6, 8),
         'mmsi': get_mmsi(bit_arr, 8, 38),
         'mmsi1': get_mmsi(bit_arr, 40, 70),
         'offset1': get_int_from_data(70, 82),
         'increment1': get_int_from_data(82, 92),
-        'mmsi2': get_mmsi(bit_arr, 92, 122),
-        'offset2': get_int_from_data(122, 134),
-        'increment2': get_int_from_data(134, 144)
+        'mmsi2': None,
+        'offset2': None,
+        'increment2': None
     }
 
+    if len(data) > 96:
+        # If the message is 96 bits long it should be interpreted as a channel assignment for two stations
+        data['mmsi2'] = get_mmsi(bit_arr, 92, 122)
+        data['offset2'] = get_int_from_data(122, 134)
+        data['increment2'] = get_int_from_data(134, 144)
+
+    return data
+
 
 def decode_msg_17(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
     """
@@ -305,7 +341,7 @@ def decode_msg_17(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
         'mmsi': get_mmsi(bit_arr, 8, 38),
         'lon': get_int_from_data(40, 58, signed=True),
         'lat': get_int_from_data(58, 75, signed=True),
-        'data': get_int_from_data(80, 816)
+        'data': binary_data(bit_arr, 80)
     }
 
 
@@ -375,7 +411,7 @@ def decode_msg_20(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
     Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_20_data_link_management_message
     """
     get_int_from_data = partial(get_int, bit_arr)
-    return {
+    data = {
         'type': get_int_from_data(0, 6),
         'repeat': get_int_from_data(8, 8),
         'mmsi': get_mmsi(bit_arr, 8, 38),
@@ -385,22 +421,41 @@ def decode_msg_20(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
         'timeout1': get_int_from_data(56, 59),
         'increment1': get_int_from_data(59, 70),
 
-        'offset2': get_int_from_data(70, 82),
-        'number2': get_int_from_data(82, 86),
-        'timeout2': get_int_from_data(86, 89),
-        'increment2': get_int_from_data(89, 100),
+        'offset2': None,
+        'number2': None,
+        'timeout2': None,
+        'increment2': None,
 
-        'offset3': get_int_from_data(100, 112),
-        'number3': get_int_from_data(112, 116),
-        'timeout3': get_int_from_data(116, 119),
-        'increment3': get_int_from_data(110, 130),
+        'offset3': None,
+        'number3': None,
+        'timeout3': None,
+        'increment3': None,
 
-        'offset4': get_int_from_data(130, 142),
-        'number4': get_int_from_data(142, 146),
-        'timeout4': get_int_from_data(146, 149),
-        'increment4': get_int_from_data(149, 160),
+        'offset4': None,
+        'number4': None,
+        'timeout4': None,
+        'increment4': None,
     }
 
+    length = len(bit_arr)
+    if 100 <= length:
+        data['offset2'] = get_int_from_data(70, 82)
+        data['number2'] = get_int_from_data(82, 86)
+        data['timeout2'] = get_int_from_data(86, 89)
+        data['increment2'] = get_int_from_data(89, 100)
+    if 130 <= length:
+        data['offset3'] = get_int_from_data(100, 112)
+        data['number3'] = get_int_from_data(112, 116)
+        data['timeout3'] = get_int_from_data(116, 119)
+        data['increment3'] = get_int_from_data(119, 130)
+    if 160 <= length:
+        data['offset4'] = get_int_from_data(130, 142)
+        data['number4'] = get_int_from_data(142, 146)
+        data['timeout4'] = get_int_from_data(146, 149)
+        data['increment4'] = get_int_from_data(149, 160)
+
+    return data
+
 
 def decode_msg_21(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
     """
@@ -446,7 +501,6 @@ def decode_msg_22(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
         'type': get_int_from_data(0, 6),
         'repeat': get_int_from_data(8, 8),
         'mmsi': get_mmsi(bit_arr, 8, 38),
-
         'channel_a': get_int_from_data(40, 52),
         'channel_b': get_int_from_data(52, 64),
         'txrx': get_int_from_data(64, 68),
@@ -570,11 +624,11 @@ def decode_msg_25(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
     if data['structured']:
         d = {
             'app_id': get_int_from_data(lo_ix, hi_ix),
-            'data': bit_arr[hi_ix:].to01()
+            'data': binary_data(bit_arr, hi_ix)
         }
     else:
         d = {
-            'data': bit_arr[lo_ix:].to01()
+            'data': binary_data(bit_arr, lo_ix)
         }
     data.update(d)
     return data
@@ -615,11 +669,11 @@ def decode_msg_26(bit_arr: bitarray.bitarray) -> Dict[str, Any]:
     if data['structured']:
         d = {
             'app_id': get_int_from_data(lo_ix, hi_ix),
-            'data': bit_arr[hi_ix:radio_status_offset].to01()
+            'data': binary_data(bit_arr, hi_ix, radio_status_offset)
         }
     else:
         d = {
-            'data': bit_arr[lo_ix:radio_status_offset].to01()
+            'data': binary_data(bit_arr, lo_ix, radio_status_offset)
         }
 
     data.update(d)
diff --git a/pyais/util.py b/pyais/util.py
index 5ef6529..a7cefd9 100644
--- a/pyais/util.py
+++ b/pyais/util.py
@@ -2,7 +2,7 @@ import warnings
 from collections import OrderedDict
 from functools import partial, reduce
 from operator import xor
-from typing import Any, Generator, Hashable, TYPE_CHECKING, Callable
+from typing import Any, Generator, Hashable, TYPE_CHECKING, Callable, Optional
 
 from bitarray import bitarray
 
@@ -88,7 +88,7 @@ def get_int(data: bitarray, ix_low: int, ix_high: int, signed: bool = False) ->
     :param ix_low: the lower index of the sub-array
     :param ix_high: the upper index of the sub-array
     :param signed: True if the value should be interpreted as a signed integer
-    :return: a normal integer (int)
+    :return: The integer value of the sub-array data[ix_low:ix_high]
     """
     shift: int = (8 - ((ix_high - ix_low) % 8)) % 8
     data = data[ix_low:ix_high]
@@ -96,13 +96,40 @@ def get_int(data: bitarray, ix_low: int, ix_high: int, signed: bool = False) ->
     return i >> shift
 
 
+def binary_data(data: bitarray, ix_low: int, ix_high: Optional[int] = None) -> Optional[str]:
+    """
+    Get a sub_array of a bitarray as bitstring.
+
+    :param data: some bitarray
+    :param ix_low: the lower index of the sub-array
+    :param ix_high: the upper index of the sub-array
+    :return: The integer value of the sub-array data[ix_low:ix_high]
+    """
+    length = len(data)
+    if ix_high is None:
+        ix_high = length
+    if ix_low >= length or ix_high > length:
+        # Indices out of bounds
+        return None
+
+    return data[ix_low:ix_high].to01()
+
+
 def get_mmsi(data: bitarray, ix_low: int, ix_high: int) -> str:
     """
     A Maritime Mobile Service Identity (MMSI) is a series of nine digits.
     Every digit is required and therefore we can NOT use a int.
     See: issue #6
     """
+
     mmsi_int: int = get_int(data, ix_low, ix_high)
+    if len(data) < ix_high:
+        # Remove padding from MMSIs shorter than 30 bits
+        mask = 0x3fffffff
+        d = ix_high - len(data)
+        mask ^= int(d * "1", 2)
+        mmsi_int &= mask
+
     return str(mmsi_int).zfill(9)
 
 


Test patch:
diff --git a/tests/test_ais.py b/tests/test_ais.py
index 940a09a..9b355a4 100644
--- a/tests/test_ais.py
+++ b/tests/test_ais.py
@@ -197,6 +197,24 @@ class TestAIS(unittest.TestCase):
         assert msg['destination'] == "NEW YORK"
         assert msg['dte'] == 0
 
+    def test_msg_type_5_a(self):
+        content = decode_msg(
+            "!AIVDM,2,1,9,B,53nFBv01SJ<thHp6220H4heHTf2222222222221?50:454o<`9QSlUDp,0*09",
+            "!AIVDM,2,2,9,B,888888888888880,2*2E"
+        )
+
+        assert content['type'] == 5
+        assert content['mmsi'] == "258315000"
+        assert content['imo'] == 6514895
+        assert content['callsign'] == "LFNA"
+        assert content['shipname'] == "FALKVIK"
+        assert content['shiptype'].value == 79
+        assert content['to_bow'] == 40
+        assert content['to_stern'] == 10
+        assert content['to_port'] == 4
+        assert content['to_starboard'] == 5
+        assert content['destination'] == "FORUS"
+
     def test_msg_type_6(self):
         msg = NMEAMessage(b"!AIVDM,1,1,,B,6B?n;be:cbapalgc;i6?Ow4,2*4A").decode()
         assert msg['seqno'] == 3
@@ -204,9 +222,27 @@ class TestAIS(unittest.TestCase):
         assert msg['dac'] == 669
         assert msg['fid'] == 11
 
-    def test_msg_type_7(self):
+    def test_msg_type_7_a(self):
         msg = NMEAMessage(b"!AIVDM,1,1,,A,702R5`hwCjq8,0*6B").decode()
+        assert msg['mmsi'] == "002655651"
         assert msg['mmsi1'] == "265538450"
+        assert msg['mmsi2'] is None
+        assert msg['mmsi3'] is None
+        assert msg['mmsi4'] is None
+
+    def test_msg_type_7_b(self):
+        msg = NMEAMessage(b"!AIVDM,1,1,,B,7`0Pv1L:Ac8rbgPKHA8`P,2*56").decode()
+        assert msg['mmsi'] == "537411077"
+        assert msg['mmsi1'] == "043101326"
+        assert msg['mmsi2'] == "717096664"
+
+    def test_msg_type_7_c(self):
+        msg = NMEAMessage(b"!AIVDM,1,1,,A,7IiQ4T`UjA9lC;b:M<MWE@,4*01").decode()
+        assert msg['mmsi'] == "655901842"
+        assert msg['mmsi1'] == "158483613"
+        assert msg['mmsi2'] == "321823389"
+        assert msg['mmsi3'] == "836359488"
+        assert msg['mmsi4'] is None
 
     def test_msg_type_8(self):
         msg = NMEAMessage(b"!AIVDM,1,1,,A,85Mwp`1Kf3aCnsNvBWLi=wQuNhA5t43N`5nCuI=p<IBfVqnMgPGs,0*47").decode()
@@ -307,7 +343,7 @@ class TestAIS(unittest.TestCase):
         assert msg['offset1_2'] == 617
         assert msg['offset1_1'] == 516
 
-    def test_msg_type_16(self):
+    def test_msg_type_16_b(self):
         msg = NMEAMessage(b"!AIVDM,1,1,,A,@01uEO@mMk7P<P00,0*18").decode()
         assert msg['type'] == 16
         assert msg['repeat'] == 0
@@ -316,21 +352,30 @@ class TestAIS(unittest.TestCase):
         assert msg['offset1'] == 200
         assert msg['increment1'] == 0
 
-        assert msg['offset2'] == 0
+        assert msg['offset2'] is None
         assert msg['increment1'] == 0
 
+    def test_msg_type_16_a(self):
+        msg = NMEAMessage(b"!AIVDM,1,1,,A,@TFtghNJ4G5?C7mV,0*3D").decode()
+        assert msg['type'] == 16
+        assert msg['mmsi'] == "292499393"
+        assert msg['increment1'] == 982
+
     def test_msg_type_17(self):
         msg = NMEAMessage.assemble_from_iterable(messages=[
             NMEAMessage(b"!AIVDM,2,1,5,A,A02VqLPA4I6C07h5Ed1h<OrsuBTTwS?r:C?w`?la<gno1RTRwSP9:BcurA8a,0*3A"),
             NMEAMessage(b"!AIVDM,2,2,5,A,:Oko02TSwu8<:Jbb,0*11")
         ]).decode()
-        n = 0x7c0556c07031febbf52924fe33fa2933ffa0fd2932fdb7062922fe3809292afde9122929fcf7002923ffd20c29aaaa
         assert msg['type'] == 17
         assert msg['repeat'] == 0
         assert msg['mmsi'] == "002734450"
         assert msg['lon'] == 17478
         assert msg['lat'] == 35992
-        assert msg['data'] == n
+        assert msg['data'] == "0111110000000101010101101100000001110000001100011111111010111011111101010010100" \
+                              "1001001001111111000110011111110100010100100110011111111111010000011111101001010" \
+                              "0100110010111111011011011100000110001010010010001011111110001110000000100100101" \
+                              "0010010101011111101111010010001001000101001001010011111110011110111000000000010" \
+                              "100100100011111111111101001000001100001010011010101010101010"
 
         msg = NMEAMessage(b"!AIVDM,1,1,,A,A0476BQ>J8`<h2JpH:4P0?j@2mTEw8`=DP1DEnqvj0,0*79").decode()
         assert msg['type'] == 17
@@ -338,7 +383,9 @@ class TestAIS(unittest.TestCase):
         assert msg['mmsi'] == "004310602"
         assert msg['lat'] == 20582
         assert msg['lon'] == 80290
-        assert msg['data'] == 14486955885545814640451754168044205828166539334830080
+        assert msg['data'] == "001001101011100001100000101000010010000000000000111111001001000000001011010110" \
+                              "010001010111111100100010100000110101010010000000000101010001010111011011100111" \
+                              "1110110010000000"
 
     def test_msg_type_18(self):
         msg = NMEAMessage(b"!AIVDM,1,1,,A,B5NJ;PP005l4ot5Isbl03wsUkP06,0*76").decode()
@@ -390,6 +437,27 @@ class TestAIS(unittest.TestCase):
             if k not in ('type', 'mmsi', 'offset1', 'number1', 'timeout1', 'increment1'):
                 assert not v
 
+    def test_msg_type_20_a(self):
+        msg = NMEAMessage(b"!AIVDM,1,1,,B,D030p8@2tN?b<`O6DmQO6D0,2*5D").decode()
+        assert msg['type'] == 20
+        assert msg['mmsi'] == "003160097"
+        assert msg['offset1'] == 47
+        assert msg['number1'] == 1
+        assert msg['timeout1'] == 7
+        assert msg['increment1'] == 250
+
+        assert msg['offset2'] == 2250
+        assert msg['number2'] == 1
+        assert msg['timeout2'] == 7
+        assert msg['increment2'] == 1125
+
+        assert msg['offset3'] == 856
+        assert msg['number3'] == 5
+        assert msg['timeout3'] == 7
+        assert msg['increment3'] == 1125
+
+        assert msg['offset4'] is None
+
     def test_msg_type_21(self):
         msg = NMEAMessage.assemble_from_iterable(messages=[
             NMEAMessage(b"!AIVDM,2,1,7,B,E4eHJhPR37q0000000000000000KUOSc=rq4h00000a,0*4A"),
@@ -632,3 +700,13 @@ class TestAIS(unittest.TestCase):
         self.assertEqual(content["minute"], 0)
         self.assertEqual(content["draught"], 4.7)
         self.assertEqual(content["destination"], "VIANA DO CASTELO")
+
+    def test_misc_messages(self):
+        content = decode_msg(
+            "!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26"
+        )
+
+        assert content['type'] == 1
+        assert content['mmsi'] == "244670316"
+        assert content['lon'] == 4.379285
+        assert content['lat'] == 51.89475
diff --git a/tests/test_decode_raw.py b/tests/test_decode_raw.py
index 781e2c5..da24b93 100644
--- a/tests/test_decode_raw.py
+++ b/tests/test_decode_raw.py
@@ -1,7 +1,10 @@
 import unittest
 
+from bitarray import bitarray
+
 from pyais import decode_msg
 from pyais.exceptions import InvalidNMEAMessageException, MissingMultipartMessageException, TooManyMessagesException
+from pyais.util import binary_data
 
 
 class TestDecode(unittest.TestCase):
@@ -97,3 +100,20 @@ class TestDecode(unittest.TestCase):
             msg_1,
             msg_2
         )
+
+    def test_binary_data_out_of_bounds(self):
+        b = bitarray('010100000000000010001101101001101111000000101110110100101110101110111000')
+
+        self.assertEqual("10000", binary_data(b, 35, 40))
+
+        # Lower index out of bounds
+        self.assertIsNone(binary_data(b, 72, 73))
+
+        # Upper index out of bounds
+        self.assertIsNone(binary_data(b, 0, 73))
+
+        # Lower and upper index out of bounds
+        self.assertIsNone(binary_data(b, 72, 72))
+
+        # Lower and upper index in bound
+        self.assertIsNotNone(binary_data(b, 71, 71))
diff --git a/tests/test_file_stream.py b/tests/test_file_stream.py
index 45bdb36..fc0bca4 100644
--- a/tests/test_file_stream.py
+++ b/tests/test_file_stream.py
@@ -6,7 +6,7 @@ from pyais.messages import NMEAMessage
 
 
 class TestFileReaderStream(unittest.TestCase):
-    FILENAME = "tests/ais_test_messages"
+    FILENAME = pathlib.Path(__file__).parent.joinpath("ais_test_messages")
 
     def test_reader(self):
         with FileReaderStream(self.FILENAME) as stream:
diff --git a/tests/test_main.py b/tests/test_main.py
index 0280554..3e3dedf 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,3 +1,4 @@
+import pathlib
 import sys
 import unittest
 
@@ -27,7 +28,7 @@ class TestMainApp(unittest.TestCase):
 
     def test_decode_from_file(self):
         class DemoNamespace:
-            in_file = open("tests/ais_test_messages", "rb")
+            in_file = open(pathlib.Path(__file__).parent.joinpath("ais_test_messages"), "rb")
             out_file = None
 
         assert decode_from_file(DemoNamespace()) == 0
@@ -41,9 +42,10 @@ class TestMainApp(unittest.TestCase):
         assert ns.in_file is None
 
         # But this can be overwritten to any file that exists
-        ns = parser.parse_args(["-f", "tests/ais_test_messages"])
+        file = str(pathlib.Path(__file__).parent.joinpath("ais_test_messages"))
+        ns = parser.parse_args(["-f", file])
         assert ns.func == decode_from_file
-        assert ns.in_file.name == "tests/ais_test_messages"
+        assert ns.in_file.name == file
         ns.in_file.close()
 
         # If the file does not exist an error is thrown