diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..019f533 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: CI / CD - Morse Code + +on: + pull_request: + branches: [pipfile-experiment] + push: + tags: ["v*"] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: + python-version: ["3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Install Python and Pipenv + uses: kojoru/prepare-pipenv@v1 + with: + python-version: ${{ matrix.python-version }} + + + - name: Turn on 'editable' mode + run: | + pipenv install -e . + + - name: Run pytest + run: | + pipenv install pytest + pipenv --venv + pipenv run python -m pytest + + deliver: + if: github.event_name != 'pull_request' + needs: [build] + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - uses: actions/checkout@v4 + + - name: Install Python, pipenv and Pipfile packages + uses: kojoru/prepare-pipenv@v1 + with: + python-version: "3.10" + + - name: Build package + run: | + pipenv install build + pipenv run python -m build . + + # publish to PyPI Test server + # - name: Publish to TestPyPI + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # password: ${{ secrets.TEST_PYPI_API_TOKEN }} + # repository-url: https://test.pypi.org/legacy/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..7e1d224 --- /dev/null +++ b/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pytest = "*" +morse-code = {file = ".", editable = true} + +[dev-packages] + +[requires] +python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..7c5eb6e --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,66 @@ +{ + "_meta": { + "hash": { + "sha256": "8d4fce0340a506ab07fcfec2c61ecc8853335394bbda7148e9f28d01a844aded" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "morse-code": { + "editable": true, + "file": "." + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", + "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==8.4.2" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 6022e0e..2272b65 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ # Python Package Exercise An exercise to create a Python package, build it, test it, distribute it, and use it. See [instructions](./instructions.md) for details. + +## morseify - Overview +morseify is a lightweight Python package that brings the world of Morse code to anyone curious about how digital communication began. It offers intuitive encode and decode functionality to translate between English text and Morse code, along with built-in tools for text normalization and message validation. + +For a more interactive experience, morseify also includes a quiz mode that generates random Morse code challenges for users to decode, making it both an educational and entertaining way to explore the fundamentals of encoded communication. + +## Features +- Encode English text to Morse code +- Decode Morse code to English text +- Normalize text and Morse sequences +- Validate Morse message formats +- Explain message steps +- Quiz mode to test Morse knowledge + +## Installation / Setup + + diff --git a/examples/demo.py b/examples/demo.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/demo.py @@ -0,0 +1 @@ + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6413373 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "morse_code" +version = "0.1.0" +description = "A Python package for encoding and decoding Morse code." +readme = "README.md" +requires-python = ">=3.10" +license = { file = "LICENSE" } +authors = [{ name = "Team Lumen" }] +keywords = ["python", "morse code", "encoder", "decoder"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent" +] + +[project.urls] +Homepage = "https://github.com/swe-students-fall2025/3-python-package-team_lumen" +Issues = "https://github.com/swe-students-fall2025/3-python-package-team_lumen/issues" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[project.scripts] +morseify = "morseify.cli:cli" diff --git a/src/morseify/__init__.py b/src/morseify/__init__.py new file mode 100644 index 0000000..7b188a8 --- /dev/null +++ b/src/morseify/__init__.py @@ -0,0 +1,27 @@ +""" +Morse Code Package +A Python package for encoding and decoding Morse code with additional utilities. +""" + +from morseify.core import ( + encode, + decode, + is_valid +) +from morseify.normalize import ( + normalize_text, + normalize_code +) +from morseify.explain import explain +from morseify.quiz import quiz + +__all__ = [ + 'encode', + 'decode', + 'is_valid', + 'normalize_text', + 'normalize_code', + 'explain', + 'quiz' +] + diff --git a/src/morseify/__main__.py b/src/morseify/__main__.py new file mode 100644 index 0000000..010cc90 --- /dev/null +++ b/src/morseify/__main__.py @@ -0,0 +1,46 @@ +from .core import encode, decode, is_valid +from .explain import explain +from .quiz import quiz + + +def main(): + print("Morse Code Encode/Decode Test") + # Test cases + test_cases = [ + "HELLO", + "HELLO WORLD", + "SOS", + "PYTHON", + "123", + "A1B2C3" + ] + + for text in test_cases: + encoded = encode(text) + decoded = decode(encoded) + + print(f"\nOriginal: '{text}'") + print(f"Encoded : '{encoded}'") + print(f"Decoded : '{decoded}'") + + # Check if round-trip works + if decoded == text: + print("✓ Round-trip successful!") + else: + print(f"✗ Round-trip failed! Expected: '{text}'") + + # Example of explain feature with user input + print("Explain Feature") + user_morse = input("Enter morse code to explain: ").strip() + if user_morse: + explanation = explain(user_morse) + print(explanation) + else: + print("No input provided. Using example:") + example_morse = ".... . .-.. .-.. --- / .-- --- .-. .-.. -.." + explanation = explain(example_morse) + print(explanation) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/morseify/cli.py b/src/morseify/cli.py new file mode 100644 index 0000000..296591b --- /dev/null +++ b/src/morseify/cli.py @@ -0,0 +1,41 @@ +""" +Command-line interface for morseify. +""" + +import sys +from morseify.core import encode, decode, is_valid + + +def cli(): + """ + Command-line interface for morseify. + Auto-detects whether input is text (encode) or morse code (decode). + + Usage: + morseify "HELLO" # Encodes text to morse code + morseify ".... . .-.. .-.. ---" # Decodes morse code to text + """ + if len(sys.argv) < 2: + print("Usage: morseify ") + print("Examples:") + print(' morseify "HELLO"') + print(' morseify ".... . .-.. .-.. ---"') + sys.exit(1) + + # Join all arguments in case user passes multiple words + input_text = ' '.join(sys.argv[1:]) + + # Auto-detect: if input contains only morse characters (., -, /, space), decode it + # Otherwise, encode it + valid_morse_chars = {'.', '-', '/', ' '} + is_morse = all(char in valid_morse_chars for char in input_text) and input_text.strip() + + if is_morse and is_valid(input_text): + # It's valid morse code, decode it + result = decode(input_text) + print(result) + else: + # It's text, encode it + result = encode(input_text) + print(result) + diff --git a/src/morseify/core.py b/src/morseify/core.py new file mode 100644 index 0000000..04c9343 --- /dev/null +++ b/src/morseify/core.py @@ -0,0 +1,112 @@ +from morseify.mapping import LETTER_TO_MORSE +from morseify.mapping import MORSE_TO_LETTER +from morseify.normalize import normalize_text +from morseify.normalize import normalize_code + +def encode(text): + """ + Convert English text to morse code. + + Args: + text: English text string to encode + + Returns: + Morse code string + """ + + # Normalize the text first(delete after normalize function is implemented) + # if text is None: + # return None + # normalized = text.upper().strip() + + normalized = normalize_text(text) + + # Convert each character to morse code + result = [] + for char in normalized: + if char == ' ': + # Space between words becomes ' / ' + result.append('/') + else: + # Get morse code for character + morse_char = LETTER_TO_MORSE.get(char, '') + if morse_char: + result.append(morse_char) + + # Join with spaces between letters + return ' '.join(result) + + +def decode(morse_code): + """ + Convert morse code to English text. + + Args: + morse_code: Morse code string to decode + + Returns: + English text string, or error message if morse code is invalid + """ + # Check if the morse code is valid + if not is_valid(morse_code): + return "Morse code is not valid" + + # If valid: normalize and decode + normalized = normalize_code(morse_code) + + # Split morse code by spaces to get individual morse sequences + morse_words = normalized.split(' ') + + result = [] + for i in morse_words: + if i == '': + # Skip empty sequences (multiple spaces) + continue + elif i == '/': + # Word separator + result.append(' ') + else: + # Look up the morse sequence in the dictionary + letter = MORSE_TO_LETTER.get(i, '') + result.append(letter) + + return ''.join(result) + + + + + + +def is_valid(morse_code): + """ + Check if morse code is valid. + + Args: + morse_code: Morse code string to check if valid + + Returns: + Boolean: True if valid, False otherwise + """ + + # check if morse_code is str or empty + try: + morse_code = morse_code.strip() + except AttributeError: + return False + + if not morse_code: + return False + + #check if valid chars or sequence + validchars = {'.', '-', '/', ' '} + if any(ch not in validchars for ch in morse_code): + return False + + for seq in morse_code.split(' '): + if not seq or seq == '/': + continue + if seq not in MORSE_TO_LETTER: + return False + return True + + diff --git a/src/morseify/explain.py b/src/morseify/explain.py new file mode 100644 index 0000000..f458604 --- /dev/null +++ b/src/morseify/explain.py @@ -0,0 +1,59 @@ +from morseify.mapping import MORSE_TO_LETTER, LETTER_TO_MORSE +from morseify.core import is_valid +from morseify.normalize import normalize_code + +def explain(morse_message): + + """ + WantToDo in explain.py + Provide a step-by-step breakdown of a morse code input. + + Args: + morse_message: Morse code string need to be explained + + Returns: + String containing the step-by-step breakdown + """ + #input validation + if not is_valid(morse_message): + return "Error: Invalid Morse code message. Please check the format." + + #morse code normalization + normalized = normalize_code(morse_message) + + #explainaion building + explanation = [] + explanation.append("=" * 50) + explanation.append("MORSE CODE BREAKDOWN") + explanation.append("=" * 50) + explanation.append(f"Input: {morse_message}") + + if normalized != morse_message.strip(): + explanation.append(f"Normalized: {normalized}") + + explanation.append("\nStep-by-step translation:") + explanation.append("-" * 30) + + #Process each morse sequence + sequences = normalized.split(' ') + result_chars = [] + + for seq in sequences: + if seq == '': + continue + elif seq == '/': + explanation.append(" / → [SPACE]") + result_chars.append(' ') + else: + letter = MORSE_TO_LETTER.get(seq, '?') + explanation.append(f" {seq:<8} → {letter}") + result_chars.append(letter) + + #show the final result + explanation.append("-" * 30) + explanation.append(f"\nFinal message: {(''.join(result_chars))}") + explanation.append("=" * 50) + + return '\n'.join(explanation) + + #pass \ No newline at end of file diff --git a/src/morseify/mapping.py b/src/morseify/mapping.py new file mode 100644 index 0000000..78e2a87 --- /dev/null +++ b/src/morseify/mapping.py @@ -0,0 +1,13 @@ +LETTER_TO_MORSE = { + "A": ".-", "B": "-...", "C": "-.-.", "D": "-..", "E": ".", + "F": "..-.", "G": "--.", "H": "....", "I": "..", "J": ".---", + "K": "-.-", "L": ".-..", "M": "--", "N": "-.", "O": "---", + "P": ".--.", "Q": "--.-", "R": ".-.", "S": "...", "T": "-", + "U": "..-", "V": "...-", "W": ".--", "X": "-..-", "Y": "-.--", + "Z": "--..", + "0": "-----", "1": ".----", "2": "..---", "3": "...--", "4": "....-", + "5": ".....", "6": "-....", "7": "--...", "8": "---..", "9": "----.", + ".": ".-.-.-", ",": "--..--", "?": "..--..", "/": "-..-.", "-": "-....-", + "(": "-.--.", ")": "-.--.-" +} +MORSE_TO_LETTER = {v: k for k, v in LETTER_TO_MORSE.items()} diff --git a/src/morseify/normalize.py b/src/morseify/normalize.py new file mode 100644 index 0000000..38d3392 --- /dev/null +++ b/src/morseify/normalize.py @@ -0,0 +1,56 @@ +import re +import string + +def normalize_text(text): + """ + Clean and standardize English text before encoding to Morse. + + Args: + text (str): Input text string + + Returns: + str: Normalized text ready for encoding + """ + if not isinstance(text, str): + return "" + # convert to uppercase + text = text.upper() + + # keep only supporting text + morse_punct = ".,?/-()" + allowed_chars = string.ascii_uppercase + string.digits + ' ' + morse_punct + cleaned = ''.join(ch for ch in text if ch in allowed_chars) + + # clean multiple or leading/trailing spaces + cleaned = re.sub(r'\s+', ' ', cleaned) + cleaned = cleaned.strip() + + return cleaned + + +def normalize_code(morse_code): + """ + Clean and standardize Morse code text before decoding. + + Args: + morse_code (str): Raw Morse code string + + Returns: + str: Normalized Morse code ready for decoding + """ + if not isinstance(morse_code, str): + return "" + + # replace any tabs with a space + morse_code = morse_code.replace('\t', ' ') + + # clean multiple spaces + morse_code = re.sub(r'\s+', ' ', morse_code) + + # clean multiple slashes + morse_code = re.sub(r'/+', '/', morse_code) + + # clean any leading/trailing space or slashes + morse_code = morse_code.strip(' /') + + return morse_code diff --git a/src/morseify/quiz.py b/src/morseify/quiz.py new file mode 100644 index 0000000..cac36a8 --- /dev/null +++ b/src/morseify/quiz.py @@ -0,0 +1,140 @@ +import random +import sys +from pathlib import Path + + +if __name__ == "__main__": + sys.path.insert(0, str(Path(__file__).parent.parent)) + +try: + from morseify.core import encode, decode, is_valid +except ModuleNotFoundError: + from .core import encode, decode, is_valid + +QUIZ_SENTENCES = [ + "HELLO WORLD", + "LET US TRY", + "MORSE CODE IS FUN", + "PYTHON ROCKS", + "LEARNING TAKES TIME", + "FORZA ROMA", + "SWE IS USEFUL", + "RUNNING OUT OF IDEAS", + "THIS SHOULD BE ENOUGH", + "I AM NOT VERY CREATIVE" +] + + +def quiz(mode=None): + """ + Interactive morse code quiz. + + Args: + mode: Optional - 'reading' or 'writing', or None to ask + """ + # Always use a random sentence + selected_sentence = random.choice(QUIZ_SENTENCES) + + + + if mode is None: + print("\n" + "=" * 60) + print("MORSE CODE QUIZ") + print("=" * 60) + print("Choose your mode:") + print("'reading' - Decode morse code → text") + print("'writing' - Encode text → morse code") + print() + mode = input("Enter the mode ('reading' or 'writing'): ").strip().lower() + + while mode not in ['reading', 'writing']: + print("\nInvalid mode!") + print("Please enter 'reading' or 'writing'") + mode = input("\nEnter the mode: ").strip().lower() + + print("\n" + "=" * 60) + if mode == 'reading': + print("READING MODE: Decode morse code → text") + print("=" * 60) + morse_to_decode = encode(selected_sentence) + print(f"\nMorse code: {morse_to_decode}") + print() + + while True: + answer = input("Your answer: ").upper().strip() + + if answer == selected_sentence: + print("\nCorrect! Well done!") + break + else: + print("\nIncorrect!") + retry = input("Try again? (yes/no): ").strip().lower() + + # Validate yes/no input + while retry not in ['yes', 'no']: + print("Please enter 'yes' or 'no'") + retry = input("Try again? (yes/no): ").strip().lower() + + if retry == 'yes': + + print(f"\nMorse code: {morse_to_decode}") + print() + continue # Try again + else: + print("\n" + "-" * 60) + print("ANSWER REVEALED") + print("-" * 60) + print(f"Your answer: {answer}") + print(f"Correct answer: {selected_sentence}") + print(f"\nThe morse code '{morse_to_decode}' translates to '{selected_sentence}'") + print("-" * 60) + break + + else: + print("WRITING MODE: Encode text → morse code") + print("=" * 60) + print(f"\nText to encode: {selected_sentence}") + print() + correct_morse = encode(selected_sentence) + + + while True: + answer = input("Your answer: ").strip() + + while not is_valid(answer): + print("\nInvalid morse code format!") + print("Please use only dots (.), dashes (-), spaces, and slashes (/)") + print("Example: ... --- ... (for SOS)") + answer = input("\nYour answer: ").strip() + + if answer == correct_morse: + print("\nCorrect! Well done!") + break + else: + print("\nIncorrect!") + retry = input("Try again? (yes/no): ").strip().lower() + + # Validate yes/no input + while retry not in ['yes', 'no']: + print("Please enter 'yes' or 'no'") + retry = input("Try again? (yes/no): ").strip().lower() + + if retry == 'yes': + print(f"\nText to encode: {selected_sentence}") + print() + continue + else: + print("\n" + "-" * 60) + print("ANSWER REVEALED") + print("-" * 60) + print(f"Your answer: {answer}") + print(f"Correct answer: {correct_morse}") + print(f"\n'{selected_sentence}' in morse code is: {correct_morse}") + print("-" * 60) + break + + print("=" * 60) + + +if __name__ == "__main__": + quiz() \ No newline at end of file diff --git a/tests/test_decode.py b/tests/test_decode.py new file mode 100644 index 0000000..7a5d4cb --- /dev/null +++ b/tests/test_decode.py @@ -0,0 +1,96 @@ +from morseify.core import decode + + +def test_decode_single_letter(): + """Test decoding a single letter.""" + assert decode(".-") == "A" + assert decode("-...") == "B" + assert decode("-.-.") == "C" + assert decode("-..") == "D" + assert decode(".") == "E" + assert decode("..-.") == "F" + assert decode("--.") == "G" + assert decode("....") == "H" + assert decode("..") == "I" + assert decode(".---") == "J" + assert decode("-.-") == "K" + assert decode(".-..") == "L" + assert decode("--") == "M" + assert decode("-.") == "N" + assert decode("---") == "O" + assert decode(".--.") == "P" + assert decode("--.-") == "Q" + assert decode(".-.") == "R" + assert decode("...") == "S" + assert decode("-") == "T" + assert decode("..-") == "U" + assert decode("...-") == "V" + assert decode(".--") == "W" + assert decode("-..-") == "X" + assert decode("-.--") == "Y" + assert decode("--..") == "Z" + + +def test_decode_word(): + """Test decoding a word.""" + assert decode(".... . .-.. .-.. ---") == "HELLO" + assert decode("... --- ...") == "SOS" + + +def test_decode_multiple_words(): + """Test decoding multiple words with separator.""" + assert decode(".... . .-.. .-.. --- / .-- --- .-. .-.. -..") == "HELLO WORLD" + assert decode("... --- ... / .... . .-.. .--.") == "SOS HELP" + + +def test_decode_numbers(): + """Test decoding numbers.""" + assert decode(".---- ..--- ...--") == "123" + assert decode("-----") == "0" + +def test_decode_extra_spaces(): + """Test decoding morse code with extra spaces.""" + assert decode(" ... --- ... ") == "SOS" + +def test_decode_invalid_morse_code(): + """Test decoding invalid morse code returns error message.""" + assert decode("---.") == "Morse code is not valid" # Invalid sequence not in mapping + assert decode("ABC") == "Morse code is not valid" # Contains invalid characters + assert decode("....X") == "Morse code is not valid" # Contains invalid character + assert decode("") == "Morse code is not valid" # Empty string + assert decode(" ") == "Morse code is not valid" # Only spaces + + +def test_decode_mixed_valid_invalid(): + """Test decoding morse code with invalid sequences.""" + assert decode(".... . .-.. .-.. --- ---.") == "Morse code is not valid" # Has invalid sequence + + +def test_decode_with_punctuation(): + """Test decoding morse code with punctuation.""" + assert decode(".... . .-.. .-.. --- .-.-.-") == "HELLO." + assert decode("-- ..- .-.. - .. .--. .-.. .") == "MULTIPLE" + + +def test_decode_normalized_input(): + """Test decoding already normalized morse code.""" + # decode should handle normalization internally + assert decode(" .... . .-.. .-.. --- ") == "HELLO" # Extra spaces + + + +def test_decode_punctuation_only(): + """Test decoding morse code with only punctuation.""" + assert decode(".-.-.-") == "." # period + assert decode("--..--") == "," # comma + assert decode("..--..") == "?" # question mark + assert decode("-..-.") == "/" # slash + assert decode("-....-") == "-" # hyphen + + +def test_decode_with_parentheses(): + """Test decoding morse code with parentheses.""" + assert decode("-.--. -.--.-") == "()" + assert decode(".... . .-.. .-.. --- -.--.") == "HELLO(" + assert decode("-.--.- .-- --- .-. .-.. -..") == ")WORLD" + diff --git a/tests/test_encode.py b/tests/test_encode.py new file mode 100644 index 0000000..be6716b --- /dev/null +++ b/tests/test_encode.py @@ -0,0 +1,141 @@ +from morseify.core import encode, decode + + +def test_encode_single_letter(): + """Test encoding a single letter.""" + assert encode("A") == ".-" + assert encode("B") == "-..." + assert encode("C") == "-.-." + assert encode("D") == "-.." + assert encode("E") == "." + assert encode("F") == "..-." + assert encode("G") == "--." + assert encode("H") == "...." + assert encode("I") == ".." + assert encode("J") == ".---" + assert encode("K") == "-.-" + assert encode("L") == ".-.." + assert encode("M") == "--" + assert encode("N") == "-." + assert encode("O") == "---" + assert encode("P") == ".--." + assert encode("Q") == "--.-" + assert encode("R") == ".-." + assert encode("S") == "..." + assert encode("T") == "-" + assert encode("U") == "..-" + assert encode("V") == "...-" + assert encode("W") == ".--" + assert encode("X") == "-..-" + assert encode("Y") == "-.--" + assert encode("Z") == "--.." + + +def test_encode_lowercase(): + """Test encoding lowercase letters (should convert to uppercase first).""" + assert encode("a") == ".-" + assert encode("hello") == ".... . .-.. .-.. ---" + assert encode("sos") == "... --- ..." + + +def test_encode_word(): + """Test encoding an uppercase word.""" + assert encode("HELLO") == ".... . .-.. .-.. ---" + assert encode("SOS") == "... --- ..." + +def test_encode_mixed_case(): + """Test encoding text with mixed case.""" + assert encode("Hello") == ".... . .-.. .-.. ---" + assert encode("World") == ".-- --- .-. .-.. -.." + assert encode("SoS") == "... --- ..." + + +def test_encode_multiple_words(): + """Test encoding multiple words with separator.""" + assert encode("HELLO WORLD") == ".... . .-.. .-.. --- / .-- --- .-. .-.. -.." + assert encode("SOS HELP") == "... --- ... / .... . .-.. .--." + + +def test_encode_numbers(): + """Test encoding numbers.""" + assert encode("123") == ".---- ..--- ...--" + assert encode("0") == "-----" + assert encode("0123456789") == "----- .---- ..--- ...-- ....- ..... -.... --... ---.. ----." + + +def test_encode_with_punctuation(): + """Test encoding text with punctuation.""" + assert encode("HELLO.") == ".... . .-.. .-.. --- .-.-.-" + assert encode("SOS?") == "... --- ... ..--.." + assert encode("A,B") == ".- --..-- -..." + + +def test_encode_with_parentheses(): + """Test encoding text with parentheses.""" + assert encode("()") == "-.--. -.--.-" + assert encode("HELLO(WORLD)") == ".... . .-.. .-.. --- -.--. .-- --- .-. .-.. -.. -.--.-" + assert encode("(A)") == "-.--. .- -.--.-" + + +def test_encode_with_extra_spaces(): + """Test encoding text with extra spaces.""" + assert encode(" HELLO ") == ".... . .-.. .-.. ---" + assert encode("A B") == ".- / -..." + assert encode(" SOS ") == "... --- ..." + + +def test_encode_only_spaces(): + """Test encoding only spaces.""" + assert encode(" ") == "" + assert encode(" ") == "" + assert encode(" ") == "" + + +def test_encode_empty_string(): + """Test encoding empty string.""" + assert encode("") == "" + + +def test_encode_decode_roundtrip(): + """Test that encoding then decoding returns the original text.""" + texts = [ + "A", + "HELLO", + "HELLO WORLD", + "123", + "SOS", + "HELLO.", + "SOS?", + "()", + "HELLO(WORLD)", + "0123456789" + ] + + for text in texts: + encoded = encode(text) + decoded = decode(encoded) + assert decoded == text, f"Round trip failed for '{text}': {encoded} -> {decoded}" + + +def test_decode_encode_roundtrip(): + """Test that decoding then encoding returns the original morse code.""" + morse_codes = [ + ".-", + "-", + "...", + ".... . .-.. .-.. ---", + "... --- ...", + ".... . .-.. .-.. --- / .-- --- .-. .-.. -..", + ".---- ..--- ...--", + "-----", + ".... . .-.. .-.. --- .-.-.-", + "... --- ... ..--..", + "-.--. -.--.-", + ".- .-.-.- -..." + ] + + for morse_code in morse_codes: + decoded = decode(morse_code) + encoded = encode(decoded) + assert encoded == morse_code, f"Round trip failed for '{morse_code}': {decoded} -> {encoded}" + \ No newline at end of file diff --git a/tests/test_explain.py b/tests/test_explain.py new file mode 100644 index 0000000..b14db2f --- /dev/null +++ b/tests/test_explain.py @@ -0,0 +1,68 @@ +import pytest +from morseify import explain + + +def test_explain_simple_message(): + """Test explaining a simple SOS message.""" + result = explain("... --- ...") + assert "SOS" in result + assert "..." in result + assert "---" in result + + +def test_explain_single_letter(): + """Test explaining a single letter.""" + result = explain(".-") #letter -- A + assert "A" in result + assert ".-" in result + + +def test_explain_word(): + """Test explaining a word.""" + result = explain(".... . .-.. .-.. ---") #answer is HELLO + assert "HELLO" in result + + +def test_explain_with_word_separator(): + """Test explaining message with word separator.""" + result = explain(".... .. / - .... . .-. .") #answer is HI THERE + assert "HI THERE" in result + assert "[SPACE]" in result or "/" in result + + +def test_explain_invalid_morse(): + """Test explaining invalid morse code.""" + result = explain("ABC123") #Invalid - contains letters + assert "Invalid" in result or "invalid" in result + + +def test_explain_empty_string(): + """Test explaining empty string.""" + result = explain("") + assert "Invalid" in result or "invalid" in result + + +def test_explain_with_numbers(): + """Test explaining morse code with numbers.""" + result = explain(".---- ..--- ...--") # 123 + assert "123" in result + + +def test_explain_shows_input(): + """Test that explanation shows the input.""" + morse_input = "... --- ..." + result = explain(morse_input) + assert morse_input in result or "Input" in result + + +def test_explain_shows_result(): + """Test that explanation shows the final result.""" + result = explain("... --- ...") + assert "Result" in result or "SOS" in result + + +def test_explain_with_extra_spaces(): + """Test explaining morse code with extra spaces.""" + result = explain(" ... --- ... ") + # Should still decode to SOS + assert "SOS" in result \ No newline at end of file diff --git a/tests/test_is_valid.py b/tests/test_is_valid.py new file mode 100644 index 0000000..8e33608 --- /dev/null +++ b/tests/test_is_valid.py @@ -0,0 +1,61 @@ +import pytest +from morseify.core import is_valid + +def test_valid_single(): + assert is_valid("....") is True + assert is_valid(".") is True + assert is_valid("-.--") is True + + assert is_valid("-...") is True + assert is_valid(".-.") is True + assert is_valid("---") is True + +def test_valid_multiple(): + hey = ".... . -.--" + bro = "-... .-. ---" + i = '..' + want = ".-- .- -. -" + a = ".-" + cat = "-.-. .- -" + assert is_valid(hey) is True + assert is_valid(bro) is True + assert is_valid(i) is True + assert is_valid(want) is True + assert is_valid(a) is True + assert is_valid(cat) is True + +def test_valid_separator(): + morse1 = ".... . -.-- / -... .-. ---" + morse2 = ".. / .-- .- -. - / .- / -.-. .- -" + assert is_valid(morse1) is True + assert is_valid(morse2) is True + +def test_invalid_morse_char(): + assert is_valid("x") is False + assert is_valid(".... x ...") is False + assert is_valid(".... & ...") is False + +def test_emptywhite_space(): + assert is_valid("") is False + assert is_valid(" ") is False + assert is_valid(None) is False + +def test_valid_morse_extraspace(): + morse = ".... . .-.. .-.. --- / .-- --- .-. .-.. -.." + assert is_valid(morse) is True + +def test_invalid_morse_seq(): + assert is_valid("...---...-") is False + +def test_slashspace_only(): + assert is_valid("/") is True + assert is_valid(" / ") is True + +def test_mix_validinvalid_seq(): + assert is_valid(".- / x") is False + assert is_valid(".- / .--.-") is False + +def test_multiple_separators(): + assert is_valid(".... / ....") is True + assert is_valid(".... ....") is True + assert is_valid(".... // ....") is False \ No newline at end of file diff --git a/tests/test_normalize_code.py b/tests/test_normalize_code.py new file mode 100644 index 0000000..abcd8d4 --- /dev/null +++ b/tests/test_normalize_code.py @@ -0,0 +1,2 @@ +def test_normalize_code(): + assert True \ No newline at end of file diff --git a/tests/test_normalize_text.py b/tests/test_normalize_text.py new file mode 100644 index 0000000..14519ca --- /dev/null +++ b/tests/test_normalize_text.py @@ -0,0 +1,2 @@ +def test_normalize_text(): + assert True \ No newline at end of file diff --git a/tests/test_quiz.py b/tests/test_quiz.py new file mode 100644 index 0000000..bd120f2 --- /dev/null +++ b/tests/test_quiz.py @@ -0,0 +1,181 @@ +import pytest +from morseify.quiz import quiz, QUIZ_SENTENCES +from morseify.core import encode, decode + + +class Tests: + """Test suite for the quiz module.""" + + # + # Fixtures - these are functions that can do any optional setup or teardown before or after a test function is run. + # + + @pytest.fixture + def example_fixture(self): + """ + An example of a pytest fixture - a function that can be used for setup and teardown before and after test functions are run. + """ + # place any setup you want to do before any test function that uses this fixture is run + yield # at the yield point, the test function will run and do its business + # place with any teardown you want to do after any test function that uses this fixture has completed + + # + # Test functions + # + + def test_sanity_check(self, example_fixture): + """ + Test debugging... making sure that we can run a simple test that always passes. + Note the use of the example_fixture in the parameter list - any setup and teardown in that fixture will be run before and after this test function executes + From the main project directory, run the `python3 -m pytest` command to run all tests. + """ + expected = True # the value we expect to be present + actual = True # the value we see in reality + assert actual == expected, "Expected True to be equal to True!" + + def test_quiz_sentences_list_exists(self): + """ + Verify that QUIZ_SENTENCES list is defined and not empty. + """ + assert QUIZ_SENTENCES is not None, "QUIZ_SENTENCES should not be None" + assert len(QUIZ_SENTENCES) > 0, "QUIZ_SENTENCES should not be empty" + assert all(isinstance(s, str) for s in QUIZ_SENTENCES), "All sentences should be strings" + + def test_quiz_sentences_are_uppercase(self): + """ + Verify that all quiz sentences are uppercase. + """ + for sentence in QUIZ_SENTENCES: + assert sentence == sentence.upper(), f"Sentence '{sentence}' should be uppercase" + + def test_quiz_sentences_content(self): + """ + Verify that QUIZ_SENTENCES contains expected sentences. + """ + assert len(QUIZ_SENTENCES) >= 5, f"Expected at least 5 sentences, found {len(QUIZ_SENTENCES)}" + + assert any("HELLO" in s or "SOS" in s or "PYTHON" in s for s in QUIZ_SENTENCES), \ + "Expected to find common words like HELLO, SOS, or PYTHON in sentences" + + def test_quiz_sentences_no_duplicates(self): + """ + Verify that there are no duplicate sentences in QUIZ_SENTENCES. + """ + unique_sentences = set(QUIZ_SENTENCES) + assert len(unique_sentences) == len(QUIZ_SENTENCES), "QUIZ_SENTENCES should not contain duplicates" + + def test_quiz_sentences_valid_characters(self): + """ + Verify that all sentences contain only valid characters for morse code. + """ + valid_chars = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .,?/-()") + + for sentence in QUIZ_SENTENCES: + sentence_chars = set(sentence) + assert sentence_chars.issubset(valid_chars), \ + f"Sentence '{sentence}' contains invalid characters: {sentence_chars - valid_chars}" + + def test_quiz_sentences_can_be_encoded(self): + """ + Verify that all quiz sentences can be successfully encoded to morse code. + Since encode returns a morse code string, run it for all sentences and verify the output. + """ + for sentence in QUIZ_SENTENCES: + morse = encode(sentence) + assert isinstance(morse, str), f"Expected encode('{sentence}') to return a string. Instead, it returned {morse}" + assert len(morse) > 0, f"Expected encode('{sentence}') not to be empty. Instead, it returned a string with {len(morse)} characters" + + def test_quiz_sentences_roundtrip(self): + """ + Verify that all quiz sentences can be encoded and decoded back correctly. + Make sure that encoding then decoding returns the original text. + """ + for sentence in QUIZ_SENTENCES: + morse = encode(sentence) + decoded = decode(morse) + assert decoded == sentence, \ + f"Round trip failed for '{sentence}': encoded to '{morse}', decoded to '{decoded}'" + + def test_quiz_function_exists(self): + """ + Verify that the quiz function exists and is callable. + """ + assert quiz is not None, "quiz function should exist" + assert callable(quiz), "quiz should be a callable function" + + def test_quiz_function_parameters(self): + """ + Verify that quiz function has the expected parameters. + """ + import inspect + sig = inspect.signature(quiz) + params = list(sig.parameters.keys()) + + assert 'mode' in params, "quiz should have 'mode' parameter" + assert 'sentence' not in params, "quiz should not have 'sentence' parameter (always random)" + + def test_quiz_integration_with_encode(self): + """ + Verify that quiz properly integrates with encode function. + Test that encoding works for various test sentences. + """ + test_cases = [ + ("HELLO", ".... . .-.. .-.. ---"), + ("SOS", "... --- ..."), + ("TEST", "- . ... -"), + ] + + for text, expected_morse in test_cases: + actual = encode(text) + assert actual == expected_morse, \ + f"Expected encode('{text}') to return '{expected_morse}', got '{actual}'" + + def test_quiz_sentences_length_variety(self): + """ + Verify that quiz sentences have variety in length. + """ + lengths = [len(s) for s in QUIZ_SENTENCES] + + # Check that we have both short and long sentences + assert min(lengths) < 15, "Expected some short sentences (< 15 chars)" + assert max(lengths) > 10, "Expected some longer sentences (> 10 chars)" + + def test_quiz_sentences_word_count(self): + """ + Verify that quiz sentences have varying word counts. + """ + for sentence in QUIZ_SENTENCES: + words = sentence.split() + assert len(words) > 0, f"Sentence '{sentence}' should have at least one word" + assert len(words) <= 10, f"Sentence '{sentence}' should not be too long (max 10 words)" + + def test_quiz_sentences_morse_conversion(self): + """ + Verify morse code conversion for all quiz sentences produces valid output. + """ + valid_morse_chars = set(".- /") + + for sentence in QUIZ_SENTENCES: + morse = encode(sentence) + morse_chars = set(morse) + assert morse_chars.issubset(valid_morse_chars), \ + f"Morse code for '{sentence}' contains invalid characters: {morse_chars - valid_morse_chars}" + + def test_quiz_random_sentence_selection(self): + """ + Verify that we can randomly select sentences from QUIZ_SENTENCES. + """ + import random + + for _ in range(100): + selected = random.choice(QUIZ_SENTENCES) + assert selected in QUIZ_SENTENCES, \ + f"Random selection '{selected}' not in QUIZ_SENTENCES" + + def test_quiz_specific_sentences_content(self): + """ + Verify specific expected sentences are in the quiz list. + """ + common_phrases = ["HELLO WORLD", "PYTHON ROCKS", "SWE IS USEFUL"] + found_common = any(phrase in QUIZ_SENTENCES for phrase in common_phrases) + assert found_common, f"Expected at least one common phrase from {common_phrases}"