Skip to content

Commit

Permalink
Working on points distribution
Browse files Browse the repository at this point in the history
  • Loading branch information
yves-chevallier committed Oct 9, 2024
1 parent 499b858 commit 73fcc33
Show file tree
Hide file tree
Showing 8 changed files with 2,397 additions and 1,230 deletions.
13 changes: 12 additions & 1 deletion baygon/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ExactSequence,
Optional,
Required,
Exclusive,
Self,
)
from voluptuous import Schema as VSchema
Expand Down Expand Up @@ -90,7 +91,15 @@ def __call__(self, v):
Optional("executable", default=None, description="Path to the executable"): Any(
str, None
),
Optional("points", default=0, description="Points given for this test"): int,
Optional(
Exclusive(
"points", "points_or_weight", description="Points given for this test"
)
): Any(float, int),
Optional(
Exclusive("weight", "points_or_weight", description="Weight of the test")
): Any(float, int),
Optional("min-points", default=0.1): Any(float, int),
}

test = VSchema(
Expand Down Expand Up @@ -123,6 +132,8 @@ def Schema(data, humanize=False): # noqa: N802
Required("tests"): All(
Num.reset(), [All(Any(test, group), Num.next())]
),
Optional("points"): Any(float, int),
Optional("min-points", default=0.1): Any(float, int),
}
)
.extend(common)
Expand Down
173 changes: 173 additions & 0 deletions baygon/score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
""" Module used to compute the score of a test case.
Used in academic.
"""

from decimal import Decimal, getcontext, ROUND_HALF_UP


def float_or_int(value):
"""Return a float or an integer."""
if value == int(value):
return int(value)
return float(value)


def distribute(values, total, min_value):
"""Distrubute the values given using a minimum step to ensure
the total sum is respected.
>>> distribute([1, 2, 3, 4], 10, 0.001)
[1, 2, 3, 4]
>>> distribute([1, 1, 1, 1], 100, 0.01)
[25, 25, 25, 25]
>>> distribute([12.5, 19.7, 42.1, 8.9], 100, 0.2)
[15, 23.7, 50.6, 10.7]
>>> distribute([100, 100, 200, 200], 50, 1)
[8, 8, 17, 17]
"""
getcontext().prec = 28
total_weight = sum(values)
total = Decimal(str(total))
min_value = Decimal(str(min_value))
decimal_values = [Decimal(str(v)) for v in values]
allocations = [v / Decimal(str(total_weight)) * total for v in decimal_values]

# Round down to the nearest multiple of min_value
quantizer = min_value
allocations_rounded = [
a.quantize(quantizer, rounding=ROUND_HALF_UP) for a in allocations
]

# Adjust the allocations to match the total
total_allocated = sum(allocations_rounded)
difference = total - total_allocated

# If the difference is not zero, adjust the allocations
if difference != Decimal("0"):
# Compute the number of units to adjust
units_to_adjust = int(
(difference / min_value).to_integral_value(rounding=ROUND_HALF_UP)
)
# Sort the allocations by their remainders
remainders = [
a - a.quantize(quantizer, rounding=ROUND_DOWN) for a in allocations
]
if units_to_adjust > 0:
indices = sorted(
range(len(values)), key=lambda i: remainders[i], reverse=True
)
adjustment = min_value
else:
indices = sorted(range(len(values)), key=lambda i: remainders[i])
adjustment = -min_value
units_to_adjust = abs(units_to_adjust)

for i in range(units_to_adjust):
idx = indices[i % len(indices)]
allocations_rounded[idx] += adjustment

return [float_or_int(a) for a in allocations_rounded]


def assign_points(test, parent=None):
"""Assign points recursively to each test in the structure."""
min_point = test.get("min-points", parent.get("min-points", 1) if parent else 1)

# Case 6: Default points if there are weights or points somewhere
default_point = 1 if has_weights_or_points(test) else 0

# Set default weight to 10 if not specified
if "weight" not in test and "points" not in test and "tests" in test:
test["weight"] = 10

# If 'points' is not in test, compute it based on parent's points and weights
if "points" not in test:
if "weight" in test and parent and "points" in parent:
total_siblings_weight = parent.get("_total_weights", 0)
if total_siblings_weight == 0:
total_siblings_weight = sum(
t.get("weight", 10) for t in parent.get("tests", [])
)
parent["_total_weights"] = total_siblings_weight
test["points"] = test["weight"] / total_siblings_weight * parent["points"]
test["points"] = float_or_int(
Decimal(str(test["points"])).quantize(
Decimal(str(min_point)), rounding=ROUND_HALF_UP
)
)
else:
test["points"] = default_point

# If there are subtests, assign points to them
if "tests" in test:
fixed_points = sum(
subtest.get("points", 0) for subtest in test["tests"] if "points" in subtest
)
weights = []
subtests_to_distribute = []
for subtest in test["tests"]:
if "points" in subtest:
continue # Skip subtests that already have points
elif "weight" in subtest:
weights.append(subtest["weight"])
subtests_to_distribute.append(subtest)
else:
# Case 2 and 6: Default weight is 10 if not specified
subtest["weight"] = 10
weights.append(subtest["weight"])
subtests_to_distribute.append(subtest)

points_to_distribute = test["points"] - fixed_points

