Skip to content

Commit

Permalink
Merge pull request #3 from xin-huang/dev
Browse files Browse the repository at this point in the history
Add CI
  • Loading branch information
xin-huang authored Nov 11, 2024
2 parents a142c19 + 4986275 commit 92397d7
Show file tree
Hide file tree
Showing 16 changed files with 449 additions and 5 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[report]
omit = tests/*,setup.py,*__init__.py,*conftest.py
35 changes: 35 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: build

on: [push, pull_request]

jobs:
build-linux:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10"]
max-parallel: 5

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Add micromamba to system path
uses: mamba-org/setup-micromamba@v1
with:
environment-name: sai
environment-file: build-env.yaml
- name: Test with pytest
run: |
micromamba run -n sai pip install -e .
micromamba run -n sai pytest --cov=. --cov-report term-missing -vv
micromamba run -n sai coverage xml
- name: upload coverage report to codecov
uses: codecov/codecov-action@v4
with:
name: codecov-umbrella
fail_ci_if_error: true
env_vars: OS,PYTHON
token: ${{ secrets.CODECOV_TOKEN }}
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
# sai
# SAI

[![license](https://img.shields.io/badge/license-GPL%20v3-black.svg?style=flat-square)](LICENSE)
[![language](http://img.shields.io/badge/language-Python-blue.svg?style=flat-square)](https://www.python.org/)
[![build status](https://img.shields.io/github/actions/workflow/status/xin-huang/sai/build.yaml?branch=main&style=flat-square)](https://github.com/xin-huang/sai/actions)
[![codecov](https://img.shields.io/codecov/c/github/xin-huang/sai)](https://app.codecov.io/gh/xin-huang/sai)

A Python Package for **S**tatistics for **A**daptive **I**ntrogression

The manual can be found [here](https://xin-huang.github.io/sai).
1 change: 1 addition & 0 deletions build-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ dependencies:
- pip=24.0
- python=3.9.19
- pytest=8.1.1
- pytest-cov=6.0.0
- scikit-allel=1.3.7
- scipy=1.12.0
1 change: 1 addition & 0 deletions dev-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ dependencies:
- pip=24.0
- python=3.9.19
- pytest=8.1.1
- pytest-cov=6.0.0
- scikit-allel=1.3.7
- scipy=1.12.0
5 changes: 5 additions & 0 deletions tests/data/example.ref.ind.list
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
AFR ind1
AFR ind2
AFR ind3
AFR ind4
AFR ind5
2 changes: 2 additions & 0 deletions tests/data/example.scores
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Chrom Start End Ref Tgt Src U Q95
21 0 3333 AFR CHB Nean 3 0.9
1 change: 1 addition & 0 deletions tests/data/example.src.ind.list
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Nean ind11
5 changes: 5 additions & 0 deletions tests/data/example.tgt.ind.list
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CHB ind6
CHB ind7
CHB ind8
CHB ind9
CHB ind10
19 changes: 19 additions & 0 deletions tests/data/example.vcf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
##fileformat=VCFv4.1
##FILTER=<ID=PASS,Description="All filters passed">
##contig=<ID=21,assembly=b37,length=48129895>
##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
##INFO=<ID=AA,Number=1,Type=String,Description="Ancestral Allele. Format: AA|REF|ALT|IndelType. AA: Ancestral allele, REF:Reference Allele, ALT:Alternate Allele, IndelType:Type of Indel (REF, ALT and IndelType are only defined for indels)">
##INFO=<ID=VT,Number=.,Type=String,Description="indicates what type of variant the line represents">
#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT ind1 ind2 ind3 ind4 ind5 ind6 ind7 ind8 ind9 ind10 ind11
21 111 . A T 100 PASS AA=A GT 0|0 0|0 0|0 0|0 0|0 1|0 0|0 0|0 0|0 0|0 0|0
21 222 . A T 100 PASS AA=A GT 0|0 0|0 1|1 0|0 0|0 0|1 1|1 1|1 1|1 1|1 1|1
21 333 . A T 100 PASS AA=A GT 0|1 0|0 0|0 0|0 0|0 0|0 0|1 1|1 1|1 1|1 1|1
21 444 . A T 100 PASS AA=A GT 0|0 0|0 0|0 0|0 0|0 0|1 1|1 1|1 1|1 1|1 0|0
21 555 . A T 100 PASS AA=A GT 0|0 0|0 1|1 1|0 0|1 0|0 0|0 0|0 0|0 0|0 0|0
21 666 . A T 100 PASS AA=A GT 0|0 0|0 0|0 0|0 0|0 1|0 0|0 0|0 0|0 0|0 0|0
21 777 . A T 100 PASS AA=A GT 0|0 0|0 0|0 0|0 0|0 0|1 1|1 1|1 1|1 1|1 1|1
21 888 . A T 100 PASS AA=A GT 0|0 0|0 0|0 0|0 0|0 0|0 0|0 0|0 0|0 0|1 1|1
21 999 . A T 100 PASS AA=A GT 0|0 1|1 0|0 0|0 0|0 0|0 0|0 0|0 0|0 0|0 0|0
21 1111 . A T 100 PASS AA=A GT 0|1 1|1 1|1 1|1 1|1 0|0 0|0 0|0 0|0 0|0 1|1
21 2222 . A T 100 PASS AA=A GT 0|0 0|0 0|0 0|0 0|0 0|0 0|0 0|0 0|1 1|1 1|1
21 3333 . A T 100 PASS AA=A GT 1|1 1|1 0|0 0|0 0|1 0|0 0|0 0|0 0|0 0|0 1|0
100 changes: 100 additions & 0 deletions tests/parsers/test_argument_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright 2024 Xin Huang
#
# GNU General Public License v3.0
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, please see
#
# https://www.gnu.org/licenses/gpl-3.0.en.html


import argparse
import pytest
import os
from sai.parsers.argument_validation import positive_int
from sai.parsers.argument_validation import positive_number
from sai.parsers.argument_validation import between_zero_and_one
from sai.parsers.argument_validation import existed_file


def test_positive_int():
# Valid positive integer
assert positive_int("5") == 5

# Not a positive integer (zero)
with pytest.raises(argparse.ArgumentTypeError, match="0 is not a positive integer"):
positive_int("0")

# Negative integer
with pytest.raises(
argparse.ArgumentTypeError, match="-1 is not a positive integer"
):
positive_int("-1")

# Non-integer string
with pytest.raises(argparse.ArgumentTypeError, match="abc is not a valid integer"):
positive_int("abc")


def test_positive_number():
# Valid positive number
assert positive_number("3.14") == 3.14

# Not a positive number (zero)
with pytest.raises(argparse.ArgumentTypeError, match="0 is not a positive number"):
positive_number("0")

# Negative number
with pytest.raises(
argparse.ArgumentTypeError, match="-2.5 is not a positive number"
):
positive_number("-2.5")

# Non-numeric string
with pytest.raises(argparse.ArgumentTypeError, match="xyz is not a valid number"):
positive_number("xyz")


def test_between_zero_and_one():
# Values within range
assert between_zero_and_one("0.5") == 0.5
assert between_zero_and_one("0") == 0
assert between_zero_and_one("1") == 1

# Values out of range
with pytest.raises(argparse.ArgumentTypeError, match="1.5 is not between 0 and 1"):
between_zero_and_one("1.5")

with pytest.raises(argparse.ArgumentTypeError, match="-0.1 is not between 0 and 1"):
between_zero_and_one("-0.1")

# Non-numeric string
with pytest.raises(
argparse.ArgumentTypeError, match="not_a_number is not a valid number"
):
between_zero_and_one("not_a_number")


def test_existed_file(tmp_path):
# Create a temporary file for testing
temp_file = tmp_path / "temp.txt"
temp_file.write_text("This is a test file.")

# Validate an existing file path
assert existed_file(str(temp_file)) == str(temp_file)

# Validate a non-existent file path
with pytest.raises(
argparse.ArgumentTypeError, match="non_existent_file is not found"
):
existed_file("non_existent_file")
56 changes: 56 additions & 0 deletions tests/parsers/test_outlier_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2024 Xin Huang
#
# GNU General Public License v3.0
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, please see
#
# https://www.gnu.org/licenses/gpl-3.0.en.html


import pytest
import argparse
from sai.parsers.outlier_parser import add_outlier_parser


@pytest.fixture
def parser():
# Initialize the argument parser with a subparser for the 'outlier' command
main_parser = argparse.ArgumentParser()
subparsers = main_parser.add_subparsers(dest="command")
add_outlier_parser(subparsers)
return main_parser


def test_add_outlier_parser(parser):
# Simulate command-line arguments to parse
args = parser.parse_args(
[
"outlier",
"--score",
"tests/data/example.scores",
"--output-dir",
"output/",
"--output-prefix",
"test_outliers",
"--quantile",
"0.95",
]
)

# Validate parsed arguments
assert args.command == "outlier"
assert args.score == "tests/data/example.scores"
assert args.output_dir == "output/"
assert args.output_prefix == "test_outliers"
assert args.quantile == 0.95
95 changes: 95 additions & 0 deletions tests/parsers/test_score_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Copyright 2024 Xin Huang
#
# GNU General Public License v3.0
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, please see
#
# https://www.gnu.org/licenses/gpl-3.0.en.html


import pytest
import argparse
from sai.parsers.score_parser import add_score_parser


@pytest.fixture
def parser():
# Initialize the argument parser with a subparser for the 'score' command
main_parser = argparse.ArgumentParser()
subparsers = main_parser.add_subparsers(dest="command")
add_score_parser(subparsers)
return main_parser


def test_add_score_parser(parser):
# Simulate command-line arguments to parse
args = parser.parse_args(
[
"score",
"--vcf",
"tests/data/example.vcf",
"--chr-name",
"chr1",
"--ref",
"tests/data/example.ref.ind.list",
"--tgt",
"tests/data/example.tgt.ind.list",
"--src",
"tests/data/example.src.ind.list",
"--win-len",
"50000",
"--win-step",
"10000",
"--num-src",
"2",
"--anc-allele-file",
"tests/data/test.anc.allele.bed",
"--ploidy",
"2",
"--phased",
"--w",
"0.3",
"--x",
"0.5",
"--y",
"0.1",
"0.2",
"--output",
"output/results.tsv",
"--q",
"0.95",
"--workers",
"4",
]
)

# Validate parsed arguments
assert args.command == "score"
assert args.vcf == "tests/data/example.vcf"
assert args.chr_name == "chr1"
assert args.ref == "tests/data/example.ref.ind.list"
assert args.tgt == "tests/data/example.tgt.ind.list"
assert args.src == "tests/data/example.src.ind.list"
assert args.win_len == 50000
assert args.win_step == 10000
assert args.num_src == 2
assert args.anc_allele_file == "tests/data/test.anc.allele.bed"
assert args.ploidy == 2
assert args.is_phased is True
assert args.w == 0.3
assert args.x == 0.5
assert args.y == [0.1, 0.2]
assert args.output == "output/results.tsv"
assert args.q == 0.95
assert args.workers == 4
34 changes: 34 additions & 0 deletions tests/test___main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,37 @@
# along with this program. If not, please see
#
# https://www.gnu.org/licenses/gpl-3.0.en.html


import pytest
from unittest.mock import patch, MagicMock
from sai.__main__ import main


@patch("sai.__main__._sai_cli_parser") # Mock _sai_cli_parser to control its output
@patch(
"sai.__main__._set_sigpipe_handler"
) # Mock _set_sigpipe_handler as it doesn’t need testing
def test_main(mock_set_sigpipe_handler, mock_sai_cli_parser):
# Mock parser and its return values
mock_parser = MagicMock()
mock_args = MagicMock()
mock_args.runner = MagicMock()

# Configure _sai_cli_parser to return the mock parser
mock_sai_cli_parser.return_value = mock_parser
# Configure the mock parser to return mock_args when parse_args is called
mock_parser.parse_args.return_value = mock_args

# Call the main function with a test argument list
test_args = ["score", "--vcf", "tests/data/example.vcf", "--chr-name", "chr1"]
main(test_args)

# Check if _set_sigpipe_handler was called
mock_set_sigpipe_handler.assert_called_once()

# Verify parse_args was called with test_args
mock_parser.parse_args.assert_called_once_with(test_args)

# Ensure runner was called with the parsed arguments
mock_args.runner.assert_called_once_with(mock_args)
Loading

0 comments on commit 92397d7

Please sign in to comment.