Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0a48f7a
Refactor object inference in JSON converter
Fellmonkey Aug 2, 2025
b44f0b1
Refactor model deserialization logic in C# template
Fellmonkey Aug 2, 2025
4c6b9f7
Handle optional properties in model From method
Fellmonkey Aug 9, 2025
b071cbc
synchronization with the Unity template
Fellmonkey Aug 14, 2025
e9586d2
Refactor model parsing for nullable and array properties
Fellmonkey Sep 25, 2025
5ad9a4b
Skip null parameters in request parameter loop
Fellmonkey Sep 25, 2025
953600d
Refactor model class name generation in template
Fellmonkey Sep 28, 2025
d9f6d71
Merge branch 'master' into improve-json-serialization
Fellmonkey Oct 1, 2025
cc2cd62
Add parse_value Twig function for DotNet models
Fellmonkey Oct 1, 2025
ebbad72
make generated array mappings null-safe
Fellmonkey Oct 1, 2025
ff2545a
lint
Fellmonkey Oct 3, 2025
faad585
Import Enums namespace conditionally in model template
Fellmonkey Oct 3, 2025
995ba87
Merge branch 'master' into improve-json-serialization
ChiragAgg5k Oct 4, 2025
10993e5
Refactor array handling in DotNet code generation
Fellmonkey Oct 13, 2025
76ce99c
Merge branch 'master' into improve-json-serialization
Fellmonkey Oct 13, 2025
8dfc5c5
Add .NET SDK test templates and test generation
Fellmonkey Oct 19, 2025
a71ae71
Add test execution step to SDK build workflow
Fellmonkey Oct 19, 2025
9725af6
Add new project configuration to Package.sln
Fellmonkey Oct 20, 2025
e9cb8f7
Fix InputFile filename assertion for OS differences
Fellmonkey Oct 20, 2025
be1bc73
Avoid C# 12 collection expressions for broader compatibility.
Fellmonkey Oct 20, 2025
11427a9
Remove redundant CanConvert test for non-enum type
Fellmonkey Oct 20, 2025
8bfe93e
spec.title
Fellmonkey Oct 20, 2025
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
42 changes: 42 additions & 0 deletions .github/workflows/sdk-build-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,45 @@ jobs:
exit 1
;;
esac

- name: Run Tests
working-directory: examples/${{ matrix.sdk }}
run: |
case "${{ matrix.sdk }}" in
web|node|cli|react-native)
npm test || echo "No tests available"
;;
flutter)
flutter test || echo "No tests available"
;;
apple|swift)
swift test || echo "No tests available"
;;
android)
./gradlew test || echo "No tests available"
;;
kotlin)
./gradlew test || echo "No tests available"
;;
php)
vendor/bin/phpunit || echo "No tests available"
;;
python)
python -m pytest || echo "No tests available"
;;
ruby)
bundle exec rake test || bundle exec rspec || echo "No tests available"
;;
dart)
dart test || echo "No tests available"
;;
go)
go test ./... || echo "No tests available"
;;
dotnet)
dotnet test || echo "No tests available"
;;
*)
echo "No tests for SDK: ${{ matrix.sdk }}"
;;
esac
Comment on lines +201 to +241
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Tests are being swallowed: || echo "No tests available" masks failures

Any failing test will still return success, defeating CI. Detect presence of tests first, then run without masking. Example fix:

-      - name: Run Tests
+      - name: Run Tests
         working-directory: examples/${{ matrix.sdk }}
         run: |
-          case "${{ matrix.sdk }}" in
-            web|node|cli|react-native)
-              npm test || echo "No tests available"
-              ;;
+          set -e
+          case "${{ matrix.sdk }}" in
+            web|node|cli|react-native)
+              if node -p "require('./package.json').scripts && require('./package.json').scripts.test ? 1 : 0" >/dev/null 2>&1; then
+                npm test --silent
+              else
+                echo "No tests available"
+              fi
+              ;;
             flutter)
-              flutter test || echo "No tests available"
+              if [ -d "test" ]; then flutter test; else echo "No tests available"; fi
               ;;
             apple|swift)
-              swift test || echo "No tests available"
+              if ls -1 *.package.swift *.swiftpm >/dev/null 2>&1 || [ -d "Tests" ]; then swift test; else echo "No tests available"; fi
               ;;
             android)
-              ./gradlew test || echo "No tests available"
+              if [ -f "gradlew" ] && rg -n "testImplementation|androidTestImplementation" -g '!**/build/**' -S . >/dev/null 2>&1; then ./gradlew test; else echo "No tests available"; fi
               ;;
             kotlin)
