Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 0 additions & 62 deletions .github/workflows/ci.yml

This file was deleted.

17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,20 @@
.DS_Store
__pycache__/
*.pyc
*.pyo
.env
.venv/
venv/
.eggs/
*.egg-info/
*.egg
dist/
build/
.tox/
.mypy_cache/
.pytest_cache/
.ruff_cache/
htmlcov/
.cobertura.xml
coverage.xml
*.orig
31 changes: 31 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "sublime-sema"
version = "0.1.0"
description = "Sema support for Sublime Text"
license = { text = "MIT" }
requires-python = ">=3.8"
readme = "README.md"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
addopts = "-v"

[tool.ruff]
target-version = "py38"
line-length = 100
177 changes: 177 additions & 0 deletions tests/test_eval_helpers_extended.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""Extended tests for sema_eval pure helpers.

These test functions that don't require the Sublime plugin host or LSP package.
"""
import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

import sema_eval as se


class FormatResultExtendedTests(unittest.TestCase):
"""Test format_result edge cases not covered in test_eval_command.py."""

def test_format_result_with_stderr_only(self):
text = se.format_result({
"ok": True, "value": None, "stdout": "",
"stderr": "warning: deprecated function", "elapsedMs": 15,
})
self.assertIn("warning: deprecated function", text)
self.assertIn("=> nil", text)

def test_format_result_no_elapsed_ms(self):
text = se.format_result({
"ok": True, "value": "done", "stdout": "", "stderr": "",
})
self.assertIn("=> done", text)
self.assertNotIn("ms", text)

def test_format_result_empty_output_nil(self):
text = se.format_result({
"ok": True, "value": None, "stdout": "", "stderr": "", "elapsedMs": 0,
})
self.assertIn("=> nil", text)

def test_format_result_structured_error_no_hint(self):
text = se.format_result({
"ok": False, "value": None, "stdout": "", "stderr": "",
"error": {"message": "syntax error"},
"elapsedMs": 0,
})
self.assertIn("error: syntax error", text)
self.assertNotIn("hint", text)

def test_format_result_with_line_col_no_hint(self):
text = se.format_result({
"ok": False, "value": None, "stdout": "", "stderr": "",
"error": {"message": "type mismatch", "line": 10, "col": 5},
"elapsedMs": 0,
})
self.assertIn("error at line 10, col 5: type mismatch", text)

def test_format_result_raw_json_decode_failure(self):
# Simulates when sema eval returns non-JSON output
# This tests the ValueError branch in _evaluate
# We can't easily trigger this without mocking subprocess,
# but we verify the error path logic exists
result = se._format_error("some raw error string")
self.assertEqual(result, "error: some raw error string")


class FormatProcessErrorTests(unittest.TestCase):
"""Test format_process_error edge cases."""

def test_format_process_error_with_timeout(self):
result = se.format_process_error("timed out after 15s")
self.assertIn("timed out", result)
self.assertIn("PATH", result)

def test_format_process_error_with_generic(self):
result = se.format_process_error("permission denied")
self.assertIn("permission denied", result)
self.assertIn("sema eval", result)


class AugmentedPathTests(unittest.TestCase):
"""Test augmented_path edge cases."""

def test_augmented_path_empty_current(self):
result = se.augmented_path("", "/home/u").split(":")
# Should still have common dirs, no empty entry
self.assertNotIn("", result)
self.assertIn("/home/u/.cargo/bin", result)

def test_augmented_path_preserves_order(self):
result = se.augmented_path("/usr/local/bin:/usr/bin", "/home/u").split(":")
# Original dirs should come first
self.assertEqual(result[0], "/usr/local/bin")
self.assertEqual(result[1], "/usr/bin")
# Common dirs appended after
common_dirs = ["/home/u/.cargo/bin", "/home/u/.local/bin"]
first_common_idx = min(result.index(d) for d in common_dirs)
self.assertGreater(first_common_idx, 1)

def test_augmented_path_tilde_expansion(self):
result = se.augmented_path("", "/home/myuser").split(":")
self.assertIn("/home/myuser/.cargo/bin", result)
self.assertIn("/home/myuser/.local/bin", result)


class BuildEvalArgvTests(unittest.TestCase):
"""Test build_eval_argv edge cases."""

def test_build_eval_argv_custom_timeout(self):
result = se.build_eval_argv("sema", None, 30000)
self.assertEqual(result, ["sema", "eval", "--stdin", "--json", "--timeout", "30000"])

def test_build_eval_argv_zero_timeout(self):
result = se.build_eval_argv("sema", "/test.sema", 0)
self.assertEqual(
result,
["sema", "eval", "--stdin", "--json", "--timeout", "0", "--path", "/test.sema"],
)

def test_build_eval_argv_path_with_spaces(self):
result = se.build_eval_argv("sema", "/path with spaces/file.sema")
self.assertIn("/path with spaces/file.sema", result)


class ChooseSourceTests(unittest.TestCase):
"""Test choose_source edge cases."""

def test_choose_source_empty_buffer_empty_selection(self):
self.assertEqual(se.choose_source(["", ""], ""), "")

def test_choose_source_single_empty_selection(self):
self.assertEqual(se.choose_source([""], "buffer"), "buffer")

def test_choose_source_multiple_with_empty(self):
self.assertEqual(se.choose_source(["", "(a)", "", "(b)"], "buffer"), "(a)\n(b)")

def test_choose_source_whitespace_only_selection(self):
self.assertEqual(se.choose_source([" ", "\t"], "buffer"), "buffer")


class ResolveSemaTests(unittest.TestCase):
"""Test resolve_sema behavior."""

def test_resolve_sema_real_binary_returns_path(self):
# On a system with a python binary, this should find it
result = se.resolve_sema("python3")
# python3 should be on PATH in CI
self.assertIsNotNone(result)

def test_resolve_sema_nonexistent(self):
self.assertIsNone(se.resolve_sema("this-binary-definitely-does-not-exist-xyz123"))


class ProcessTimeoutTests(unittest.TestCase):
"""Verify timeout relationship."""

def test_process_timeout_is_grace_period(self):
# PROCESS_TIMEOUT_S should be DEFAULT_TIMEOUT_MS/1000 + 5
expected = se.DEFAULT_TIMEOUT_MS / 1000 + 5
self.assertEqual(se.PROCESS_TIMEOUT_S, expected)

def test_process_timeout_positive(self):
self.assertGreater(se.PROCESS_TIMEOUT_S, 0)

def test_default_timeout_positive(self):
self.assertGreater(se.DEFAULT_TIMEOUT_MS, 0)


class StartupInfoTests(unittest.TestCase):
"""Test _startupinfo_kwargs."""

def test_startupinfo_returns_empty_on_unix(self):
import os
if os.name != "nt":
result = se._startupinfo_kwargs()
self.assertEqual(result, {})


if __name__ == "__main__":
unittest.main()