if weights and points_to_distribute > 0:
allocated_points = distribute(weights, points_to_distribute, min_point)
for subtest, points in zip(subtests_to_distribute, allocated_points):
subtest["points"] = points
elif not weights and points_to_distribute > 0:
# Case 3: No weights but points are given to all tests
equally_divided_point = points_to_distribute / len(test["tests"])
for subtest in test["tests"]:
if "points" not in subtest:
subtest["points"] = float_or_int(
Decimal(str(equally_divided_point)).quantize(
Decimal(str(min_point)), rounding=ROUND_HALF_UP
)
)

# Recursive call for subtests
if "tests" in test:
for subtest in test["tests"]:
assign_points(subtest, parent=test)

# Clean up temporary keys
if "_total_weights" in test:
del test["_total_weights"]


def has_weights_or_points(test):
"""Check if there are weights or points in the test or its subtests."""
if "weight" in test or "points" in test:
return True
if "tests" in test:
return any(has_weights_or_points(subtest) for subtest in test["tests"])
return False


def compute_points(data):
"""Compute points for the entire structure."""
# Case 4: If no weights or points exist anywhere, do nothing
if not has_weights_or_points(data):
data["compute-score"] = False
return data

# Case 5: Set compute-score to true
data["compute-score"] = True

# Case 7: If total points are given at root but no other info, set default weight
if "points" in data and "tests" in data:
for test in data["tests"]:
if "weight" not in test and "points" not in test:
test["weight"] = 10

assign_points(data)
return data
5 changes: 4 additions & 1 deletion baygon/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .id import Id
from .matchers import InvalidExitStatus, MatcherFactory
from .schema import Schema
from .score import compute_points


def find_testfile(path=None):
Expand Down Expand Up @@ -121,7 +122,7 @@ def __init__(self, *args, **kwargs):

self.name = config["name"]
self.id = Id(config["test_id"])
self.points = config["points"]
self.points = config.get("points", 1)

super().__init__(*args, **kwargs)

Expand Down Expand Up @@ -268,6 +269,8 @@ def __init__(self, data: dict = None, path=None, executable=None, cwd=None):
cwd = self.path
self.config = load_config(self.path)

# compute_points(self.config)

self.name = self.config.get("name", "Test Suite")
self.version = self.config.get("version")

Expand Down
43 changes: 17 additions & 26 deletions docs/docs/.vuepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,34 @@
import { defineUserConfig } from 'vuepress'
import { defaultTheme } from '@vuepress/theme-default'
import { viteBundler } from '@vuepress/bundler-vite'

export default defineUserConfig({
base: '/baygon/',
base: '/', // Ou '/baygon/' si tu déploies dans un sous-dossier
lang: 'en-US',
title: 'Baygon',
description: "Minimalistic functional test framework",
markdown: {
code: {
lineNumbers: false
}
},
bundler: viteBundler({
viteOptions: {},
vuePluginOptions: {},
}),
theme: defaultTheme({
repo: 'heig-tin-info/baygon',
repoLabel: 'Contribute!',
docsDir: 'docs',
editLinkText: '',
lastUpdated: false,
docsDir: 'docs', // Assure-toi que cela correspond à ton répertoire source
navbar: [
{ text: 'Guide', link: '/guide/' },
{ text: 'Baygon', link: 'https://pypi.org/project/baygon/' },
],
sidebar: [
{
text: 'Guide',
link: '/guide/',
children: [
{ text: 'Getting Started', link: '/guide/' },
{ text: 'Syntax', link: '/guide/syntax.md' },
{ text: 'Scripting', link: '/guide/scripting.md' },
{ text: 'Advanced', link: '/guide/advanced.md' },
],
},
{
text: 'Baygon',
link: 'https://pypi.org/project/baygon/'
}
],
sidebar: [{
text: 'Guide',
link: '/guide/',
children: [
{ text: 'Getting Started', link: '/guide/README.md' },
{ text: 'Syntax', link: '/guide/syntax.md' },
{ text: 'Scripting', link: '/guide/scripting.md' },
{ text: 'Advanced', link: '/guide/advanced.md' },
]
}]
}),
plugins: [
]
})
19 changes: 19 additions & 0 deletions docs/docs/guide/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,22 @@ tests:
- regex: f(oo|aa|uu) # Must match
- equals: foobar
```

## Given Points

In the case Baygon is used in an academic context, you can assign points to each test. The following assigns 10 points to the first test and 5 to the second:

```yaml
version: 1
tests:
- points: 10
args: [1, 2]
stdout: 3
- points: 5
args: [1, 2]
stdout: 3
```

Points can be given at different levels:

1. At the top level, all tests will have the same points.
5 changes: 5 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
"docs:build": "vuepress build docs"
},
"devDependencies": {
"@vuepress/bundler-vite": "^2.0.0-rc.17",
"@vuepress/plugin-docsearch": "^2.0.0-beta.53",
"@vuepress/plugin-search": "^2.0.0-beta.53",
"sass-embedded": "^1.79.4",
"vuepress": "^2.0.0-beta.53"
},
"dependencies": {
"@vuepress/theme-default": "^2.0.0-rc.52"
}
}
Loading

0 comments on commit 73fcc33

Please sign in to comment.