-              ./gradlew test || echo "No tests available"
+              if [ -f "gradlew" ] && rg -n "testImplementation" -g '!**/build/**' -S . >/dev/null 2>&1; then ./gradlew test; else echo "No tests available"; fi
               ;;
             php)
-              vendor/bin/phpunit || echo "No tests available"
+              if [ -x "vendor/bin/phpunit" ] || [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then vendor/bin/phpunit; else echo "No tests available"; fi
               ;;
             python)
-              python -m pytest || echo "No tests available"
+              if python - <<'PY' 2>/dev/null; then print(); PY
+              then
+                echo "No Python"; exit 0
+              fi
+              if [ -d "tests" ] || rg -n "^pytest" -S pyproject.toml setup.cfg tox.ini >/dev/null 2>&1; then python -m pytest; else echo "No tests available"; fi
               ;;
             ruby)
-              bundle exec rake test || bundle exec rspec || echo "No tests available"
+              if [ -f "Rakefile" ]; then bundle exec rake test || bundle exec rspec; elif [ -d "spec" ]; then bundle exec rspec; else echo "No tests available"; fi
               ;;
             dart)
-              dart test || echo "No tests available"
+              if [ -d "test" ]; then dart test; else echo "No tests available"; fi
               ;;
             go)
-              go test ./... || echo "No tests available"
+              if rg -n "func Test" -t go >/dev/null 2>&1; then go test ./...; else echo "No tests available"; fi
               ;;
             dotnet)
-              dotnet test || echo "No tests available"
+              if ls -1 *.sln */*Tests.csproj >/dev/null 2>&1; then dotnet test --nologo --verbosity minimal; else echo "No tests available"; fi
               ;;
             *)
               echo "No tests for SDK: ${{ matrix.sdk }}"
               ;;
           esac

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
.github/workflows/sdk-build-validation.yml lines 201-241: the current run step
appends `|| echo "No tests available"` which masks test failures; change each
case to first detect whether tests exist or the test command is available (for
node/web/react-native/cli check for a test script in package.json or presence of
test files, for flutter/dart run a lightweight discovery or check for test
directory/files, for swift/apple check for Package.swift tests, for
android/kotlin check for gradle tasks or test sources, for php check for phpunit
config or tests dir, for python check for pytest discovery or tests/ dir, for
ruby check for spec/test dirs, for go check for *_test.go files, for dotnet
check for test project files), and only run the test command without `|| echo
...` so failures surface; if no tests are detected, print the "No tests
available" message and exit 0. Ensure you remove the `|| echo "No tests
available"` from the actual test command invocations so failing tests return
non-zero.

184 changes: 182 additions & 2 deletions src/SDK/Language/DotNet.php
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,85 @@ public function getFiles(): array
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}/Enums/IEnum.cs',
'template' => 'dotnet/Package/Enums/IEnum.cs.twig',
],
// Tests
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/{{ spec.title | caseUcfirst }}.Tests.csproj',
'template' => 'dotnet/Package.Tests/Tests.csproj.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/.gitignore',
'template' => 'dotnet/Package.Tests/.gitignore',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/ClientTests.cs',
'template' => 'dotnet/Package.Tests/ClientTests.cs.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/IDTests.cs',
'template' => 'dotnet/Package.Tests/IDTests.cs.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/PermissionTests.cs',
'template' => 'dotnet/Package.Tests/PermissionTests.cs.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/RoleTests.cs',
'template' => 'dotnet/Package.Tests/RoleTests.cs.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/QueryTests.cs',
'template' => 'dotnet/Package.Tests/QueryTests.cs.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/ExceptionTests.cs',
'template' => 'dotnet/Package.Tests/ExceptionTests.cs.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/UploadProgressTests.cs',
'template' => 'dotnet/Package.Tests/UploadProgressTests.cs.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/Models/InputFileTests.cs',
'template' => 'dotnet/Package.Tests/Models/InputFileTests.cs.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/Converters/ObjectToInferredTypesConverterTests.cs',
'template' => 'dotnet/Package.Tests/Converters/ObjectToInferredTypesConverterTests.cs.twig',
],
[
'scope' => 'default',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/Converters/ValueClassConverterTests.cs',
'template' => 'dotnet/Package.Tests/Converters/ValueClassConverterTests.cs.twig',
],
// Tests for each definition (model)
[
'scope' => 'definition',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/Models/{{ definition.name | caseUcfirst | overrideIdentifier }}Tests.cs',
'template' => 'dotnet/Package.Tests/Models/ModelTests.cs.twig',
],
// Tests for each enum
[
'scope' => 'enum',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/Enums/{{ enum.name | caseUcfirst | overrideIdentifier }}Tests.cs',
'template' => 'dotnet/Package.Tests/Enums/EnumTests.cs.twig',
],
// Tests for each service
[
'scope' => 'service',
'destination' => '{{ spec.title | caseUcfirst }}.Tests/Services/{{service.name | caseUcfirst}}Tests.cs',
'template' => 'dotnet/Package.Tests/Services/ServiceTests.cs.twig',
]
];
}
Expand All @@ -463,11 +542,17 @@ public function getFilters(): array
}
return $property;
}),
new TwigFilter('escapeCsString', function ($value) {
if (is_string($value)) {
return addcslashes($value, '\\"');
}
return $value;
}),
];
}

/**
* get sub_scheme and property_name functions
* get sub_scheme, property_name and parse_value functions
* @return TwigFunction[]
*/
public function getFunctions(): array
Expand All @@ -494,7 +579,31 @@ public function getFunctions(): array
}

