Syed Arfan commited on
Commit
08367f3
·
1 Parent(s): 5fb7b67

Add GitHub Actions CI/CD pipeline

Browse files

- Automatically run tests on every push
- Test on Python 3.11 in clean environment
- Verify Docker build succeeds
- Run on main branch pushes and pull requests
- Provides automated quality gate

.github/workflows/test.yml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GitHub Actions Workflow for Sentiment API
2
+ # This runs automatically on every push and pull request
3
+ # Ensures code quality through automated testing
4
+
5
+ name: Test and Build
6
+
7
+ # When to run this workflow
8
+ on:
9
+ push:
10
+ branches: [ main ] # Run on pushes to main
11
+ pull_request:
12
+ branches: [ main ] # Run on PRs to main
13
+
14
+ # Define the jobs to run
15
+ jobs:
16
+ test:
17
+ name: Run Tests
18
+ runs-on: ubuntu-latest # Use Ubuntu Linux environment
19
+
20
+ steps:
21
+ # Step 1: Get the code from repository
22
+ - name: Checkout code
23
+ uses: actions/checkout@v4
24
+
25
+ # Step 2: Set up Python environment
26
+ - name: Set up Python 3.11
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version: '3.11'
30
+
31
+ # Step 3: Install dependencies
32
+ - name: Install dependencies
33
+ run: |
34
+ python -m pip install --upgrade pip
35
+ pip install -r requirements.txt
36
+
37
+ # Step 4: Run tests with pytest
38
+ - name: Run tests
39
+ run: |
40
+ pytest tests/ -v --tb=short
41
+
42
+ # Step 5: Verify Docker build works
43
+ - name: Test Docker build
44
+ run: |
45
+ docker build -t sentiment-api:test .
requirements.txt CHANGED
@@ -4,4 +4,8 @@ transformers==4.46.3
4
  torch==2.5.1
5
  pydantic==2.10.3
6
  pytest==8.3.4
7
- httpx==0.28.1
 
 
 
 
 
4
  torch==2.5.1
5
  pydantic==2.10.3
6
  pytest==8.3.4