return $result;
}),
}, ['is_safe' => ['html']]),
new TwigFunction('test_item_type', function (array $property) {
// For test templates: returns the item type for arrays without the List<> wrapper
$result = '';

if (isset($property['sub_schema']) && !empty($property['sub_schema'])) {
// Model type
$result = $this->toPascalCase($property['sub_schema']);
$result = 'Appwrite.Models.' . $result;
} elseif (isset($property['enum']) && !empty($property['enum'])) {
// Enum type
$enumName = $property['enumName'] ?? $property['name'];
$result = 'Appwrite.Enums.' . $this->toPascalCase($enumName);
} elseif (isset($property['items']) && isset($property['items']['type'])) {
// Primitive array type (for definitions)
$result = $this->getTypeName($property['items']);
} elseif (isset($property['array']) && isset($property['array']['type'])) {
// Primitive array type (for method parameters)
$result = $this->getTypeName($property['array']);
} else {
$result = 'object';
}

return $result;
}, ['is_safe' => ['html']]),
new TwigFunction('property_name', function (array $definition, array $property) {
$name = $property['name'];
$name = \str_replace('$', '', $name);
Expand All @@ -504,6 +613,77 @@ public function getFunctions(): array
}
return $name;
}),
new TwigFunction('parse_value', function (array $property, string $mapAccess, string $v) {
$required = $property['required'] ?? false;

// Handle sub_schema
if (isset($property['sub_schema']) && !empty($property['sub_schema'])) {
$subSchema = \ucfirst($property['sub_schema']);

if ($property['type'] === 'array') {
$src = $required ? $mapAccess : $v;
return "{$src}.ToEnumerable().Select(it => {$subSchema}.From(map: (Dictionary<string, object>)it)).ToList()";
} else {
if ($required) {
return "{$subSchema}.From(map: (Dictionary<string, object>){$mapAccess})";
}
return "({$v} as Dictionary<string, object>) is { } obj ? {$subSchema}.From(map: obj) : null";
}
}

// Handle enum
if (isset($property['enum']) && !empty($property['enum'])) {
$enumName = $property['enumName'] ?? $property['name'];
$enumClass = \ucfirst($enumName);

if ($required) {
return "new {$enumClass}({$mapAccess}.ToString())";
}
return "{$v} == null ? null : new {$enumClass}({$v}.ToString())";
}

// Handle arrays
if ($property['type'] === 'array') {
$itemsType = $property['items']['type'] ?? 'object';
$src = $required ? $mapAccess : $v;

$selectExpression = match ($itemsType) {
'string' => 'x.ToString()',
'integer' => 'Convert.ToInt64(x)',
'number' => 'Convert.ToDouble(x)',
'boolean' => '(bool)x',
default => 'x'
};

return "{$src}.ToEnumerable().Select(x => {$selectExpression}).ToList()";
}

// Handle integer/number
if ($property['type'] === 'integer' || $property['type'] === 'number') {
$convertMethod = $property['type'] === 'integer' ? 'Int64' : 'Double';

if ($required) {
return "Convert.To{$convertMethod}({$mapAccess})";
}
return "{$v} == null ? null : Convert.To{$convertMethod}({$v})";
}

// Handle boolean
if ($property['type'] === 'boolean') {
$typeName = $this->getTypeName($property);

if ($required) {
return "({$typeName}){$mapAccess}";
}
return "({$typeName}?){$v}";
}

// Handle string type
if ($required) {
return "{$mapAccess}.ToString()";
}
return "{$v}?.ToString()";
}, ['is_safe' => ['html']]),
];
}

Expand Down
23 changes: 23 additions & 0 deletions templates/dotnet/Package.Tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Test results
TestResults/
*.trx
*.coverage
*.coveragexml

# Coverage reports
coverage/
coverage.json
coverage.opencover.xml
lcov.info

# Build outputs
bin/
obj/
*.user
*.suo

# Rider
.idea/

# Visual Studio
.vs/
Loading
Loading