7
+ httpx==0.28.1
8
+
9
+ # Testing
10
+ pytest==8.3.4
11
+ pytest-cov==6.0.0
src/__init__.py ADDED
File without changes
tests/__init__.py ADDED
File without changes
tests/test_api.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Unit tests for sentiment analysis API
3
+
4
+ These tests verify:
5
+ - All endpoints work correctly
6
+ - Sentiment analysis returns expected results
7
+ - Input validation catches errors
8
+ - Error handling works properly
9
+ """
10
+
11
+ from fastapi.testclient import TestClient
12
+ from src.main import app
13
+ import pytest
14
+
15
+ # Create test client
16
+ client = TestClient(app)
17
+
18
+
19
+ # ============================================
20
+ # Health Check Tests
21
+ # ============================================
22
+
23
+ def test_root_endpoint():
24
+ """Test the root endpoint returns health status"""
25
+ response = client.get("/")
26
+
27
+ assert response.status_code == 200
28
+ data = response.json()
29
+ assert data["status"] == "healthy"
30
+ assert data["service"] == "sentiment-api"
31
+ assert "version" in data
32
+
33
+
34
+ def test_health_endpoint():
35
+ """Test the /health endpoint for Kubernetes"""
36
+ response = client.get("/health")
37
+
38
+ assert response.status_code == 200
39
+ assert response.json() == {"status": "ok"}
40
+
41
+
42
+ # ============================================
43
+ # Sentiment Analysis Tests
44
+ # ============================================
45
+
46
+ def test_analyze_positive_sentiment():
47
+ """Test sentiment analysis with clearly positive text"""
48
+ response = client.post(
49
+ "/analyze",
50
+ json={"text": "I absolutely love this product! It's amazing and wonderful!"}
51
+ )
52
+
53
+ assert response.status_code == 200
54
+ data = response.json()
55
+
56
+ # Verify response structure
57
+ assert "text" in data
58
+ assert "sentiment" in data
59
+ assert "confidence" in data
60
+ assert "processing_time_ms" in data
61
+
62
+ # Verify sentiment detection
63
+ assert data["sentiment"] == "POSITIVE"
64
+ assert data["confidence"] > 0.9 # Should be very confident
65
+ assert data["processing_time_ms"] > 0 # Should take some time
66
+
67
+
68
+ def test_analyze_negative_sentiment():
69
+ """Test sentiment analysis with clearly negative text"""
70
+ response = client.post(
71
+ "/analyze",
72
+ json={"text": "This is terrible, horrible, and awful. I hate it."}
73
+ )
74
+
75
+ assert response.status_code == 200
76
+ data = response.json()
77
+
78
+ assert data["sentiment"] == "NEGATIVE"
79
+ assert data["confidence"] > 0.9
80
+
81
+
82
+ def test_analyze_neutral_text():
83
+ """Test sentiment analysis with neutral text"""
84
+ response = client.post(
85
+ "/analyze",
86
+ json={"text": "The item is blue."}
87
+ )
88
+
89
+ assert response.status_code == 200
90
+ data = response.json()
91
+
92
+ # Neutral text might be classified as either POSITIVE or NEGATIVE
93
+ # with lower confidence
94
+ assert data["sentiment"] in ["POSITIVE", "NEGATIVE"]
95
+ assert "confidence" in data
96
+
97
+
98
+ # ============================================
99
+ # Input Validation Tests
100
+ # ============================================
101
+
102
+ def test_empty_text_rejected():
103
+ """Test that empty text is rejected"""
104
+ response = client.post(
105
+ "/analyze",
106
+ json={"text": ""}
107
+ )
108
+
109
+ assert response.status_code == 422 # Validation error
110
+ # FastAPI returns detailed validation errors
111
+
112
+
113
+ def test_missing_text_field():
114
+ """Test that missing text field is rejected"""
115
+ response = client.post(
116
+ "/analyze",
117
+ json={}
118
+ )
119
+
120
+ assert response.status_code == 422
121
+
122
+
123
+ def test_text_too_long():
124
+ """Test that text exceeding max length is rejected"""
125
+ long_text = "a" * 513 # Max is 512 characters
126
+
127
+ response = client.post(
128
+ "/analyze",
129
+ json={"text": long_text}
130
+ )
131
+
132
+ assert response.status_code == 422
133
+
134
+
135
+ def test_non_string_text():
136
+ """Test that non-string text is rejected"""
137
+ response = client.post(
138
+ "/analyze",
139
+ json={"text": 12345} # Number instead of string
140
+ )
141
+
142
+ assert response.status_code == 422
143
+
144
+
145
+ # ============================================
146
+ # Response Format Tests
147
+ # ============================================
148
+
149
+ def test_response_contains_all_fields():
150
+ """Test that response has all required fields"""
151
+ response = client.post(
152
+ "/analyze",
153
+ json={"text": "Great product!"}
154
+ )
155
+
156
+ assert response.status_code == 200
157
+ data = response.json()
158
+
159
+ # Check all required fields exist
160
+ required_fields = ["text", "sentiment", "confidence", "processing_time_ms"]
161
+ for field in required_fields:
162
+ assert field in data, f"Missing field: {field}"
163
+
164
+
165
+ def test_confidence_is_valid_probability():
166
+ """Test that confidence is between 0 and 1"""
167
+ response = client.post(
168
+ "/analyze",
169
+ json={"text": "Excellent!"}
170
+ )
171
+
172
+ data = response.json()
173
+ confidence = data["confidence"]
174
+
175
+ assert 0 <= confidence <= 1, "Confidence must be between 0 and 1"
176
+
177
+
178
+ def test_sentiment_is_valid_label():
179
+ """Test that sentiment is either POSITIVE or NEGATIVE"""
180
+ response = client.post(
181
+ "/analyze",
182
+ json={"text": "Test text"}
183
+ )
184
+
185
+ data = response.json()
186
+ sentiment = data["sentiment"]
187
+
188
+ assert sentiment in ["POSITIVE", "NEGATIVE"], "Invalid sentiment label"
189
+
190
+
191
+ # ============================================
192
+ # Edge Case Tests
193
+ # ============================================
194
+
195
+ def test_special_characters():
196
+ """Test handling of special characters"""
197
+ response = client.post(
198
+ "/analyze",
199
+ json={"text": "Wow!!! This is #amazing 😊 @company"}
200
+ )
201
+
202
+ assert response.status_code == 200
203
+ # Should handle emojis and special chars gracefully
204
+
205
+
206
+ def test_multiple_languages_english_only():
207
+ """Test that non-English text still gets processed"""
208
+ # Note: DistilBERT is trained on English
209
+ # This test just verifies it doesn't crash
210
+ response = client.post(
211
+ "/analyze",
212
+ json={"text": "Hola mundo"}
213
+ )
214
+
215
+ assert response.status_code == 200
216
+ # May not be accurate, but shouldn't crash
217
+
218
+
219
+ def test_very_short_text():
220
+ """Test analysis of very short text"""
221
+ response = client.post(
222
+ "/analyze",
223
+ json={"text": "Good"}
224
+ )
225
+
226
+ assert response.status_code == 200
227
+ data = response.json()
228
+ assert data["sentiment"] == "POSITIVE"
229
+
230
+
231
+ def test_maximum_length_text():
232
+ """Test analysis of text at maximum allowed length"""
233
+ max_text = "a" * 512 # Exactly at max
234
+
235
+ response = client.post(
236
+ "/analyze",
237
+ json={"text": max_text}
238
+ )
239
+
240
+ assert response.status_code == 200
241
+
242
+
243
+ # ============================================
244
+ # Performance Tests
245
+ # ============================================
246
+
247
+ def test_response_time_reasonable():
248
+ """Test that response time is reasonable (< 5 seconds)"""
249
+ import time
250
+
251
+ start = time.time()
252
+ response = client.post(
253
+ "/analyze",
254
+ json={"text": "Test performance"}
255
+ )
256
+ elapsed = time.time() - start
257
+
258
+ assert response.status_code == 200
259
+ assert elapsed < 5.0, f"Response took {elapsed}s, should be < 5s"
260
+
261
+
262
+ # ============================================
263
+ # API Documentation Tests
264
+ # ============================================
265
+
266
+ def test_openapi_docs_available():
267
+ """Test that API documentation is available"""
268
+ response = client.get("/docs")
269
+ assert response.status_code == 200
270
+
271
+
272
+ def test_openapi_json_available():
273
+ """Test that OpenAPI JSON schema is available"""
274
+ response = client.get("/openapi.json")
275
+ assert response.status_code == 200
276
+
277
+ schema = response.json()
278
+ assert "openapi" in schema
279
+ assert "info" in schema
280
+ assert "paths" in schema