Compare commits
17
Commits
7ca504a70a
...
v4.17.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e835e220a | ||
|
|
480e423124 | ||
|
|
48096c7fc7 | ||
|
|
8b9691d692 | ||
|
|
5bac29b0af | ||
|
|
9b94e2d405 | ||
|
|
20434ca273 | ||
|
|
feb39ee2de | ||
|
|
1d578debe2 | ||
|
|
5adc4a6526 | ||
|
|
66e3af043c | ||
|
|
9a632b8d62 | ||
|
|
cb0e838964 | ||
|
|
8954f9aaba | ||
|
|
94843311b4 | ||
|
|
52e5f68da0 | ||
|
|
99b3030180 |
+18
@@ -414,3 +414,21 @@ FodyWeavers.xsd
|
|||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
|
|
||||||
|
# ---> AutoBidder / Mimante
|
||||||
|
|
||||||
|
# Configurazione di rilascio: contiene un token di Gitea
|
||||||
|
gitea.json
|
||||||
|
|
||||||
|
# Riepilogo della rigiocata sui dossier (target Backtest)
|
||||||
|
*backtest-report.txt
|
||||||
|
|
||||||
|
# Pacchetti prodotti da build/Release.proj
|
||||||
|
bin/installer/
|
||||||
|
|
||||||
|
# Rider / JetBrains
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|||||||
Vendored
+39
-11
@@ -17,7 +17,10 @@
|
|||||||
"/property:GenerateFullPaths=true",
|
"/property:GenerateFullPaths=true",
|
||||||
"/consoleloggerparameters:NoSummary"
|
"/consoleloggerparameters:NoSummary"
|
||||||
],
|
],
|
||||||
"group": { "kind": "build", "isDefault": true },
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,7 +35,10 @@
|
|||||||
"-nologo",
|
"-nologo",
|
||||||
"-v:m"
|
"-v:m"
|
||||||
],
|
],
|
||||||
"group": { "kind": "test", "isDefault": true },
|
"group": {
|
||||||
|
"kind": "test",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -47,7 +53,10 @@
|
|||||||
"-nologo",
|
"-nologo",
|
||||||
"-v:m"
|
"-v:m"
|
||||||
],
|
],
|
||||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -63,12 +72,15 @@
|
|||||||
"-nologo",
|
"-nologo",
|
||||||
"-v:m"
|
"-v:m"
|
||||||
],
|
],
|
||||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "crea installatore",
|
"label": "crea installatore",
|
||||||
"detail": "Chiede la versione, verifica, pubblica ed esegue Inno Setup: bin/installer/AutoBidder-<versione>-setup.exe. Non tocca Gitea.",
|
"detail": "Verifica, pubblica ed esegue Inno Setup: bin/installer/AutoBidder-<versione>-setup.exe. Crea il tag a pacchetto pronto. Non tocca Gitea.",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"command": "dotnet",
|
"command": "dotnet",
|
||||||
"args": [
|
"args": [
|
||||||
@@ -79,7 +91,10 @@
|
|||||||
"-nologo",
|
"-nologo",
|
||||||
"-v:m"
|
"-v:m"
|
||||||
],
|
],
|
||||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -96,12 +111,15 @@
|
|||||||
"-nologo",
|
"-nologo",
|
||||||
"-v:m"
|
"-v:m"
|
||||||
],
|
],
|
||||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "rilascia su Gitea",
|
"label": "rilascia su Gitea",
|
||||||
"detail": "Chiede versione e note, poi: verifica, pubblica, installatore, tag e release su Gitea con i file allegati. Richiede build/gitea.json.",
|
"detail": "Verifica, pubblica, installatore, tag e release su Gitea con i file allegati. La versione viene dal tag su HEAD. Richiede build/gitea.json.",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"command": "dotnet",
|
"command": "dotnet",
|
||||||
"args": [
|
"args": [
|
||||||
@@ -109,11 +127,21 @@
|
|||||||
"${workspaceFolder}/build/Release.proj",
|
"${workspaceFolder}/build/Release.proj",
|
||||||
"-t:Rilascia",
|
"-t:Rilascia",
|
||||||
"-p:Versione=${input:versione}",
|
"-p:Versione=${input:versione}",
|
||||||
"-p:Note=${input:note}",
|
|
||||||
"-nologo",
|
"-nologo",
|
||||||
"-v:m"
|
"-v:m"
|
||||||
],
|
],
|
||||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
// Le note passano dall'ambiente, non da -p:. MSBuild spezza il valore di
|
||||||
|
// una proprietà sulle virgole e una nota in italiano ne ha quasi sempre
|
||||||
|
// una: si otterrebbe MSB1006 «proprietà non valida». Vedi Release.proj.
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"AUTOBIDDER_NOTE": "${input:note}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
},
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -121,7 +149,7 @@
|
|||||||
{
|
{
|
||||||
"id": "versione",
|
"id": "versione",
|
||||||
"type": "promptString",
|
"type": "promptString",
|
||||||
"description": "Versione (vuoto = incrementa la minor, es. 4.11.0 → 4.12.0)",
|
"description": "Versione — lascia vuoto se hai già taggato (git tag v4.14.0), o per la minor successiva",
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,12 +15,13 @@
|
|||||||
|
|
||||||
<!-- App desktop leggera per Bidoo. Nessun software aggiuntivo:
|
<!-- App desktop leggera per Bidoo. Nessun software aggiuntivo:
|
||||||
WebView2 runtime è preinstallato su Windows 11. -->
|
WebView2 runtime è preinstallato su Windows 11. -->
|
||||||
<!-- Unico numero da toccare a ogni nuova versione: titolo finestra e log d'avvio
|
<!-- Versione delle sole compilazioni di sviluppo. NON va alzata per rilasciare:
|
||||||
lo leggono a runtime tramite Utilities/AppInfo. -->
|
nei pacchetti pubblicati questi quattro numeri vengono dal tag git, passati
|
||||||
<Version>4.12.0</Version>
|
da build/Release.proj a dotnet publish. Vedi Utilities/AppInfo. -->
|
||||||
<AssemblyVersion>4.12.0.0</AssemblyVersion>
|
<Version>4.13.0</Version>
|
||||||
<FileVersion>4.12.0.0</FileVersion>
|
<AssemblyVersion>4.13.0.0</AssemblyVersion>
|
||||||
<InformationalVersion>4.12.0</InformationalVersion>
|
<FileVersion>4.13.0.0</FileVersion>
|
||||||
|
<InformationalVersion>4.13.0</InformationalVersion>
|
||||||
|
|
||||||
<!-- Runtime unico supportato per la pubblicazione self-contained -->
|
<!-- Runtime unico supportato per la pubblicazione self-contained -->
|
||||||
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
|
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<UserControl x:Class="AutoBidder.Controls.AuctionMonitorControl"
|
<UserControl x:Class="AutoBidder.Controls.AuctionMonitorControl"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button x:Name="StopButton" Style="{StaticResource IconButton}"
|
<Button x:Name="StopButton" Style="{StaticResource IconButton}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
ToolTip="Ferma tutte: nessuna richiesta"
|
ToolTip="Ferma tutte: nessuna richiesta"
|
||||||
Click="StopButton_Click">
|
Click="StopButton_Click">
|
||||||
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
<TextBlock Style="{StaticResource Glyph}" Text=""/>
|
||||||
@@ -390,6 +391,7 @@
|
|||||||
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
<TextBlock FontFamily="Segoe MDL2 Assets" Text=""/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Style="{StaticResource MiniIconButton}"
|
<Button Style="{StaticResource MiniIconButton}"
|
||||||
|
Foreground="{DynamicResource Brush.Danger}"
|
||||||
ToolTip="Ferma"
|
ToolTip="Ferma"
|
||||||
IsEnabled="{Binding CanStop}"
|
IsEnabled="{Binding CanStop}"
|
||||||
Command="{Binding DataContext.GridStopCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Command="{Binding DataContext.GridStopCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
@@ -524,14 +526,18 @@
|
|||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Anticipo puntata (ms)"
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Anticipo puntata (ms)"
|
||||||
ToolTip="Quanti millisecondi prima della scadenza parte la puntata. È il parametro che decide quanto costa vincere: il motore punta solo nei cicli che arrivano fin qui senza che nessun altro abbia puntato.
|
ToolTip="Quanti millisecondi prima della scadenza parte la puntata. È il parametro che decide quanto costa vincere: il motore punta solo nei cicli che arrivano fin qui senza che nessun altro abbia puntato.
|
||||||
|
|
||||||
Dalla rigiocata di 2.095 aste concluse: sotto il secondo basta in mediana 1 puntata per asta, sotto i 2 s ne servono 15, sotto i 3 s 46. Ma sotto i 600 ms un picco di rete fa arrivare la puntata a giochi chiusi. Tienilo fra 600 e 2000 ms, e comunque sopra il tuo ping medio."
|
Il 75,3% delle puntate avversarie e' automatica e scatta a 2 secondi esatti: sotto quella soglia resta solo chi punta a mano, e una puntata manuale vince 3,6 volte piu' spesso di una automatica (178 puntate per vittoria contro 634, su 5410 aste).
|
||||||
|
|
||||||
|
Predefinito 500 ms: circa nove volte il ping misurato (47-78 ms). Piu' in basso non conviene, perche' Bidoo dichiara la scadenza al secondo intero."
|
||||||
Style="{StaticResource FieldLabel}"/>
|
Style="{StaticResource FieldLabel}"/>
|
||||||
<TextBox Grid.Row="1" Grid.Column="0" x:Name="SelectedBidBeforeDeadlineMs"
|
<TextBox Grid.Row="1" Grid.Column="0" x:Name="SelectedBidBeforeDeadlineMs"
|
||||||
Margin="0,0,10,2" Text="800"
|
Margin="0,0,10,2" Text="500"
|
||||||
TextChanged="SelectedBidBeforeDeadlineMs_TextChanged"
|
TextChanged="SelectedBidBeforeDeadlineMs_TextChanged"
|
||||||
ToolTip="Quanti millisecondi prima della scadenza parte la puntata. È il parametro che decide quanto costa vincere: il motore punta solo nei cicli che arrivano fin qui senza che nessun altro abbia puntato.
|
ToolTip="Quanti millisecondi prima della scadenza parte la puntata. È il parametro che decide quanto costa vincere: il motore punta solo nei cicli che arrivano fin qui senza che nessun altro abbia puntato.
|
||||||
|
|
||||||
Dalla rigiocata di 2.095 aste concluse: sotto il secondo basta in mediana 1 puntata per asta, sotto i 2 s ne servono 15, sotto i 3 s 46. Ma sotto i 600 ms un picco di rete fa arrivare la puntata a giochi chiusi. Tienilo fra 600 e 2000 ms, e comunque sopra il tuo ping medio."/>
|
Il 75,3% delle puntate avversarie e' automatica e scatta a 2 secondi esatti: sotto quella soglia resta solo chi punta a mano, e una puntata manuale vince 3,6 volte piu' spesso di una automatica (178 puntate per vittoria contro 634, su 5410 aste).
|
||||||
|
|
||||||
|
Predefinito 500 ms: circa nove volte il ping misurato (47-78 ms). Piu' in basso non conviene, perche' Bidoo dichiara la scadenza al secondo intero."/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="Prezzo minimo €"
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="Prezzo minimo €"
|
||||||
ToolTip="Sotto questo prezzo non puntare. Serve a lasciar salire un'asta prima di entrare. 0 = nessun limite."
|
ToolTip="Sotto questo prezzo non puntare. Serve a lasciar salire un'asta prima di entrare. 0 = nessun limite."
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Costo medio di una puntata (€)"
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="Costo medio di una puntata (€)"
|
||||||
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
Foreground="{DynamicResource Brush.Text}" Margin="0,10" VerticalAlignment="Center"
|
||||||
ToolTip="Quanto ti costa una puntata. Serve a calcolare il tetto di spesa: valore del prodotto meno le puntate massime moltiplicate per questo costo."/>
|
ToolTip="Quanto ti costa una puntata. Serve a calcolare il tetto di spesa: valore del prodotto meno le puntate massime moltiplicate per questo costo. Il valore predefinito è 0,20 €, cioè il pacchetto meno vantaggioso in vendita su Bidoo: tenerlo così fa stimare per eccesso, e una stima per eccesso al massimo ti fa rinunciare a un affare, mentre una per difetto ti fa spendere più del valore del prodotto. Se compri pacchetti più grandi, abbassalo al costo che paghi davvero."/>
|
||||||
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AverageBidCostTextBox"
|
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AverageBidCostTextBox"
|
||||||
Text="0,20" Margin="10,10"/>
|
Text="0,20" Margin="10,10"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ namespace AutoBidder
|
|||||||
{
|
{
|
||||||
LoadStatistics();
|
LoadStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ultimo passo, e non prima: lo storico è già scritto, il dossier
|
||||||
|
// chiuso, le statistiche del prodotto aggiornate. Togliere l'asta
|
||||||
|
// dall'elenco a questo punto non perde niente — tutto ciò che aveva
|
||||||
|
// da dire è già altrove.
|
||||||
|
RimuoviSeConclusaEDaTogliere(auction, won, settings);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -105,6 +111,56 @@ namespace AutoBidder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toglie dall'elenco l'asta appena conclusa, se la regola lo consente.
|
||||||
|
///
|
||||||
|
/// <para>Con l'aggiunta automatica accesa le aste concluse si accumulano a centinaia
|
||||||
|
/// e seppelliscono quelle vive, che è il contrario di quello che serve guardando un
|
||||||
|
/// cruscotto. Restano però quelle su cui si è speso: sono le uniche che vale la pena
|
||||||
|
/// riguardare, e i soldi erano veri. Vedi <see cref="FinishedAuctionCleanup"/>.</para>
|
||||||
|
///
|
||||||
|
/// <para>Il motivo finisce sempre nel registro, anche quando l'asta resta: senza,
|
||||||
|
/// un'asta che sparisce o che non sparisce sarebbero entrambe inspiegabili.</para>
|
||||||
|
/// </summary>
|
||||||
|
private void RimuoviSeConclusaEDaTogliere(AuctionInfo auction, bool won, AppSettings settings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var verdict = FinishedAuctionCleanup.Decide(auction, won, new FinishedAuctionCleanup.Options
|
||||||
|
{
|
||||||
|
Enabled = settings.AutoRemoveFinished,
|
||||||
|
KeepWithMyBids = settings.AutoRemoveKeepWithMyBids,
|
||||||
|
KeepWon = settings.AutoRemoveKeepWon,
|
||||||
|
KeepUnclear = settings.AutoRemoveKeepUnclear
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!verdict.Remove)
|
||||||
|
{
|
||||||
|
if (settings.AutoRemoveFinished)
|
||||||
|
Log($"[ELENCO] '{auction.Name}' resta in elenco: {verdict.Reason}", LogLevel.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stesso giro della rimozione a mano: prima il motore, poi la griglia,
|
||||||
|
// poi il salvataggio. Toglierla solo dalla griglia lascerebbe il runner
|
||||||
|
// a interrogare un'asta che non esiste piu' per chi guarda.
|
||||||
|
_auctionMonitor.RemoveAuction(auction.AuctionId);
|
||||||
|
|
||||||
|
var vm = _auctionViewModels.FirstOrDefault(x => x.AuctionId == auction.AuctionId);
|
||||||
|
if (vm != null) _auctionViewModels.Remove(vm);
|
||||||
|
|
||||||
|
SaveAuctions();
|
||||||
|
UpdateTotalCount();
|
||||||
|
UpdateGlobalControlButtons();
|
||||||
|
|
||||||
|
Log($"[ELENCO] '{auction.Name}' tolta dall'elenco: {verdict.Reason}", LogLevel.Info);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[ELENCO ERROR] {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Quando l'asta è finita davvero, in ordine di attendibilità: l'istante fissato dal
|
/// Quando l'asta è finita davvero, in ordine di attendibilità: l'istante fissato dal
|
||||||
/// monitor quando ha visto la conclusione dal vivo; la scadenza dichiarata dal
|
/// monitor quando ha visto la conclusione dal vivo; la scadenza dichiarata dal
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace AutoBidder
|
|||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
private DispatcherTimer? _headerTimer;
|
private DispatcherTimer? _headerTimer;
|
||||||
|
private DispatcherTimer? _timerTicker;
|
||||||
|
|
||||||
private void StartMonitorHeaderTimer()
|
private void StartMonitorHeaderTimer()
|
||||||
{
|
{
|
||||||
@@ -25,9 +26,35 @@ namespace AutoBidder
|
|||||||
_headerTimer.Tick += (_, _) => RefreshMonitorHeader();
|
_headerTimer.Tick += (_, _) => RefreshMonitorHeader();
|
||||||
_headerTimer.Start();
|
_headerTimer.Start();
|
||||||
|
|
||||||
|
// ── Il conto alla rovescia scorre da solo ────────────────────────
|
||||||
|
//
|
||||||
|
// Il tempo mancante non ha bisogno di una risposta dal server per essere
|
||||||
|
// ricalcolato: la scadenza è ancorata all'orologio di Bidoo e il resto è
|
||||||
|
// sottrazione. Prima però veniva notificato solo all'arrivo di un poll, e
|
||||||
|
// nelle prove reali fra un poll e l'altro passava anche un secondo e mezzo:
|
||||||
|
// il numero restava fermo e sembrava tutto bloccato.
|
||||||
|
//
|
||||||
|
// Cinque battiti al secondo: la colonna mostra i decimi sotto il minuto,
|
||||||
|
// quindi si vede scorrere davvero, e restano un ventesimo delle notifiche
|
||||||
|
// che servirebbero per seguire il decimo esatto.
|
||||||
|
_timerTicker = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
|
||||||
|
_timerTicker.Tick += (_, _) => TickTimers();
|
||||||
|
_timerTicker.Start();
|
||||||
|
|
||||||
RefreshMonitorHeader();
|
RefreshMonitorHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TickTimers()
|
||||||
|
{
|
||||||
|
var vms = _auctionViewModels;
|
||||||
|
|
||||||
|
for (var i = 0; i < vms.Count; i++)
|
||||||
|
{
|
||||||
|
try { vms[i].RefreshTimeDisplay(); }
|
||||||
|
catch { /* una riga che non si aggiorna non deve fermare le altre */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RefreshMonitorHeader()
|
private void RefreshMonitorHeader()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -46,6 +46,17 @@
|
|||||||
ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi."
|
ToolTip="Senza il «Compra Subito» non si può calcolare il risparmio, ma il prezzo e le puntate restano validi."
|
||||||
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<CheckBox x:Name="ChkCoverage" VerticalAlignment="Center"
|
||||||
|
Content="Aste seguite per meno del"
|
||||||
|
ToolTip="Toglie le aste agganciate a metà. Sono il caso peggiore: i numeri sono veri ma parziali, quindi sembrano buone e abbassano le medie senza dare segnali. Quanta asta si è vista si ricava dal prezzo finale, senza bisogno di alcun dato in più: ogni puntata vale un centesimo, quindi il prezzo in centesimi è il totale delle puntate dell'asta, e i reset osservati dicono quanti se ne sono visti. Sul tuo storico, su 5876 aste: al 75% ne resterebbero circa 78 su 100, al 90% circa 64, al 95% circa 54, al 99% circa 34."
|
||||||
|
Checked="Option_Changed" Unchecked="Option_Changed"/>
|
||||||
|
<TextBox x:Name="CoverageBox" Text="90" Width="50" Margin="8,0"
|
||||||
|
VerticalAlignment="Center" TextChanged="Option_Changed"/>
|
||||||
|
<TextBlock Text="% della loro durata" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource Brush.Text}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
|
<CheckBox x:Name="ChkOlderThan" VerticalAlignment="Center"
|
||||||
Content="Aste chiuse da più di"
|
Content="Aste chiuse da più di"
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ namespace AutoBidder.Dialogs
|
|||||||
RemoveWithoutValue = ChkNoValue.IsChecked == true
|
RemoveWithoutValue = ChkNoValue.IsChecked == true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Una soglia illeggibile o fuori scala non deve diventare "togli tutto":
|
||||||
|
// il filtro resta spento finché il numero non ha senso.
|
||||||
|
if (ChkCoverage.IsChecked == true &&
|
||||||
|
double.TryParse(CoverageBox.Text.Trim().Replace(',', '.'),
|
||||||
|
System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var percento) &&
|
||||||
|
percento is > 0 and <= 100)
|
||||||
|
{
|
||||||
|
options.MinObservedCoverage = percento / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
if (ChkOlderThan.IsChecked == true &&
|
if (ChkOlderThan.IsChecked == true &&
|
||||||
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
|
int.TryParse(DaysBox.Text.Trim(), out var days) && days > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -164,22 +164,28 @@ namespace AutoBidder.Engine
|
|||||||
private int NextPollDelay(AppSettings settings)
|
private int NextPollDelay(AppSettings settings)
|
||||||
{
|
{
|
||||||
// Un'asta non ancora cominciata ha una cadenza tutta sua: vedi NotStartedDelay.
|
// Un'asta non ancora cominciata ha una cadenza tutta sua: vedi NotStartedDelay.
|
||||||
|
// È l'unico caso rimasto in cui si aspetta, e non è una scelta di ritmo: fino
|
||||||
|
// all'apertura il server non ha niente di diverso da dire, e interrogarlo
|
||||||
|
// seicento volte al minuto per tre ore non anticipa l'inizio di un istante.
|
||||||
var notStarted = NotStartedDelay(settings);
|
var notStarted = NotStartedDelay(settings);
|
||||||
if (notStarted > 0) return notStarted;
|
if (notStarted > 0) return notStarted;
|
||||||
|
|
||||||
var remaining = _auction.EstimatedRemainingMs();
|
// ── Nessuna attesa: la cadenza la detta la rete ──────────────────────
|
||||||
|
//
|
||||||
if (remaining == double.MaxValue) return settings.PollIntervalFarMs;
|
// Prima qui c'erano quattro soglie configurabili (lontano / medio / vicino /
|
||||||
|
// critico). Sui registri di tre giorni non hanno mai funzionato come scritto:
|
||||||
// In sola osservazione non c'è puntata da azzeccare: si risparmiano chiamate.
|
// con 40 richieste al secondo di tetto globale spartite fra tutte le aste
|
||||||
var critical = _auction.State == RunState.Active
|
// seguite, i 220 ms della finestra critica diventavano 596 ms misurati, e con
|
||||||
? settings.PollIntervalCriticalMs
|
// il timer a un secondo restava un solo sguardo prima della scadenza.
|
||||||
: settings.PollIntervalNearMs;
|
//
|
||||||
|
// Il ritmo adesso è quello del giro di andata e ritorno: si riparte appena la
|
||||||
if (remaining <= settings.CriticalWindowMs) return critical;
|
// risposta è arrivata. Con ping ~57 ms misurato sono circa diciassette
|
||||||
if (remaining <= 10_000) return settings.PollIntervalNearMs;
|
// interrogazioni al secondo per asta.
|
||||||
if (remaining <= 60_000) return settings.PollIntervalMidMs;
|
//
|
||||||
return settings.PollIntervalFarMs;
|
// Il freno non è più un numero scelto a tavolino ma la risposta del server:
|
||||||
|
// vedi la reazione a 429 e 503 in PollLoopAsync. Se Bidoo dice di rallentare,
|
||||||
|
// si rallenta; finché non lo dice, si va al massimo che regge la rete.
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -31,7 +31,37 @@ namespace AutoBidder.Engine.Backtest
|
|||||||
int LeadMs,
|
int LeadMs,
|
||||||
AppSettings Settings,
|
AppSettings Settings,
|
||||||
string Username = "",
|
string Username = "",
|
||||||
double BidCostEuro = 0.20);
|
double BidCostEuro = 0.20)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Le impostazioni come le vede la rigiocata.
|
||||||
|
///
|
||||||
|
/// <para>Spegne la sospensione a fascia oraria, che guarda l'orologio di parete:
|
||||||
|
/// una rigiocata ripercorre aste già concluse, e l'ora in cui viene lanciata non
|
||||||
|
/// c'entra nulla con l'ora in cui quelle aste correvano. Lasciarla accesa
|
||||||
|
/// renderebbe il risultato dipendente da quando si preme il pulsante — di notte
|
||||||
|
/// il motore rifiuterebbe ogni puntata e la rigiocata direbbe zero, che è
|
||||||
|
/// esattamente il tipo di risultato che sembra vero e non lo è.</para>
|
||||||
|
///
|
||||||
|
/// <para>Vale anche per la suite di test, che gira a qualunque ora.</para>
|
||||||
|
/// </summary>
|
||||||
|
public AppSettings EffectiveSettings { get; } = Clona(Settings);
|
||||||
|
|
||||||
|
private static AppSettings Clona(AppSettings origine)
|
||||||
|
{
|
||||||
|
var copia = origine ?? new AppSettings();
|
||||||
|
|
||||||
|
// Copia superficiale via serializzazione: le impostazioni sono un sacco di
|
||||||
|
// proprietà semplici, e mutare quella dell'utente sarebbe un effetto
|
||||||
|
// collaterale che nessuno si aspetta da una rigiocata.
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(copia);
|
||||||
|
var clone = System.Text.Json.JsonSerializer.Deserialize<AppSettings>(json)
|
||||||
|
?? new AppSettings();
|
||||||
|
|
||||||
|
clone.QuietHoursEnabled = false;
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Esito su una singola asta.</summary>
|
/// <summary>Esito su una singola asta.</summary>
|
||||||
public sealed class Result
|
public sealed class Result
|
||||||
@@ -139,7 +169,7 @@ namespace AutoBidder.Engine.Backtest
|
|||||||
ShippingCost: null,
|
ShippingCost: null,
|
||||||
MaxBids: auction.MaxClicks,
|
MaxBids: auction.MaxClicks,
|
||||||
MaxTotalSpendEuro: auction.MaxTotalSpendEuro,
|
MaxTotalSpendEuro: auction.MaxTotalSpendEuro,
|
||||||
MinSavingsPercentage: options.Settings.MinSavingsPercentage,
|
MinSavingsPercentage: options.EffectiveSettings.MinSavingsPercentage,
|
||||||
StopAtBreakEven: auction.StopAtBreakEven));
|
StopAtBreakEven: auction.StopAtBreakEven));
|
||||||
|
|
||||||
if (!budget.CanBid)
|
if (!budget.CanBid)
|
||||||
@@ -151,8 +181,8 @@ namespace AutoBidder.Engine.Backtest
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
strategy.UpdateHeatMetric(auction, options.Settings, options.Username);
|
strategy.UpdateHeatMetric(auction, options.EffectiveSettings, options.Username);
|
||||||
var decision = strategy.ShouldPlaceBid(auction, state, options.Settings, options.Username);
|
var decision = strategy.ShouldPlaceBid(auction, state, options.EffectiveSettings, options.Username);
|
||||||
|
|
||||||
if (!decision.ShouldBid)
|
if (!decision.ShouldBid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -566,6 +566,47 @@ namespace AutoBidder.Models
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsInSoftRetreat { get; set; } = false;
|
public bool IsInSoftRetreat { get; set; } = false;
|
||||||
|
|
||||||
|
// ── Duello con l'autopuntata avversaria ──────────────────────────
|
||||||
|
// Vedi Utilities/AutoBidDuel per il perche' e per i numeri misurati.
|
||||||
|
|
||||||
|
/// <summary>Istante dell'ultima puntata nostra, per misurare quanto tarda la risposta.</summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public DateTime? LastMyBidAtUtc { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Risposte avversarie di fila con la firma dell'autopuntata (~6 s). Oltre la
|
||||||
|
/// soglia si smette di puntare su quest'asta: vedi <see cref="AutoBidDuelDetected"/>.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int AutoResponsesInARow { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Riconosciuta un'autopuntata armata dall'altra parte.</summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool AutoBidDuelDetected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registra la risposta avversaria arrivata adesso e aggiorna il conteggio.
|
||||||
|
/// Restituisce true se proprio con questa risposta il duello viene riconosciuto,
|
||||||
|
/// cosi' chi chiama puo' scriverlo nel registro una volta sola.
|
||||||
|
/// </summary>
|
||||||
|
public bool NoteOpponentResponse(DateTime nowUtc, int soglia)
|
||||||
|
{
|
||||||
|
if (LastMyBidAtUtc is not { } miaUltima) return false;
|
||||||
|
|
||||||
|
var ritardo = (nowUtc - miaUltima).TotalSeconds;
|
||||||
|
|
||||||
|
// Consumata: la prossima risposta va misurata dalla prossima puntata nostra.
|
||||||
|
LastMyBidAtUtc = null;
|
||||||
|
|
||||||
|
AutoResponsesInARow = Utilities.AutoBidDuel.Aggiorna(AutoResponsesInARow, ritardo);
|
||||||
|
|
||||||
|
if (AutoBidDuelDetected) return false;
|
||||||
|
if (!Utilities.AutoBidDuel.DuelloRiconosciuto(AutoResponsesInARow, soglia)) return false;
|
||||||
|
|
||||||
|
AutoBidDuelDetected = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contatore puntate effettuate in questa sessione su questa asta
|
/// Contatore puntate effettuate in questa sessione su questa asta
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -663,6 +704,13 @@ namespace AutoBidder.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? SoftRetreatEnabledOverride { get; set; }
|
public bool? SoftRetreatEnabledOverride { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override per questa asta della sospensione a fascia oraria.
|
||||||
|
/// <c>null</c> = come dice il prodotto, e se il prodotto tace come dicono le
|
||||||
|
/// impostazioni globali. Vedi <see cref="Utilities.BiddingHours"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool? QuietHoursEnabledOverride { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Override: limite puntate per questa asta
|
/// Override: limite puntate per questa asta
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -14,11 +14,37 @@ namespace AutoBidder.Models
|
|||||||
[JsonPropertyName("Price")]
|
[JsonPropertyName("Price")]
|
||||||
public decimal Price { get; set; }
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Puntata fatta dall'autopuntata del sito: scatta a 2 secondi dalla fine.</summary>
|
||||||
|
public const string TipoAuto = "Auto";
|
||||||
|
|
||||||
|
/// <summary>Puntata fatta a mano. Vale 3,6 volte una automatica: vedi <see cref="Utilities.AutoBidDuel"/>.</summary>
|
||||||
|
public const string TipoManuale = "Manuale";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tipo di puntata (Auto/Manuale)
|
/// Il tipo non lo ha detto il server.
|
||||||
|
///
|
||||||
|
/// <para>Serve perché non tutte le puntate in elenco vengono dallo storico di
|
||||||
|
/// <c>data.php</c>: quando il motore si accorge di un cambio di prezzo fra due
|
||||||
|
/// interrogazioni ne ricostruisce una da sé, e di quella il tipo non lo sa. Prima
|
||||||
|
/// venivano marcate "Auto" comunque, il che faceva contare come automatiche delle
|
||||||
|
/// puntate su cui nessuno aveva detto niente.</para>
|
||||||
|
/// </summary>
|
||||||
|
public const string TipoSconosciuto = "—";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tipo di puntata: <see cref="TipoAuto"/>, <see cref="TipoManuale"/> oppure
|
||||||
|
/// <see cref="TipoSconosciuto"/>. Lo dichiara Bidoo nello storico dell'asta.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("BidType")]
|
[JsonPropertyName("BidType")]
|
||||||
public string BidType { get; set; } = "Auto";
|
public string BidType { get; set; } = TipoSconosciuto;
|
||||||
|
|
||||||
|
/// <summary>Vero solo se il server ha davvero detto che era automatica.</summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool IsAuto => string.Equals(BidType, TipoAuto, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>Vero solo se il server ha davvero detto che era manuale.</summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool IsManual => string.Equals(BidType, TipoManuale, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timestamp della puntata (Unix timestamp)
|
/// Timestamp della puntata (Unix timestamp)
|
||||||
@@ -55,7 +81,7 @@ namespace AutoBidder.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indica se la puntata è stata fatta dall'utente corrente
|
/// Indica se la puntata � stata fatta dall'utente corrente
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsMyBid { get; set; }
|
public bool IsMyBid { get; set; }
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ namespace AutoBidder.Models
|
|||||||
/// <summary>Prezzo minimo sotto il quale non puntare. <c>null</c> = predefinito.</summary>
|
/// <summary>Prezzo minimo sotto il quale non puntare. <c>null</c> = predefinito.</summary>
|
||||||
public double? MinPrice { get; set; }
|
public double? MinPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sospendere le puntate nella fascia oraria. <c>null</c> = come le impostazioni
|
||||||
|
/// generali. Un'asta può a sua volta scavalcare questo valore.
|
||||||
|
/// Vedi <see cref="Utilities.BiddingHours"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool? QuietHoursEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>Prezzo oltre il quale lasciar perdere l'asta. <c>null</c> = predefinito.</summary>
|
/// <summary>Prezzo oltre il quale lasciar perdere l'asta. <c>null</c> = predefinito.</summary>
|
||||||
public double? MaxPrice { get; set; }
|
public double? MaxPrice { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Ottimo! Ora altre modifiche:
|
||||||
|
- Aggiungi l'opzione di default già attiva che mi permette di cancellare dall'elenco un'asta terminata. Aggiungere altre opzioni per non eliminare quelle su cui ho puntato ma che sono terminate in modo da verificarle a mano
|
||||||
@@ -111,6 +111,53 @@ namespace AutoBidder.Net
|
|||||||
/// <summary>Aggiorna i limiti di ritmo a caldo, senza ricreare il trasporto.</summary>
|
/// <summary>Aggiorna i limiti di ritmo a caldo, senza ricreare il trasporto.</summary>
|
||||||
public void Configure(double requestsPerSecond) => _limiter.SetRate(requestsPerSecond);
|
public void Configure(double requestsPerSecond) => _limiter.SetRate(requestsPerSecond);
|
||||||
|
|
||||||
|
// ── Freno dettato dal server ─────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Senza tetto fisso serve qualcosa che reagisca, altrimenti l'unico segnale di
|
||||||
|
// "troppe richieste" sarebbe un blocco dell'account. Un 429 o un 503 mettono in
|
||||||
|
// pausa il polling per un secondo, raddoppiando fino a otto se insistono, e la
|
||||||
|
// pausa si azzera al primo giro andato bene.
|
||||||
|
//
|
||||||
|
// Le puntate hanno priorità Critical e non passano da qui: se il momento giusto
|
||||||
|
// è adesso, un freno preventivo costerebbe l'asta.
|
||||||
|
|
||||||
|
private const int FrenoMinimoMs = 1000;
|
||||||
|
private const int FrenoMassimoMs = 8000;
|
||||||
|
|
||||||
|
private long _frenoFinoA;
|
||||||
|
private int _frenoCorrenteMs = FrenoMinimoMs;
|
||||||
|
|
||||||
|
/// <summary>Millisecondi di pausa ancora da scontare. Zero quando si va liberi.</summary>
|
||||||
|
public int FrenoResiduoMs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var mancano = (Interlocked.Read(ref _frenoFinoA) - Stopwatch.GetTimestamp())
|
||||||
|
* 1000 / Stopwatch.Frequency;
|
||||||
|
return mancano > 0 ? (int)mancano : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Frena()
|
||||||
|
{
|
||||||
|
var durata = Volatile.Read(ref _frenoCorrenteMs);
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref _frenoFinoA,
|
||||||
|
Stopwatch.GetTimestamp() + durata * Stopwatch.Frequency / 1000);
|
||||||
|
|
||||||
|
Volatile.Write(ref _frenoCorrenteMs, Math.Min(FrenoMassimoMs, durata * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Rilascia() => Volatile.Write(ref _frenoCorrenteMs, FrenoMinimoMs);
|
||||||
|
|
||||||
|
private ValueTask RispettaFrenoAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var mancano = FrenoResiduoMs;
|
||||||
|
return mancano <= 0
|
||||||
|
? ValueTask.CompletedTask
|
||||||
|
: new ValueTask(Task.Delay(mancano, ct));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Costruisce una GET già completa di header di autenticazione e di contesto.</summary>
|
/// <summary>Costruisce una GET già completa di header di autenticazione e di contesto.</summary>
|
||||||
public HttpRequestMessage BuildGet(string url, string? referer = null, bool ajax = true)
|
public HttpRequestMessage BuildGet(string url, string? referer = null, bool ajax = true)
|
||||||
{
|
{
|
||||||
@@ -206,6 +253,7 @@ namespace AutoBidder.Net
|
|||||||
|
|
||||||
if (!critical)
|
if (!critical)
|
||||||
{
|
{
|
||||||
|
await RispettaFrenoAsync(ct).ConfigureAwait(false);
|
||||||
await _limiter.WaitAsync(ct).ConfigureAwait(false);
|
await _limiter.WaitAsync(ct).ConfigureAwait(false);
|
||||||
await _concurrency.WaitAsync(ct).ConfigureAwait(false);
|
await _concurrency.WaitAsync(ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -224,6 +272,12 @@ namespace AutoBidder.Net
|
|||||||
|
|
||||||
if (!response.IsSuccessStatusCode) Interlocked.Increment(ref _requestsFailed);
|
if (!response.IsSuccessStatusCode) Interlocked.Increment(ref _requestsFailed);
|
||||||
|
|
||||||
|
// Tolto il tetto fisso, il freno lo dà il server. 429 e 503 sono il modo in
|
||||||
|
// cui dice "stai chiedendo troppo": si smette per un attimo invece di
|
||||||
|
// insistere finché non arriva un blocco vero. Le puntate non passano di qui.
|
||||||
|
if ((int)response.StatusCode is 429 or 503) Frena();
|
||||||
|
else if (response.IsSuccessStatusCode) Rilascia();
|
||||||
|
|
||||||
return new HttpOutcome
|
return new HttpOutcome
|
||||||
{
|
{
|
||||||
Success = response.IsSuccessStatusCode,
|
Success = response.IsSuccessStatusCode,
|
||||||
@@ -304,9 +358,20 @@ namespace AutoBidder.Net
|
|||||||
private double _tokens;
|
private double _tokens;
|
||||||
private long _lastTicks;
|
private long _lastTicks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zero o negativo = nessun limite: il secchiello si toglie di mezzo del tutto.
|
||||||
|
///
|
||||||
|
/// <para>Serve perché il tetto globale era il vero collo di bottiglia. Misurato sui
|
||||||
|
/// registri: con 24 aste seguite insieme e 40 richieste al secondo di tetto, la
|
||||||
|
/// distribuzione delle richieste era tagliata netta sul limite (p90 39, p99 41) e
|
||||||
|
/// ogni asta finiva interrogata ogni ~596 ms invece dei 220 configurati — proprio
|
||||||
|
/// negli ultimi secondi, dove quel ritardo costa l'asta.</para>
|
||||||
|
/// </summary>
|
||||||
|
private bool Illimitato => _ratePerSecond <= 0;
|
||||||
|
|
||||||
public TokenBucket(double ratePerSecond, int capacity)
|
public TokenBucket(double ratePerSecond, int capacity)
|
||||||
{
|
{
|
||||||
_ratePerSecond = Math.Max(1, ratePerSecond);
|
_ratePerSecond = ratePerSecond;
|
||||||
_capacity = Math.Max(1, capacity);
|
_capacity = Math.Max(1, capacity);
|
||||||
_tokens = _capacity;
|
_tokens = _capacity;
|
||||||
_lastTicks = Stopwatch.GetTimestamp();
|
_lastTicks = Stopwatch.GetTimestamp();
|
||||||
@@ -316,8 +381,8 @@ namespace AutoBidder.Net
|
|||||||
{
|
{
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
_ratePerSecond = Math.Max(1, ratePerSecond);
|
_ratePerSecond = ratePerSecond;
|
||||||
_capacity = Math.Max(8, _ratePerSecond);
|
_capacity = Math.Max(8, Math.Abs(_ratePerSecond));
|
||||||
if (_tokens > _capacity) _tokens = _capacity;
|
if (_tokens > _capacity) _tokens = _capacity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,6 +393,8 @@ namespace AutoBidder.Net
|
|||||||
|
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
|
if (Illimitato) return ValueTask.CompletedTask;
|
||||||
|
|
||||||
var now = Stopwatch.GetTimestamp();
|
var now = Stopwatch.GetTimestamp();
|
||||||
var elapsedSeconds = (now - _lastTicks) / (double)Stopwatch.Frequency;
|
var elapsedSeconds = (now - _lastTicks) / (double)Stopwatch.Frequency;
|
||||||
_lastTicks = now;
|
_lastTicks = now;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -620,7 +620,7 @@ namespace AutoBidder.Services
|
|||||||
Username = state.LastBidder,
|
Username = state.LastBidder,
|
||||||
Price = statePrice,
|
Price = statePrice,
|
||||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
BidType = "Auto"
|
BidType = BidHistoryEntry.TipoSconosciuto
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1193,10 +1193,20 @@ namespace AutoBidder.Services
|
|||||||
Username = currentBidder,
|
Username = currentBidder,
|
||||||
Price = statePrice,
|
Price = statePrice,
|
||||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
BidType = "Auto"
|
BidType = BidHistoryEntry.TipoSconosciuto
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Una puntata avversaria: se arriva col ritardo tipico dell'autopuntata
|
||||||
|
// (~6 s dalla nostra) alimenta il conteggio del duello.
|
||||||
|
var impostazioni = SettingsManager.Load();
|
||||||
|
if (impostazioni.AutoBidDuelWithdrawEnabled &&
|
||||||
|
auction.NoteOpponentResponse(DateTime.UtcNow, impostazioni.AutoBidDuelResponses))
|
||||||
|
{
|
||||||
|
auction.AddLog(AutoBidDuel.Spiegazione(auction.AutoResponsesInARow),
|
||||||
|
AuctionLogLevel.Warning, AuctionLogCategory.Strategy);
|
||||||
|
}
|
||||||
|
|
||||||
// I conteggi per utente restano a carico di UpdateBidderStatsFromRecentBids,
|
// I conteggi per utente restano a carico di UpdateBidderStatsFromRecentBids,
|
||||||
// unico punto che li tocca: contarli anche qui li raddoppierebbe.
|
// unico punto che li tocca: contarli anche qui li raddoppierebbe.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,46 @@ namespace AutoBidder.Services
|
|||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0. DUELLO CON L'AUTOPUNTATA — il primo controllo, perché è quello che
|
||||||
|
// costa di più sbagliare.
|
||||||
|
//
|
||||||
|
// L'autopuntata del sito scatta a 2 secondi e si riarma ogni volta che il suo
|
||||||
|
// padrone perde la testa: risponde sempre, risponde in ~6 secondi, e non si
|
||||||
|
// stanca. Contro di lei non esiste attrito che si possa vincere — si
|
||||||
|
// scambiano puntate una a una finché uno dei due esaurisce il credito.
|
||||||
|
//
|
||||||
|
// Nelle prove reali il motore ci ha lasciato 191 puntate su tre aste per zero
|
||||||
|
// vittorie, con un ciclo regolare da 13,0 secondi (7 di attesa nostra più 6
|
||||||
|
// della sua risposta). Rigiocando i dossier con questa regola, quelle aste
|
||||||
|
// sarebbero costate una manciata di puntate senza perdere nessuna vittoria,
|
||||||
|
// perché vittorie non ce n'erano.
|
||||||
|
if (settings.AutoBidDuelWithdrawEnabled && auction.AutoBidDuelDetected)
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = Utilities.AutoBidDuel.Spiegazione(auction.AutoResponsesInARow);
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0-bis. FASCIA ORARIA SOSPESA.
|
||||||
|
//
|
||||||
|
// L'asta NON viene fermata: resta Attiva, continua a essere seguita e
|
||||||
|
// riprende a puntare da sola alla fine della fascia. È voluto — le aste
|
||||||
|
// lunghe attraversano la fascia, e fermarle davvero significherebbe
|
||||||
|
// perderle invece che risparmiare.
|
||||||
|
//
|
||||||
|
// Su 6060 aste concluse: chiusura mediana al 4,6% del valore alle 12 e al
|
||||||
|
// 5,3% fra le 10 e le 13, contro l'8,6% a mezzanotte e il 9,7% alle 9, con
|
||||||
|
// 26-29 puntate per vincere invece di 21-24. Quasi il doppio del costo per
|
||||||
|
// lo stesso oggetto.
|
||||||
|
if (QuietHoursApply(auction, settings) &&
|
||||||
|
Utilities.BiddingHours.IsQuiet(DateTime.Now, settings.QuietHoursStart, settings.QuietHoursEnd))
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = Utilities.BiddingHours.Spiegazione(
|
||||||
|
settings.QuietHoursStart, settings.QuietHoursEnd);
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
// ? RIMOSSO: Entry Point - Era sbagliato!
|
// ? RIMOSSO: Entry Point - Era sbagliato!
|
||||||
// I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
|
// I limiti MinPrice/MaxPrice impostati dall'utente sono RIGIDI.
|
||||||
// Se l'utente imposta MaxPrice=2�, vuole puntare FINO A 2�, non fino al 70%!
|
// Se l'utente imposta MaxPrice=2�, vuole puntare FINO A 2�, non fino al 70%!
|
||||||
@@ -466,6 +506,26 @@ namespace AutoBidder.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registra una puntata effettuata (per tracking)
|
/// Registra una puntata effettuata (per tracking)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// La sospensione a fascia oraria vale per quest'asta?
|
||||||
|
///
|
||||||
|
/// <para>Tre livelli, dal più specifico al più generale: l'asta scavalca il
|
||||||
|
/// prodotto, il prodotto scavalca le impostazioni generali. Un <c>null</c> non
|
||||||
|
/// significa "no", significa "non ho un'opinione": è la differenza che permette di
|
||||||
|
/// spegnere il filtro su una singola asta senza toccare il resto.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static bool QuietHoursApply(AuctionInfo auction, AppSettings settings)
|
||||||
|
{
|
||||||
|
if (auction?.QuietHoursEnabledOverride is { } perAsta) return perAsta;
|
||||||
|
|
||||||
|
// Le aste del monitor non portano con sé l'id prodotto di Bidoo: la scheda si
|
||||||
|
// ritrova dal nome, che è la via prevista da WatchedProductsStore.
|
||||||
|
var scheda = WatchedProductsStore.FindByName(auction?.Name);
|
||||||
|
if (scheda?.QuietHoursEnabled is { } perProdotto) return perProdotto;
|
||||||
|
|
||||||
|
return settings.QuietHoursEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public void RecordBidAttempt(AuctionInfo auction, bool success, bool collision = false)
|
public void RecordBidAttempt(AuctionInfo auction, bool success, bool collision = false)
|
||||||
{
|
{
|
||||||
auction.SessionBidCount++;
|
auction.SessionBidCount++;
|
||||||
@@ -475,6 +535,11 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
auction.SuccessfulBidCount++;
|
auction.SuccessfulBidCount++;
|
||||||
auction.ConsecutiveCollisions = 0;
|
auction.ConsecutiveCollisions = 0;
|
||||||
|
|
||||||
|
// Da qui si misura quanto tarda la risposta avversaria: e' l'unico punto
|
||||||
|
// attraversato sia dal motore dal vivo sia dalla rigiocata sui dossier,
|
||||||
|
// quindi la regola del duello vede le stesse cose in entrambi.
|
||||||
|
auction.LastMyBidAtUtc = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -192,8 +192,10 @@ namespace AutoBidder.Services
|
|||||||
var settings = config.PromoHarvest;
|
var settings = config.PromoHarvest;
|
||||||
var text = body ?? "";
|
var text = body ?? "";
|
||||||
|
|
||||||
if (FreeBidsPageParser.LooksLikeLogin(text, config))
|
// Il riconoscimento della pagina di accesso scattava anche su pagine che di
|
||||||
return new RedeemResult(code, false, "sessione scaduta: risposta la pagina di accesso");
|
// accesso non erano: nei registri ogni riscatto risultava fallito mentre le
|
||||||
|
// puntate arrivavano. Aprire il collegamento e' il riscatto, e il numero vero lo
|
||||||
|
// da' la differenza di saldo. Vedi FreeBidsResponseValidator.Validate.
|
||||||
|
|
||||||
var success = settings.SuccessMarkers
|
var success = settings.SuccessMarkers
|
||||||
.FirstOrDefault(m => !string.IsNullOrWhiteSpace(m) &&
|
.FirstOrDefault(m => !string.IsNullOrWhiteSpace(m) &&
|
||||||
|
|||||||
@@ -35,10 +35,51 @@ namespace AutoBidder.Services
|
|||||||
/// <summary>Oltre questa lunghezza la risposta è trattata come pagina, non come messaggio.</summary>
|
/// <summary>Oltre questa lunghezza la risposta è trattata come pagina, non come messaggio.</summary>
|
||||||
private const int FragmentMaxLength = 2000;
|
private const int FragmentMaxLength = 2000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aprire il collegamento <b>è</b> il riscatto: non si giudica cosa risponde.
|
||||||
|
///
|
||||||
|
/// <para>Il giudizio qui sotto è stato disattivato perché sbagliava nel verso
|
||||||
|
/// peggiore. Nei registri di tre giorni ogni singolo riscatto risultava «sessione
|
||||||
|
/// scaduta: risposta la pagina di accesso» — 6 trovati, 0 riscossi, 6 non riusciti,
|
||||||
|
/// giro dopo giro — mentre le puntate arrivavano lo stesso. Il riconoscimento della
|
||||||
|
/// pagina di accesso scattava su pagine che di accesso non erano, e il risultato era
|
||||||
|
/// un servizio che dichiarava di non funzionare mentre funzionava.</para>
|
||||||
|
///
|
||||||
|
/// <para>Un premio si riscuote una volta sola: se la risposta viene letta male, il
|
||||||
|
/// codice resta segnato come non preso e viene riaperto per sempre, oppure viene
|
||||||
|
/// dato per riscosso quando non lo era. Fra i due errori, insistere su un premio già
|
||||||
|
/// preso non costa nulla — mentre insistere per sempre riempie il registro di falsi
|
||||||
|
/// allarmi. Quindi: si apre, si scrive che è stato aperto, e il numero vero lo dà la
|
||||||
|
/// differenza di saldo, che è l'unica misura che non mente.</para>
|
||||||
|
///
|
||||||
|
/// <para>La logica di giudizio resta qui sotto, raggiungibile con
|
||||||
|
/// <see cref="GiudicaRisposta"/>, per i test e per chi volesse riaccenderla.</para>
|
||||||
|
/// </summary>
|
||||||
public static FreeBidsResponseVerdict Validate(
|
public static FreeBidsResponseVerdict Validate(
|
||||||
FreeBidsSiteConfig config,
|
FreeBidsSiteConfig config,
|
||||||
int statusCode,
|
int statusCode,
|
||||||
string? body)
|
string? body)
|
||||||
|
{
|
||||||
|
// Un errore di trasporto è l'unica cosa che si guarda ancora: lì il collegamento
|
||||||
|
// non è stato aperto affatto, e dirlo non è un giudizio sul contenuto.
|
||||||
|
if (statusCode is < 200 or > 299)
|
||||||
|
return new FreeBidsResponseVerdict(false, null, null, $"il sito ha risposto {statusCode}");
|
||||||
|
|
||||||
|
var dichiarate = FreeBidsPageParser.ReadGainedBids(body ?? "", (config ?? new FreeBidsSiteConfig()).Normalised());
|
||||||
|
|
||||||
|
return new FreeBidsResponseVerdict(true, dichiarate, null,
|
||||||
|
dichiarate.HasValue ? $"collegamento aperto (+{dichiarate} puntate dichiarate)"
|
||||||
|
: "collegamento aperto");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Il vecchio giudizio sul contenuto della risposta. Non più sul percorso normale:
|
||||||
|
/// vedi <see cref="Validate"/> per il perché.
|
||||||
|
/// </summary>
|
||||||
|
public static FreeBidsResponseVerdict GiudicaRisposta(
|
||||||
|
FreeBidsSiteConfig config,
|
||||||
|
int statusCode,
|
||||||
|
string? body)
|
||||||
{
|
{
|
||||||
config = (config ?? new FreeBidsSiteConfig()).Normalised();
|
config = (config ?? new FreeBidsSiteConfig()).Normalised();
|
||||||
|
|
||||||
|
|||||||
@@ -56,57 +56,89 @@ namespace AutoBidder.Services
|
|||||||
var failed = 0;
|
var failed = 0;
|
||||||
var skipped = 0;
|
var skipped = 0;
|
||||||
|
|
||||||
for (var i = 0; i < missing.Count; i++)
|
// ── Perché si salva a blocchi ────────────────────────────────
|
||||||
|
// Un Append per asta rilegge e riscrive l'intero storico ogni volta:
|
||||||
|
// su un recupero completo (4675 aste) erano minuti di solo rimescolamento
|
||||||
|
// JSON e svariati gigabyte scritti sul disco. Un blocco ogni cinquanta
|
||||||
|
// taglia i salvataggi di cinquanta volte senza mettere a rischio più di
|
||||||
|
// cinquanta richieste già pagate: quello che non è ancora stato salvato
|
||||||
|
// viene comunque scritto all'uscita, annullamento compreso.
|
||||||
|
const int DimensioneBlocco = 50;
|
||||||
|
var daSalvare = new List<CompletedAuctionRecord>(DimensioneBlocco);
|
||||||
|
|
||||||
|
void Deposita()
|
||||||
{
|
{
|
||||||
if (ct.IsCancellationRequested) break;
|
if (daSalvare.Count == 0) return;
|
||||||
|
|
||||||
var record = missing[i];
|
CompletedAuctionsStore.AppendMany(daSalvare);
|
||||||
|
daSalvare.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
for (var i = 0; i < missing.Count; i++)
|
||||||
{
|
{
|
||||||
var state = await FetchAsync(record.AuctionId, ct).ConfigureAwait(false);
|
if (ct.IsCancellationRequested) break;
|
||||||
|
|
||||||
if (state?.WinnerBidsTotal is null)
|
var record = missing[i];
|
||||||
{
|
|
||||||
skipped++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
record.WinnerBidsPaid = state.WinnerBidsPaid;
|
|
||||||
record.WinnerBidsFree = state.WinnerBidsFree;
|
|
||||||
|
|
||||||
var check = AuctionIntegrity.CheckWinnerBids(record);
|
try
|
||||||
if (check.IsTrustworthy)
|
{
|
||||||
|
var state = await FetchAsync(record.AuctionId, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (state?.WinnerBidsTotal is null)
|
||||||
{
|
{
|
||||||
// Si riscrive solo il record toccato: Append sostituisce
|
skipped++;
|
||||||
// quello con lo stesso id.
|
|
||||||
CompletedAuctionsStore.Append(record);
|
|
||||||
updated++;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Il valore c'è ma non regge il confronto col prezzo: non si
|
record.WinnerBidsPaid = state.WinnerBidsPaid;
|
||||||
// salva, e si dice perché. Meglio un buco che una media storta.
|
record.WinnerBidsFree = state.WinnerBidsFree;
|
||||||
record.WinnerBidsPaid = null;
|
|
||||||
record.WinnerBidsFree = null;
|
var check = AuctionIntegrity.CheckWinnerBids(record);
|
||||||
skipped++;
|
if (check.IsTrustworthy)
|
||||||
OnLog?.Invoke($"[RECUPERO] {record.Name}: scartato ({check.Reason})");
|
{
|
||||||
|
// Va nel blocco: AppendMany sostituisce i record con lo
|
||||||
|
// stesso id, quindi il risultato è identico a un Append
|
||||||
|
// per ciascuno, con un salvataggio invece di cinquanta.
|
||||||
|
daSalvare.Add(record);
|
||||||
|
if (daSalvare.Count >= DimensioneBlocco) Deposita();
|
||||||
|
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Il valore c'è ma non regge il confronto col prezzo: non
|
||||||
|
// si salva, e si dice perché. Meglio un buco che una media
|
||||||
|
// storta.
|
||||||
|
record.WinnerBidsPaid = null;
|
||||||
|
record.WinnerBidsFree = null;
|
||||||
|
skipped++;
|
||||||
|
OnLog?.Invoke($"[RECUPERO] {record.Name}: scartato ({check.Reason})");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
catch (OperationCanceledException)
|
||||||
catch (OperationCanceledException)
|
{
|
||||||
{
|
break;
|
||||||
break;
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
catch (Exception ex)
|
{
|
||||||
{
|
failed++;
|
||||||
failed++;
|
OnLog?.Invoke($"[RECUPERO] {record.AuctionId}: {ex.Message}");
|
||||||
OnLog?.Invoke($"[RECUPERO] {record.AuctionId}: {ex.Message}");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
OnProgress?.Invoke(i + 1, missing.Count, updated);
|
OnProgress?.Invoke(i + 1, missing.Count, updated);
|
||||||
|
|
||||||
if (!await Wait.DelayAsync(DelayBetweenRequestsMs, ct).ConfigureAwait(false)) break;
|
if (!await Wait.DelayAsync(DelayBetweenRequestsMs, ct).ConfigureAwait(false)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Anche se si esce per annullamento o per un errore: le richieste già
|
||||||
|
// fatte sono state pagate al server, buttarne i risultati significa
|
||||||
|
// rifarle al giro dopo.
|
||||||
|
Deposita();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Result(missing.Count, updated, failed, skipped);
|
return new Result(missing.Count, updated, failed, skipped);
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riconoscimento dell'autopuntata avversaria.
|
||||||
|
///
|
||||||
|
/// <para>La proprieta' da difendere non e' "riconosce le macchine": e' che <b>non si ritira
|
||||||
|
/// da un'asta che si poteva vincere</b>. Sbagliare in quel verso costa un'asta; sbagliare
|
||||||
|
/// nell'altro costa cinque puntate. Da qui la scelta di azzerare il conteggio a ogni
|
||||||
|
/// risposta fuori firma invece di scalarlo.</para>
|
||||||
|
///
|
||||||
|
/// <para>I numeri vengono dai dossier: 2.799.681 intervalli fra puntate consecutive in 5037
|
||||||
|
/// aste, mediana 6,0 s, quartili 5,3 e 7,1 s.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class AutoBidDuelTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(6.0, true)] // la mediana misurata
|
||||||
|
[InlineData(5.3, true)] // primo quartile
|
||||||
|
[InlineData(7.1, true)] // terzo quartile
|
||||||
|
[InlineData(3.4, false)] // troppo svelta: qualcuno che stava guardando
|
||||||
|
[InlineData(13.0, false)] // troppo lenta
|
||||||
|
[InlineData(0.0, false)] // nessun ritardo: non e' una risposta
|
||||||
|
[InlineData(-2.0, false)] // prima della nostra puntata: non e' una risposta
|
||||||
|
public void La_firma_e_sei_secondi_piu_o_meno_due_e_mezzo(double ritardo, bool atteso)
|
||||||
|
{
|
||||||
|
Assert.Equal(atteso, AutoBidDuel.ERispostaAutomatica(ritardo));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Le_risposte_in_firma_si_sommano()
|
||||||
|
{
|
||||||
|
var n = 0;
|
||||||
|
for (var i = 0; i < 5; i++) n = AutoBidDuel.Aggiorna(n, 6.0);
|
||||||
|
|
||||||
|
Assert.Equal(5, n);
|
||||||
|
Assert.True(AutoBidDuel.DuelloRiconosciuto(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Una_risposta_fuori_firma_azzera_tutto()
|
||||||
|
{
|
||||||
|
// Il punto della regola: quattro indizi piu' uno contrario non fanno quattro
|
||||||
|
// indizi. Serve una sequenza pulita, perche' ritirarsi per sbaglio costa l'asta.
|
||||||
|
var n = 0;
|
||||||
|
for (var i = 0; i < 4; i++) n = AutoBidDuel.Aggiorna(n, 6.0);
|
||||||
|
Assert.Equal(4, n);
|
||||||
|
|
||||||
|
n = AutoBidDuel.Aggiorna(n, 2.0);
|
||||||
|
|
||||||
|
Assert.Equal(0, n);
|
||||||
|
Assert.False(AutoBidDuel.DuelloRiconosciuto(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Sotto_la_soglia_non_ci_si_ritira()
|
||||||
|
{
|
||||||
|
var n = 0;
|
||||||
|
for (var i = 0; i < 4; i++) n = AutoBidDuel.Aggiorna(n, 6.0);
|
||||||
|
|
||||||
|
Assert.False(AutoBidDuel.DuelloRiconosciuto(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lo stato sull'asta ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static AuctionInfo Asta() => new() { AuctionId = "1", Name = "prova" };
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Il_duello_si_riconosce_una_volta_sola()
|
||||||
|
{
|
||||||
|
var a = Asta();
|
||||||
|
var t = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var riconosciuto = 0;
|
||||||
|
for (var i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
a.LastMyBidAtUtc = t;
|
||||||
|
t = t.AddSeconds(6);
|
||||||
|
if (a.NoteOpponentResponse(t, 5)) riconosciuto++;
|
||||||
|
t = t.AddSeconds(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(a.AutoBidDuelDetected);
|
||||||
|
Assert.Equal(1, riconosciuto); // non una volta per ogni risposta successiva
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Senza_una_nostra_puntata_non_si_misura_niente()
|
||||||
|
{
|
||||||
|
// Una risposta avversaria che non risponde a noi non dice nulla sulla loro
|
||||||
|
// autopuntata: puo' essere un terzo che punta per conto suo.
|
||||||
|
var a = Asta();
|
||||||
|
|
||||||
|
Assert.False(a.NoteOpponentResponse(DateTime.UtcNow, 5));
|
||||||
|
Assert.Equal(0, a.AutoResponsesInARow);
|
||||||
|
Assert.False(a.AutoBidDuelDetected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ogni_risposta_si_misura_dalla_puntata_sua()
|
||||||
|
{
|
||||||
|
// Consumato il riferimento, una seconda risposta di fila non deve contare due
|
||||||
|
// volte lo stesso intervallo.
|
||||||
|
var a = Asta();
|
||||||
|
var t = DateTime.UtcNow;
|
||||||
|
|
||||||
|
a.LastMyBidAtUtc = t;
|
||||||
|
a.NoteOpponentResponse(t.AddSeconds(6), 5);
|
||||||
|
Assert.Equal(1, a.AutoResponsesInARow);
|
||||||
|
|
||||||
|
a.NoteOpponentResponse(t.AddSeconds(12), 5);
|
||||||
|
Assert.Equal(1, a.AutoResponsesInARow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_avversario_umano_irregolare_non_viene_scambiato_per_macchina()
|
||||||
|
{
|
||||||
|
var a = Asta();
|
||||||
|
var t = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Ritardi veri, presi dalla distribuzione misurata fuori dalla firma.
|
||||||
|
foreach (var ritardo in new[] { 6.0, 3.1, 6.2, 9.4, 5.9, 2.2, 6.0 })
|
||||||
|
{
|
||||||
|
a.LastMyBidAtUtc = t;
|
||||||
|
t = t.AddSeconds(ritardo);
|
||||||
|
a.NoteOpponentResponse(t, 5);
|
||||||
|
t = t.AddSeconds(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(a.AutoBidDuelDetected);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -309,4 +309,73 @@ public class StatsMaintenanceTests
|
|||||||
Assert.Equal(report.Kept, kept.Count);
|
Assert.Equal(report.Kept, kept.Count);
|
||||||
Assert.Equal(report.Removed, list.Count - kept.Count);
|
Assert.Equal(report.Removed, list.Count - kept.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Aste seguite solo in parte ───────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Il prezzo finale in centesimi e' il totale delle puntate dell'asta: confrontarlo
|
||||||
|
// con i reset osservati dice quanta asta si e' vista, senza bisogno di registrare
|
||||||
|
// nulla di nuovo. Vale quindi anche sullo storico raccolto prima della regola.
|
||||||
|
|
||||||
|
private static CompletedAuctionRecord Seguita(string id, double price, int resets)
|
||||||
|
{
|
||||||
|
var r = R(id: id, price: price, bidsFree: 1);
|
||||||
|
r.TotalResets = resets;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1.00, 100, 1.0)] // 1,00 EUR = 100 puntate, viste tutte
|
||||||
|
[InlineData(1.00, 50, 0.5)] // agganciata a meta'
|
||||||
|
[InlineData(1.00, 0, 0.0)] // vista solo la fine
|
||||||
|
public void La_copertura_e_i_reset_sul_totale_di_puntate(
|
||||||
|
double price, int resets, double attesa)
|
||||||
|
{
|
||||||
|
Assert.Equal(attesa, AuctionIntegrity.ObservedCoverage(price, resets)!.Value, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Senza_prezzo_la_copertura_non_si_puo_dire()
|
||||||
|
{
|
||||||
|
Assert.Null(AuctionIntegrity.ObservedCoverage(0, 42));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void La_copertura_non_supera_mai_uno()
|
||||||
|
{
|
||||||
|
// L'arrotondamento del prezzo puo' produrre qualche reset di troppo: non e'
|
||||||
|
// un'asta osservata piu' del possibile, e non deve diventare 103%.
|
||||||
|
Assert.Equal(1.0, AuctionIntegrity.ObservedCoverage(1.00, 103)!.Value, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Toglie_le_aste_seguite_sotto_la_soglia()
|
||||||
|
{
|
||||||
|
var list = new List<CompletedAuctionRecord>
|
||||||
|
{
|
||||||
|
Seguita("intera", 1.00, 100),
|
||||||
|
Seguita("quasi", 1.00, 95),
|
||||||
|
Seguita("meta", 1.00, 50),
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = new StatsMaintenance.CleanupOptions
|
||||||
|
{
|
||||||
|
MinObservedCoverage = 0.90
|
||||||
|
};
|
||||||
|
|
||||||
|
var kept = StatsMaintenance.Apply(list, options);
|
||||||
|
|
||||||
|
Assert.Equal(2, kept.Count);
|
||||||
|
Assert.DoesNotContain(kept, r => r.AuctionId == "meta");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Spenta_la_soglia_non_toglie_nulla_per_copertura()
|
||||||
|
{
|
||||||
|
var list = new List<CompletedAuctionRecord> { Seguita("meta", 1.00, 1) };
|
||||||
|
|
||||||
|
var report = StatsMaintenance.Preview(list,
|
||||||
|
new StatsMaintenance.CleanupOptions { MinObservedCoverage = null });
|
||||||
|
|
||||||
|
Assert.DoesNotContain("seguita solo in parte", report.ByReason.Keys);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fascia oraria sospesa.
|
||||||
|
///
|
||||||
|
/// <para>La proprieta' da difendere e' che la fascia <b>non fermi l'asta</b>: salta la
|
||||||
|
/// puntata e basta, perche' le aste lunghe attraversano la fascia e fermarle davvero
|
||||||
|
/// significherebbe perderle. Qui si prova il calcolo della fascia; che lo stato resti
|
||||||
|
/// Attiva e' garantito da come la regola e' collegata, non da questo file.</para>
|
||||||
|
///
|
||||||
|
/// <para>I numeri che motivano il predefinito 0-10: su 6060 aste concluse la chiusura
|
||||||
|
/// mediana e' il 4,6% del valore alle 12 e il 5,3% fra le 10 e le 13, contro l'8,6% a
|
||||||
|
/// mezzanotte e il 9,7% alle 9.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class BiddingHoursTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, true)] // mezzanotte: la peggiore, 8,6%
|
||||||
|
[InlineData(3, true)]
|
||||||
|
[InlineData(9, true)] // ultima ora dentro: 9,7%
|
||||||
|
[InlineData(10, false)] // si riparte: 4,9%
|
||||||
|
[InlineData(12, false)] // la migliore: 4,6%
|
||||||
|
[InlineData(23, false)]
|
||||||
|
public void La_fascia_predefinita_va_da_mezzanotte_alle_dieci(int ora, bool sospesa)
|
||||||
|
{
|
||||||
|
Assert.Equal(sospesa,
|
||||||
|
BiddingHours.IsQuiet(ora, BiddingHours.DefaultStart, BiddingHours.DefaultEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Inizio_incluso_e_fine_esclusa()
|
||||||
|
{
|
||||||
|
Assert.True(BiddingHours.IsQuiet(1, 1, 5));
|
||||||
|
Assert.True(BiddingHours.IsQuiet(4, 1, 5));
|
||||||
|
Assert.False(BiddingHours.IsQuiet(5, 1, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(22, true)]
|
||||||
|
[InlineData(23, true)]
|
||||||
|
[InlineData(0, true)]
|
||||||
|
[InlineData(5, true)]
|
||||||
|
[InlineData(6, false)]
|
||||||
|
[InlineData(12, false)]
|
||||||
|
public void Una_fascia_a_cavallo_di_mezzanotte_funziona(int ora, bool sospesa)
|
||||||
|
{
|
||||||
|
Assert.Equal(sospesa, BiddingHours.IsQuiet(ora, 22, 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Inizio_uguale_a_fine_significa_filtro_spento()
|
||||||
|
{
|
||||||
|
// L'alternativa sarebbe non puntare mai, che non e' quello che intende nessuno
|
||||||
|
// scrivendo due volte la stessa ora.
|
||||||
|
for (var ora = 0; ora < 24; ora++)
|
||||||
|
Assert.False(BiddingHours.IsQuiet(ora, 3, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(24, 0)]
|
||||||
|
[InlineData(25, 1)]
|
||||||
|
[InlineData(-1, 23)]
|
||||||
|
public void Le_ore_fuori_scala_si_riportano_dentro(int fuori, int atteso)
|
||||||
|
{
|
||||||
|
// Un valore scritto a mano nelle impostazioni non deve produrre una fascia che
|
||||||
|
// non sospende mai o che sospende sempre.
|
||||||
|
Assert.Equal(BiddingHours.IsQuiet(atteso, 0, 10), BiddingHours.IsQuiet(fuori, 0, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void La_spiegazione_dice_la_fascia_e_che_riprende_da_sola()
|
||||||
|
{
|
||||||
|
var testo = BiddingHours.Spiegazione(0, 10);
|
||||||
|
|
||||||
|
Assert.Contains("00:00-10:00", testo);
|
||||||
|
Assert.Contains("resta attiva", testo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void La_versione_con_data_usa_solo_l_ora()
|
||||||
|
{
|
||||||
|
var dentro = new DateTime(2026, 8, 7, 3, 45, 0);
|
||||||
|
var fuori = new DateTime(2026, 8, 7, 15, 45, 0);
|
||||||
|
|
||||||
|
Assert.True(BiddingHours.IsQuiet(dentro, 0, 10));
|
||||||
|
Assert.False(BiddingHours.IsQuiet(fuori, 0, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Archivio delle aste concluse: copia in memoria e salvataggio a blocchi.
|
||||||
|
///
|
||||||
|
/// <para>Sono le due modifiche che hanno tolto dal percorso caldo la rilettura dell'intero
|
||||||
|
/// file — ~72 ms su 5876 record, pagati sul thread dell'interfaccia a ogni asta che finisce.
|
||||||
|
/// La proprieta' da difendere e' che andare piu' veloce non cambi <b>cosa</b> viene salvato:
|
||||||
|
/// AppendMany deve dare lo stesso risultato di tanti Append, e la copia in memoria non deve
|
||||||
|
/// mai servire un contenuto che sul disco non c'e' piu'.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class CompletedAuctionsStoreTests
|
||||||
|
{
|
||||||
|
private static CompletedAuctionRecord R(string id, double price = 1.00, string? nome = null) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AuctionId = id,
|
||||||
|
Name = nome ?? $"Asta {id}",
|
||||||
|
ProductKey = "prova",
|
||||||
|
FinalPrice = price,
|
||||||
|
Winner = "tizio",
|
||||||
|
EndedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void Svuota() => CompletedAuctionsStore.ReplaceAll(new List<CompletedAuctionRecord>());
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Append_sostituisce_quella_con_lo_stesso_id()
|
||||||
|
{
|
||||||
|
Svuota();
|
||||||
|
|
||||||
|
CompletedAuctionsStore.Append(R("100", price: 1.00));
|
||||||
|
CompletedAuctionsStore.Append(R("100", price: 2.50));
|
||||||
|
|
||||||
|
var tutte = CompletedAuctionsStore.LoadAll();
|
||||||
|
|
||||||
|
Assert.Single(tutte);
|
||||||
|
Assert.Equal(2.50, tutte[0].FinalPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppendMany_da_lo_stesso_risultato_di_tanti_Append()
|
||||||
|
{
|
||||||
|
var lotto = new[] { R("a", 1), R("b", 2), R("c", 3) };
|
||||||
|
|
||||||
|
Svuota();
|
||||||
|
foreach (var r in lotto) CompletedAuctionsStore.Append(r);
|
||||||
|
var unoAllaVolta = CompletedAuctionsStore.LoadAll()
|
||||||
|
.OrderBy(r => r.AuctionId).Select(r => (r.AuctionId, r.FinalPrice)).ToList();
|
||||||
|
|
||||||
|
Svuota();
|
||||||
|
CompletedAuctionsStore.AppendMany(lotto);
|
||||||
|
var inBlocco = CompletedAuctionsStore.LoadAll()
|
||||||
|
.OrderBy(r => r.AuctionId).Select(r => (r.AuctionId, r.FinalPrice)).ToList();
|
||||||
|
|
||||||
|
Assert.Equal(unoAllaVolta, inBlocco);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppendMany_sostituisce_e_aggiunge_nello_stesso_giro()
|
||||||
|
{
|
||||||
|
Svuota();
|
||||||
|
CompletedAuctionsStore.AppendMany(new[] { R("x", 1), R("y", 2) });
|
||||||
|
|
||||||
|
CompletedAuctionsStore.AppendMany(new[] { R("y", 9), R("z", 3) });
|
||||||
|
|
||||||
|
var tutte = CompletedAuctionsStore.LoadAll().ToDictionary(r => r.AuctionId, r => r.FinalPrice);
|
||||||
|
|
||||||
|
Assert.Equal(3, tutte.Count);
|
||||||
|
Assert.Equal(9, tutte["y"]);
|
||||||
|
Assert.Equal(1, tutte["x"]);
|
||||||
|
Assert.Equal(3, tutte["z"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppendMany_senza_niente_da_fare_non_esplode()
|
||||||
|
{
|
||||||
|
Svuota();
|
||||||
|
CompletedAuctionsStore.AppendMany(Array.Empty<CompletedAuctionRecord>());
|
||||||
|
|
||||||
|
Assert.Empty(CompletedAuctionsStore.LoadAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void I_record_senza_id_restano_tutti()
|
||||||
|
{
|
||||||
|
// Un id vuoto non identifica niente: due record cosi' non sono lo stesso record,
|
||||||
|
// e accorparli farebbe sparire un'asta davvero conclusa.
|
||||||
|
Svuota();
|
||||||
|
CompletedAuctionsStore.AppendMany(new[] { R(""), R("") });
|
||||||
|
|
||||||
|
Assert.Equal(2, CompletedAuctionsStore.LoadAll().Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void La_copia_in_memoria_si_accorge_se_il_file_cambia_da_fuori()
|
||||||
|
{
|
||||||
|
Svuota();
|
||||||
|
CompletedAuctionsStore.Append(R("uno"));
|
||||||
|
Assert.Single(CompletedAuctionsStore.LoadAll());
|
||||||
|
|
||||||
|
// Ripristino di un backup o modifica a mano: la copia non deve continuare a
|
||||||
|
// servire il contenuto vecchio. La data si forza per non dipendere dalla
|
||||||
|
// granularita' con cui il file system aggiorna l'ora di scrittura.
|
||||||
|
File.WriteAllText(CompletedAuctionsStore.StorePath,
|
||||||
|
JsonSerializer.Serialize(new List<CompletedAuctionRecord> { R("due"), R("tre") }));
|
||||||
|
File.SetLastWriteTimeUtc(CompletedAuctionsStore.StorePath, DateTime.UtcNow.AddSeconds(5));
|
||||||
|
|
||||||
|
var dopo = CompletedAuctionsStore.LoadAll();
|
||||||
|
|
||||||
|
Assert.Equal(2, dopo.Count);
|
||||||
|
Assert.DoesNotContain(dopo, r => r.AuctionId == "uno");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dopo_ReplaceAll_si_legge_quello_nuovo()
|
||||||
|
{
|
||||||
|
Svuota();
|
||||||
|
CompletedAuctionsStore.AppendMany(new[] { R("vecchia") });
|
||||||
|
|
||||||
|
CompletedAuctionsStore.ReplaceAll(new List<CompletedAuctionRecord> { R("nuova") });
|
||||||
|
|
||||||
|
var tutte = CompletedAuctionsStore.LoadAll();
|
||||||
|
|
||||||
|
Assert.Single(tutte);
|
||||||
|
Assert.Equal("nuova", tutte[0].AuctionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rimozione automatica delle aste concluse dall'elenco del monitor.
|
||||||
|
///
|
||||||
|
/// <para>La proprieta' da difendere e' asimmetrica, e va nel verso della prudenza: togliere
|
||||||
|
/// un'asta solo osservata non costa niente, perche' lo storico la racconta meglio
|
||||||
|
/// dell'elenco; togliere un'asta su cui si e' speso significa perdere l'occasione di
|
||||||
|
/// controllare dove sono finiti dei soldi veri. Nel dubbio si trattiene.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class FinishedAuctionCleanupTests
|
||||||
|
{
|
||||||
|
private static FinishedAuctionCleanup.Verdict Decidi(
|
||||||
|
bool conclusa = true, bool vinta = false, int puntate = 0, bool fineVista = true,
|
||||||
|
FinishedAuctionCleanup.Options? opzioni = null) =>
|
||||||
|
FinishedAuctionCleanup.Decide(conclusa, vinta, puntate, fineVista,
|
||||||
|
opzioni ?? new FinishedAuctionCleanup.Options());
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_asta_persa_senza_mie_puntate_si_toglie()
|
||||||
|
{
|
||||||
|
Assert.True(Decidi().Remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_asta_ancora_in_corso_non_si_tocca()
|
||||||
|
{
|
||||||
|
var v = Decidi(conclusa: false);
|
||||||
|
|
||||||
|
Assert.False(v.Remove);
|
||||||
|
Assert.Contains("non conclusa", v.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Le_aste_su_cui_ho_puntato_restano()
|
||||||
|
{
|
||||||
|
// Sono quelle costate qualcosa, ed e' l'unico elenco in cui si vedono accanto ai
|
||||||
|
// loro dati: farle sparire da sole significa perdere il controllo a mano.
|
||||||
|
var v = Decidi(puntate: 12);
|
||||||
|
|
||||||
|
Assert.False(v.Remove);
|
||||||
|
Assert.Contains("12 puntate", v.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Le_vinte_restano_perche_vanno_confermate_su_Bidoo()
|
||||||
|
{
|
||||||
|
var v = Decidi(vinta: true);
|
||||||
|
|
||||||
|
Assert.False(v.Remove);
|
||||||
|
Assert.Contains("conferma", v.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Se_la_fine_non_e_stata_vista_l_asta_resta()
|
||||||
|
{
|
||||||
|
var v = Decidi(fineVista: false);
|
||||||
|
|
||||||
|
Assert.False(v.Remove);
|
||||||
|
Assert.Contains("fine non", v.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Una_vinta_su_cui_ho_puntato_resta_per_il_motivo_piu_importante()
|
||||||
|
{
|
||||||
|
// Due ragioni per trattenerla: quella che finisce nel registro deve essere la
|
||||||
|
// vittoria, non le puntate, perche' e' quella che richiede un'azione.
|
||||||
|
var v = Decidi(vinta: true, puntate: 30);
|
||||||
|
|
||||||
|
Assert.False(v.Remove);
|
||||||
|
Assert.Contains("vinta", v.Reason.ToLowerInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Spegnendo_la_rimozione_non_si_toglie_piu_niente()
|
||||||
|
{
|
||||||
|
var v = Decidi(opzioni: new FinishedAuctionCleanup.Options { Enabled = false });
|
||||||
|
|
||||||
|
Assert.False(v.Remove);
|
||||||
|
Assert.Contains("spenta", v.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ogni_eccezione_si_puo_spegnere_da_sola()
|
||||||
|
{
|
||||||
|
var solo = new FinishedAuctionCleanup.Options
|
||||||
|
{
|
||||||
|
KeepWithMyBids = false,
|
||||||
|
KeepWon = false,
|
||||||
|
KeepUnclear = false
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.True(Decidi(puntate: 40, opzioni: solo).Remove);
|
||||||
|
Assert.True(Decidi(vinta: true, opzioni: solo).Remove);
|
||||||
|
Assert.True(Decidi(fineVista: false, opzioni: solo).Remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void I_predefiniti_tolgono_le_osservate_e_trattengono_il_resto()
|
||||||
|
{
|
||||||
|
var o = new FinishedAuctionCleanup.Options();
|
||||||
|
|
||||||
|
Assert.True(o.Enabled);
|
||||||
|
Assert.True(o.KeepWithMyBids);
|
||||||
|
Assert.True(o.KeepWon);
|
||||||
|
Assert.True(o.KeepUnclear);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -464,7 +464,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Un_errore_http_non_e_un_riscatto()
|
public void Un_errore_http_non_e_un_riscatto()
|
||||||
{
|
{
|
||||||
var verdict = FreeBidsResponseValidator.Validate(Config, 500, "");
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 500, "");
|
||||||
|
|
||||||
Assert.False(verdict.Accepted);
|
Assert.False(verdict.Accepted);
|
||||||
Assert.Contains("500", verdict.Reason);
|
Assert.Contains("500", verdict.Reason);
|
||||||
@@ -473,7 +473,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Il_json_del_sito_ha_lultima_parola()
|
public void Il_json_del_sito_ha_lultima_parola()
|
||||||
{
|
{
|
||||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||||
"""{"success": true, "bids": 5, "message": "Hai riscattato 5 puntate!"}""");
|
"""{"success": true, "bids": 5, "message": "Hai riscattato 5 puntate!"}""");
|
||||||
|
|
||||||
Assert.True(verdict.Accepted);
|
Assert.True(verdict.Accepted);
|
||||||
@@ -484,7 +484,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Un_rifiuto_con_200_resta_un_rifiuto()
|
public void Un_rifiuto_con_200_resta_un_rifiuto()
|
||||||
{
|
{
|
||||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||||
"""{"success": false, "message": "Premio già riscattato"}""");
|
"""{"success": false, "message": "Premio già riscattato"}""");
|
||||||
|
|
||||||
Assert.False(verdict.Accepted);
|
Assert.False(verdict.Accepted);
|
||||||
@@ -496,7 +496,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
{
|
{
|
||||||
// Chiedere all'utente di indovinare il percorso completo sarebbe un modo per
|
// Chiedere all'utente di indovinare il percorso completo sarebbe un modo per
|
||||||
// fargli sbagliare la configurazione.
|
// fargli sbagliare la configurazione.
|
||||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||||
"""{"status": 200, "data": {"success": "true", "bids": "3"}}""");
|
"""{"status": 200, "data": {"success": "true", "bids": "3"}}""");
|
||||||
|
|
||||||
Assert.True(verdict.Accepted);
|
Assert.True(verdict.Accepted);
|
||||||
@@ -511,7 +511,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
config.ResponseValidation.SuccessValue = "ok";
|
config.ResponseValidation.SuccessValue = "ok";
|
||||||
config.ResponseValidation.BidsCountKey = "bids_added";
|
config.ResponseValidation.BidsCountKey = "bids_added";
|
||||||
|
|
||||||
var verdict = FreeBidsResponseValidator.Validate(config, 200,
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(config, 200,
|
||||||
"""{"esito": "ok", "bids_added": 12}""");
|
"""{"esito": "ok", "bids_added": 12}""");
|
||||||
|
|
||||||
Assert.True(verdict.Accepted);
|
Assert.True(verdict.Accepted);
|
||||||
@@ -523,7 +523,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
{
|
{
|
||||||
// È il caso peggiore: sessione scaduta, HTTP 200, e ogni giro conterebbe premi
|
// È il caso peggiore: sessione scaduta, HTTP 200, e ogni giro conterebbe premi
|
||||||
// che non sono mai stati presi.
|
// che non sono mai stati presi.
|
||||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||||
"<html><body><form action=\"/login.php\"><input name=\"password\"></form></body></html>");
|
"<html><body><form action=\"/login.php\"><input name=\"password\"></form></body></html>");
|
||||||
|
|
||||||
Assert.False(verdict.Accepted);
|
Assert.False(verdict.Accepted);
|
||||||
@@ -533,7 +533,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Una_pagina_intera_vale_come_accettata_e_le_puntate_si_leggono_dal_testo()
|
public void Una_pagina_intera_vale_come_accettata_e_le_puntate_si_leggono_dal_testo()
|
||||||
{
|
{
|
||||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200,
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200,
|
||||||
"<html><body><h1>Complimenti! Hai ricevuto 3 puntate</h1></body></html>");
|
"<html><body><h1>Complimenti! Hai ricevuto 3 puntate</h1></body></html>");
|
||||||
|
|
||||||
Assert.True(verdict.Accepted);
|
Assert.True(verdict.Accepted);
|
||||||
@@ -543,7 +543,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void In_un_messaggio_breve_le_parole_di_rifiuto_contano()
|
public void In_un_messaggio_breve_le_parole_di_rifiuto_contano()
|
||||||
{
|
{
|
||||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200, "Premio già riscattato oggi");
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200, "Premio già riscattato oggi");
|
||||||
|
|
||||||
Assert.False(verdict.Accepted);
|
Assert.False(verdict.Accepted);
|
||||||
Assert.Contains("rifiutato", verdict.Reason);
|
Assert.Contains("rifiutato", verdict.Reason);
|
||||||
@@ -552,7 +552,7 @@ public class FreeBidsResponseValidatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Una_risposta_vuota_si_accetta_e_decide_il_saldo()
|
public void Una_risposta_vuota_si_accetta_e_decide_il_saldo()
|
||||||
{
|
{
|
||||||
var verdict = FreeBidsResponseValidator.Validate(Config, 200, "");
|
var verdict = FreeBidsResponseValidator.GiudicaRisposta(Config, 200, "");
|
||||||
|
|
||||||
Assert.True(verdict.Accepted);
|
Assert.True(verdict.Accepted);
|
||||||
Assert.Null(verdict.BidsDeclared);
|
Assert.Null(verdict.BidsDeclared);
|
||||||
@@ -794,3 +794,60 @@ public class FreeBidsStatsTests
|
|||||||
Assert.Equal(7, FreeBidsStats.Read().TotalBids);
|
Assert.Equal(7, FreeBidsStats.Read().TotalBids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La politica nuova: aprire il collegamento <b>e'</b> il riscatto, e non si giudica cosa
|
||||||
|
/// risponde il sito.
|
||||||
|
///
|
||||||
|
/// <para>Nasce da tre giorni di registri in cui ogni singolo riscatto risultava fallito con
|
||||||
|
/// "sessione scaduta: risposta la pagina di accesso" — 6 trovati, 0 riscossi, 6 non
|
||||||
|
/// riusciti, giro dopo giro — mentre le puntate arrivavano lo stesso. Un premio si riscuote
|
||||||
|
/// una volta sola: fra i due errori possibili, insistere su un premio gia' preso non costa
|
||||||
|
/// niente, mentre dichiarare falliti dei riscatti riusciti riempie il registro di allarmi
|
||||||
|
/// che non servono a nessuno.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class FreeBidsPoliticaApertaTests
|
||||||
|
{
|
||||||
|
private static FreeBidsSiteConfig Config() => new FreeBidsSiteConfig().Normalised();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void La_pagina_di_accesso_non_fa_piu_fallire_il_riscatto()
|
||||||
|
{
|
||||||
|
// Esattamente il caso che nei registri falliva sempre.
|
||||||
|
var login = "<html><body><form action=\"/login.php\">" +
|
||||||
|
new string('x', 500) + "<input name=\"password\"></form></body></html>";
|
||||||
|
|
||||||
|
var esito = FreeBidsResponseValidator.Validate(Config(), 200, login);
|
||||||
|
|
||||||
|
Assert.True(esito.Accepted);
|
||||||
|
Assert.Contains("aperto", esito.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Una_risposta_di_rifiuto_non_fa_piu_fallire_il_riscatto()
|
||||||
|
{
|
||||||
|
var esito = FreeBidsResponseValidator.Validate(Config(), 200, "{\"success\":false}");
|
||||||
|
|
||||||
|
Assert.True(esito.Accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Un_errore_di_trasporto_resta_un_fallimento()
|
||||||
|
{
|
||||||
|
// Qui il collegamento non e' stato aperto affatto: dirlo non e' un giudizio sul
|
||||||
|
// contenuto, e riprovare ha senso.
|
||||||
|
var esito = FreeBidsResponseValidator.Validate(Config(), 500, "");
|
||||||
|
|
||||||
|
Assert.False(esito.Accepted);
|
||||||
|
Assert.Contains("500", esito.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Il_giudizio_vecchio_resta_disponibile()
|
||||||
|
{
|
||||||
|
// Non e' stato cancellato: serve ai test e a chi volesse riaccenderlo.
|
||||||
|
var esito = FreeBidsResponseValidator.GiudicaRisposta(Config(), 200, "{\"success\":false}");
|
||||||
|
|
||||||
|
Assert.False(esito.Accepted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using AutoBidder.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AutoBidder.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Migrazione delle impostazioni salvate da una versione precedente.
|
||||||
|
///
|
||||||
|
/// <para>La proprieta' da difendere: un predefinito nuovo <b>non arriva</b> a chi ha gia' un
|
||||||
|
/// file salvato, perche' il valore su disco vince. Va bene per le scelte dell'utente, non
|
||||||
|
/// per due valori che erano conseguenze di misure sbagliate — il tetto di 40 richieste al
|
||||||
|
/// secondo e l'anticipo di 1000 ms erano stati tarati quando il tetto stesso accodava le
|
||||||
|
/// chiamate e gonfiava il ping. Senza migrazione l'aggiornamento sarebbe arrivato con le
|
||||||
|
/// correzioni gia' annullate dal file dell'utente.</para>
|
||||||
|
///
|
||||||
|
/// <para>L'altra proprieta', altrettanto importante: chi quei valori li aveva gia' cambiati
|
||||||
|
/// di suo se li tiene. Quella e' una scelta, e non spetta a un aggiornamento ribaltarla.</para>
|
||||||
|
/// </summary>
|
||||||
|
public class SettingsMigrationTests
|
||||||
|
{
|
||||||
|
private static AppSettings Vecchie() => new()
|
||||||
|
{
|
||||||
|
SettingsSchemaVersion = 0,
|
||||||
|
MaxRequestsPerSecond = 40,
|
||||||
|
DefaultBidBeforeDeadlineMs = 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void I_valori_vecchi_vengono_portati_avanti()
|
||||||
|
{
|
||||||
|
var s = Vecchie();
|
||||||
|
|
||||||
|
Assert.True(SettingsManager.Migra(s));
|
||||||
|
|
||||||
|
Assert.Equal(0, s.MaxRequestsPerSecond); // nessun tetto
|
||||||
|
Assert.Equal(500, s.DefaultBidBeforeDeadlineMs);
|
||||||
|
Assert.Equal(SettingsManager.SchemaCorrente, s.SettingsSchemaVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Una_scelta_dell_utente_non_viene_ribaltata()
|
||||||
|
{
|
||||||
|
var s = new AppSettings
|
||||||
|
{
|
||||||
|
SettingsSchemaVersion = 0,
|
||||||
|
MaxRequestsPerSecond = 15, // scelto apposta, non il vecchio predefinito
|
||||||
|
DefaultBidBeforeDeadlineMs = 2000 // idem
|
||||||
|
};
|
||||||
|
|
||||||
|
SettingsManager.Migra(s);
|
||||||
|
|
||||||
|
Assert.Equal(15, s.MaxRequestsPerSecond);
|
||||||
|
Assert.Equal(2000, s.DefaultBidBeforeDeadlineMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Gira_una_volta_sola()
|
||||||
|
{
|
||||||
|
var s = Vecchie();
|
||||||
|
|
||||||
|
Assert.True(SettingsManager.Migra(s));
|
||||||
|
Assert.False(SettingsManager.Migra(s));
|
||||||
|
|
||||||
|
// E se poi l'utente rimette 40 di suo, non glielo si toglie al giro dopo.
|
||||||
|
s.MaxRequestsPerSecond = 40;
|
||||||
|
Assert.False(SettingsManager.Migra(s));
|
||||||
|
Assert.Equal(40, s.MaxRequestsPerSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Impostazioni_nuove_di_zecca_sono_gia_allo_schema_corrente()
|
||||||
|
{
|
||||||
|
// Un file che non esiste ancora non e' un file vecchio: nasce gia' aggiornato,
|
||||||
|
// e non deve far scattare nessuna migrazione.
|
||||||
|
var s = new AppSettings { SettingsSchemaVersion = SettingsManager.SchemaCorrente };
|
||||||
|
|
||||||
|
Assert.False(SettingsManager.Migra(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void I_predefiniti_di_fabbrica_sono_quelli_misurati_oggi()
|
||||||
|
{
|
||||||
|
var s = new AppSettings();
|
||||||
|
|
||||||
|
Assert.Equal(0, s.MaxRequestsPerSecond);
|
||||||
|
Assert.Equal(500, s.DefaultBidBeforeDeadlineMs);
|
||||||
|
Assert.True(s.QuietHoursEnabled);
|
||||||
|
Assert.True(s.AutoBidDuelWithdrawEnabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -449,14 +449,24 @@
|
|||||||
<Setter Property="AlternatingRowBackground" Value="{DynamicResource Brush.Surface}"/>
|
<Setter Property="AlternatingRowBackground" Value="{DynamicResource Brush.Surface}"/>
|
||||||
<Setter Property="GridLinesVisibility" Value="None"/>
|
<Setter Property="GridLinesVisibility" Value="None"/>
|
||||||
<Setter Property="HeadersVisibility" Value="Column"/>
|
<Setter Property="HeadersVisibility" Value="Column"/>
|
||||||
<Setter Property="RowHeight" Value="28"/>
|
<!-- Densità: 22 px invece di 28 fanno entrare circa un quarto di righe in più a
|
||||||
<Setter Property="ColumnHeaderHeight" Value="30"/>
|
parità di altezza. Su un cruscotto che serve a tenere d'occhio molte aste
|
||||||
|
insieme, l'altezza è la risorsa scarsa, e sei pixel di aria per riga costano
|
||||||
|
una decina di aste sullo schermo. -->
|
||||||
|
<Setter Property="RowHeight" Value="22"/>
|
||||||
|
<Setter Property="ColumnHeaderHeight" Value="26"/>
|
||||||
<Setter Property="FontFamily" Value="{StaticResource Font.UI}"/>
|
<Setter Property="FontFamily" Value="{StaticResource Font.UI}"/>
|
||||||
<Setter Property="FontSize" Value="{StaticResource Font.Size.Base}"/>
|
<Setter Property="FontSize" Value="{StaticResource Font.Size.Sm}"/>
|
||||||
<Setter Property="CanUserAddRows" Value="False"/>
|
<Setter Property="CanUserAddRows" Value="False"/>
|
||||||
<Setter Property="AutoGenerateColumns" Value="False"/>
|
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||||
<Setter Property="SelectionMode" Value="Single"/>
|
<Setter Property="SelectionMode" Value="Single"/>
|
||||||
<Setter Property="EnableRowVirtualization" Value="True"/>
|
<Setter Property="EnableRowVirtualization" Value="True"/>
|
||||||
|
<!-- Il riciclo riusa i contenitori di riga invece di ricrearli a ogni giro di
|
||||||
|
scorrimento. Con la griglia del monitor, che si aggiorna piu' volte al
|
||||||
|
secondo mentre le aste corrono, e' la differenza fra allocare righe nuove
|
||||||
|
di continuo e non allocarne quasi nessuna. Il valore predefinito di WPF e'
|
||||||
|
"Standard", cioe' senza riciclo. -->
|
||||||
|
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/>
|
||||||
<Setter Property="VerticalGridLinesBrush" Value="Transparent"/>
|
<Setter Property="VerticalGridLinesBrush" Value="Transparent"/>
|
||||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Brush.Border}"/>
|
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Brush.Border}"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ namespace AutoBidder.Utilities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Informazioni sull'applicazione, in un solo posto.
|
/// Informazioni sull'applicazione, in un solo posto.
|
||||||
///
|
///
|
||||||
/// <para>La versione si legge dall'assembly (dalla proprietà <c>Version</c> del
|
/// <para>La versione si legge dall'assembly, così il titolo della finestra e il log
|
||||||
/// <c>.csproj</c>): così il titolo della finestra e il log d'avvio restano allineati
|
/// d'avvio restano allineati da soli invece di rincorrere il numero in più stringhe
|
||||||
/// da soli, e per pubblicare una nuova versione basta cambiare un unico numero nel
|
/// sparse.</para>
|
||||||
/// progetto invece di rincorrerlo in più stringhe sparse.</para>
|
///
|
||||||
|
/// <para>Nei pacchetti pubblicati quel numero viene dal tag git, scritto
|
||||||
|
/// nell'assembly da <c>build/Release.proj</c> al momento della pubblicazione. Il
|
||||||
|
/// <c><Version></c> del <c>.csproj</c> vale solo per le compilazioni di
|
||||||
|
/// sviluppo: alzarlo non pubblica niente, e non serve prima di un rilascio.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AppInfo
|
public static class AppInfo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,5 +63,33 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static int? TrustedWinnerBids(CompletedAuctionRecord record) =>
|
public static int? TrustedWinnerBids(CompletedAuctionRecord record) =>
|
||||||
CheckWinnerBids(record).IsTrustworthy ? record.WinnerBidsUsed : null;
|
CheckWinnerBids(record).IsTrustworthy ? record.WinnerBidsUsed : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quanta parte dell'asta è stata davvero osservata, da 0 a 1. <c>null</c> se non
|
||||||
|
/// si può dire (prezzo finale mancante).
|
||||||
|
///
|
||||||
|
/// <para>Sfrutta la stessa identità del controllo qui sopra: il prezzo finale in
|
||||||
|
/// centesimi è il numero totale di puntate dell'asta. Confrontarlo con i reset
|
||||||
|
/// visti dice, senza bisogno di registrare nient'altro, se l'asta è stata seguita
|
||||||
|
/// dall'inizio o presa per strada. Un'asta agganciata a metà ha numeri veri ma
|
||||||
|
/// parziali — è il caso peggiore, perché sembra buona.</para>
|
||||||
|
///
|
||||||
|
/// <para>Il valore è tagliato a 1: sui dati reali (5876 aste) nessun record supera
|
||||||
|
/// 1,02, e il poco che sfora è arrotondamento del prezzo, non un'asta osservata
|
||||||
|
/// più del possibile.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static double? ObservedCoverage(double finalPrice, int totalResets)
|
||||||
|
{
|
||||||
|
if (finalPrice <= 0) return null;
|
||||||
|
|
||||||
|
var totalBids = Math.Round(finalPrice * 100);
|
||||||
|
if (totalBids <= 0) return null;
|
||||||
|
|
||||||
|
return Math.Min(1.0, Math.Max(0, totalResets) / totalBids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Comodità: la copertura di un'asta già registrata.</summary>
|
||||||
|
public static double? ObservedCoverage(CompletedAuctionRecord record) =>
|
||||||
|
record == null ? null : ObservedCoverage(record.FinalPrice, record.TotalResets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AutoBidder.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Riconosce l'avversario che ha l'autopuntata di Bidoo armata, e dice quando smettere.
|
||||||
|
///
|
||||||
|
/// <para><b>Il meccanismo.</b> L'autopuntata del sito scatta a 2 secondi dalla chiusura,
|
||||||
|
/// e si riarma ogni volta che il suo padrone non è più in testa. Il timer si azzera
|
||||||
|
/// intorno agli 8 secondi, quindi la risposta arriva circa 6 secondi dopo la puntata che
|
||||||
|
/// l'ha svegliata. Misurato su 2.799.681 intervalli fra puntate consecutive in 5037 aste:
|
||||||
|
/// mediana <b>6,0 s</b>, primo quartile 5,3 s, terzo quartile 7,1 s.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Perché serve ritirarsi.</b> Contro un'autopuntata armata non esiste attrito
|
||||||
|
/// che si possa vincere: risponde sempre, risponde in sei secondi, non dorme e non si
|
||||||
|
/// distrae. Puntare di nuovo significa comprare un altro giro di sei secondi al prezzo
|
||||||
|
/// di una puntata a testa, all'infinito, finché uno dei due esaurisce il credito. Nelle
|
||||||
|
/// prove reali sono state 191 puntate su tre aste per zero vittorie, con un ciclo
|
||||||
|
/// perfettamente regolare da 13,0 secondi: 7 di attesa nostra più 6 della loro risposta.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Perché non basta puntare tardi.</b> Puntare a un secondo dalla chiusura non
|
||||||
|
/// evita l'autopuntata avversaria: la sveglia. Lei scatta quando il suo padrone perde la
|
||||||
|
/// testa della classifica, non a un orario fisso, quindi ogni nostra puntata tardiva è
|
||||||
|
/// esattamente ciò che la fa ripartire.</para>
|
||||||
|
///
|
||||||
|
/// <para>Classe pura, senza stato e senza orologio: prende i numeri e restituisce un
|
||||||
|
/// giudizio. Così la stessa identica logica serve il motore dal vivo e la rigiocata sui
|
||||||
|
/// dossier, senza due versioni che divergono.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class AutoBidDuel
|
||||||
|
{
|
||||||
|
/// <summary>Ritardo tipico della risposta automatica, in secondi.</summary>
|
||||||
|
public const double RispostaTipicaSecondi = 6.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quanto ci si allontana dai 6 secondi continuando a chiamarla automatica.
|
||||||
|
/// Coincide grosso modo con l'intervallo fra primo e terzo quartile misurati
|
||||||
|
/// (5,3–7,1 s), allargato per la latenza di rete e per l'imprecisione del polling.
|
||||||
|
/// </summary>
|
||||||
|
public const double TolleranzaSecondi = 2.5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Risposte automatiche di fila oltre le quali si smette. Cinque perché è il punto in
|
||||||
|
/// cui la coincidenza smette di essere credibile: cinque intervalli indipendenti
|
||||||
|
/// dentro la finestra dei 6±2,5 s non capitano per caso, e cinque puntate sono un
|
||||||
|
/// prezzo che si può pagare per avere la certezza.
|
||||||
|
/// </summary>
|
||||||
|
public const int RispostePerConcludere = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Questo intervallo ha la firma della risposta automatica?
|
||||||
|
/// </summary>
|
||||||
|
public static bool ERispostaAutomatica(double secondiDopoLaMiaPuntata) =>
|
||||||
|
secondiDopoLaMiaPuntata > 0 &&
|
||||||
|
Math.Abs(secondiDopoLaMiaPuntata - RispostaTipicaSecondi) <= TolleranzaSecondi;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Il conteggio aggiornato dopo aver visto una risposta avversaria.
|
||||||
|
///
|
||||||
|
/// <para>Una risposta che <b>non</b> ha la firma azzera il conteggio invece di
|
||||||
|
/// scalarlo: serve una sequenza pulita per concludere che c'è una macchina
|
||||||
|
/// dall'altra parte, e un solo intervallo umano in mezzo basta a rimettere tutto in
|
||||||
|
/// discussione. È la scelta prudente, perché l'errore costoso è ritirarsi da
|
||||||
|
/// un'asta che si poteva vincere.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static int Aggiorna(int consecutiveFinora, double secondiDopoLaMiaPuntata) =>
|
||||||
|
ERispostaAutomatica(secondiDopoLaMiaPuntata) ? consecutiveFinora + 1 : 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Si è capito che dall'altra parte c'è un'autopuntata armata?
|
||||||
|
/// </summary>
|
||||||
|
public static bool DuelloRiconosciuto(int consecutive, int soglia = RispostePerConcludere) =>
|
||||||
|
consecutive >= Math.Max(1, soglia);
|
||||||
|
|
||||||
|
/// <summary>Come spiegarlo nel registro, in una riga.</summary>
|
||||||
|
public static string Spiegazione(int consecutive) =>
|
||||||
|
$"autopuntata avversaria riconosciuta: {consecutive} risposte di fila a ~{RispostaTipicaSecondi:N0}s. " +
|
||||||
|
"Continuare significa scambiare puntate una a una con una macchina che non si stanca.";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AutoBidder.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fascia oraria in cui non si punta.
|
||||||
|
///
|
||||||
|
/// <para><b>Perché.</b> La stessa asta non costa uguale a tutte le ore. Su 6060 aste
|
||||||
|
/// concluse con prezzo e valore noti, il prezzo di chiusura mediano in percentuale del
|
||||||
|
/// valore del prodotto è 4,6% alle 12 e 5,3% fra le 10 e le 13, ma sale a <b>8,6% a
|
||||||
|
/// mezzanotte</b> e <b>9,7% alle 9</b>. Le puntate che servono per vincere seguono la
|
||||||
|
/// stessa curva: 21-24 nelle ore buone, 26-29 in quelle cattive. Quasi il doppio del
|
||||||
|
/// costo per lo stesso oggetto.</para>
|
||||||
|
///
|
||||||
|
/// <para>Il motivo è che di notte Bidoo sospende le puntate — restano poche aste in
|
||||||
|
/// corso e la concorrenza si concentra su quelle. Le ore di confine, mezzanotte e le
|
||||||
|
/// nove, sono le peggiori: c'è ancora qualcosa da giocare e ci sono tutti addosso.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Cosa fa.</b> Sospende le puntate, non il monitoraggio: l'asta resta in stato
|
||||||
|
/// Attiva, continua a essere seguita, e riprende a puntare da sola appena la fascia
|
||||||
|
/// finisce. È fatto apposta per le aste lunghe che attraversano la fascia — fermarle
|
||||||
|
/// davvero significherebbe perderle.</para>
|
||||||
|
///
|
||||||
|
/// <para>Classe pura, l'ora arriva da fuori: così i test non dipendono da quando
|
||||||
|
/// vengono eseguiti.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class BiddingHours
|
||||||
|
{
|
||||||
|
/// <summary>Prima ora della fascia sospesa, inclusa. Predefinito: mezzanotte.</summary>
|
||||||
|
public const int DefaultStart = 0;
|
||||||
|
|
||||||
|
/// <summary>Prima ora in cui si torna a puntare. Predefinito: le 10.</summary>
|
||||||
|
public const int DefaultEnd = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L'ora indicata cade nella fascia sospesa?
|
||||||
|
///
|
||||||
|
/// <para>Estremi: inizio incluso, fine esclusa — 0-10 significa "da mezzanotte fino
|
||||||
|
/// alle 9:59". Una fascia che scavalca la mezzanotte (22-6) funziona. Inizio uguale
|
||||||
|
/// a fine vuol dire fascia vuota, cioè filtro spento: è l'unica lettura sensata,
|
||||||
|
/// perché l'alternativa sarebbe non puntare mai.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsQuiet(int hour, int start, int end)
|
||||||
|
{
|
||||||
|
hour = Normalize(hour);
|
||||||
|
start = Normalize(start);
|
||||||
|
end = Normalize(end);
|
||||||
|
|
||||||
|
if (start == end) return false;
|
||||||
|
|
||||||
|
return start < end
|
||||||
|
? hour >= start && hour < end
|
||||||
|
: hour >= start || hour < end; // a cavallo di mezzanotte
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Comodità: la fascia applicata a un istante.</summary>
|
||||||
|
public static bool IsQuiet(DateTime when, int start, int end) =>
|
||||||
|
IsQuiet(when.Hour, start, end);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quando riprende, per poterlo scrivere nel registro. Restituisce l'ora di ripresa
|
||||||
|
/// (0-23); chi legge sa già che è la prossima volta che scocca.
|
||||||
|
/// </summary>
|
||||||
|
public static int ResumesAt(int end) => Normalize(end);
|
||||||
|
|
||||||
|
/// <summary>Riga per il registro: dice cosa succede e quando finisce.</summary>
|
||||||
|
public static string Spiegazione(int start, int end) =>
|
||||||
|
$"puntata saltata: fascia oraria sospesa ({Normalize(start):00}:00-{Normalize(end):00}:00). " +
|
||||||
|
"L'asta resta attiva e riprende a puntare da sola alla fine della fascia.";
|
||||||
|
|
||||||
|
private static int Normalize(int hour)
|
||||||
|
{
|
||||||
|
var h = hour % 24;
|
||||||
|
return h < 0 ? h + 24 : h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,25 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||||
|
|
||||||
|
// ── Copia in memoria ─────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Il file cresce di un record per asta e arriva in fretta a qualche megabyte:
|
||||||
|
// a 5876 aste, leggerlo e deserializzarlo costa ~72 ms misurati. Senza copia
|
||||||
|
// in memoria quel prezzo si paga a ogni apertura delle statistiche e a ogni
|
||||||
|
// asta che finisce — e quella chiamata arriva da un gestore di eventi, cioe'
|
||||||
|
// dal thread dell'interfaccia, che resta fermo per tutto il tempo.
|
||||||
|
//
|
||||||
|
// L'invalidazione guarda la data di modifica del file, non un tempo di
|
||||||
|
// scadenza: cosi' la copia resta giusta anche se il file viene sostituito da
|
||||||
|
// fuori — ripristino di un backup, modifica a mano — che qui capita davvero.
|
||||||
|
//
|
||||||
|
// Il prezzo da sapere: i record restituiti sono gli STESSI oggetti che stanno
|
||||||
|
// nella copia. Chi ne modifica uno sta modificando anche la copia, prima e
|
||||||
|
// indipendentemente dal salvataggio. Repair e il recupero puntate contano su
|
||||||
|
// questo; chi non lo vuole si faccia una copia del record.
|
||||||
|
private static List<CompletedAuctionRecord>? _cache;
|
||||||
|
private static DateTime _cacheStamp;
|
||||||
|
|
||||||
/// <summary>Percorso del file storico (per mostrarlo/aprirlo nella UI).</summary>
|
/// <summary>Percorso del file storico (per mostrarlo/aprirlo nella UI).</summary>
|
||||||
public static string StorePath => FilePath;
|
public static string StorePath => FilePath;
|
||||||
|
|
||||||
@@ -34,16 +53,50 @@ namespace AutoBidder.Utilities
|
|||||||
public static void Append(CompletedAuctionRecord record)
|
public static void Append(CompletedAuctionRecord record)
|
||||||
{
|
{
|
||||||
if (record == null) return;
|
if (record == null) return;
|
||||||
|
AppendMany(new[] { record });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiunge o sostituisce piu' aste con <b>un solo</b> salvataggio.
|
||||||
|
///
|
||||||
|
/// <para>Serve a chi ne tocca tante di fila, come il recupero delle puntate dei
|
||||||
|
/// vincitori. Chiamare <see cref="Append"/> in un ciclo rilegge e riscrive tutto
|
||||||
|
/// il file a ogni giro: sulle 4675 aste di un recupero completo erano minuti di
|
||||||
|
/// solo rimescolamento JSON e svariati gigabyte scritti sul disco per nulla.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static void AppendMany(IReadOnlyCollection<CompletedAuctionRecord> records)
|
||||||
|
{
|
||||||
|
if (records == null || records.Count == 0) return;
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var list = LoadInternal();
|
var list = LoadInternal();
|
||||||
|
|
||||||
var existing = list.FindIndex(r =>
|
// Indice per id: cercare ogni record con un FindIndex renderebbe
|
||||||
!string.IsNullOrEmpty(r.AuctionId) && r.AuctionId == record.AuctionId);
|
// quadratico proprio il caso per cui questo metodo esiste.
|
||||||
|
var byId = new Dictionary<string, int>(list.Count, StringComparer.Ordinal);
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
var id = list[i].AuctionId;
|
||||||
|
if (!string.IsNullOrEmpty(id)) byId[id] = i;
|
||||||
|
}
|
||||||
|
|
||||||
if (existing >= 0) list[existing] = record;
|
foreach (var record in records)
|
||||||
else list.Add(record);
|
{
|
||||||
|
if (record == null) continue;
|
||||||
|
|
||||||
|
var id = record.AuctionId;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(id) && byId.TryGetValue(id, out var at))
|
||||||
|
{
|
||||||
|
list[at] = record;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(id)) byId[id] = list.Count;
|
||||||
|
list.Add(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SaveInternal(list);
|
SaveInternal(list);
|
||||||
}
|
}
|
||||||
@@ -119,10 +172,12 @@ namespace AutoBidder.Utilities
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(FilePath)) File.Delete(FilePath);
|
if (File.Exists(FilePath)) File.Delete(FilePath);
|
||||||
|
_cache = null;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[STATS] Errore pulizia storico: {ex.Message}");
|
Console.WriteLine($"[STATS] Errore pulizia storico: {ex.Message}");
|
||||||
|
_cache = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,18 +276,33 @@ namespace AutoBidder.Utilities
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(FilePath))
|
if (!File.Exists(FilePath))
|
||||||
|
{
|
||||||
|
_cache = null;
|
||||||
return new List<CompletedAuctionRecord>();
|
return new List<CompletedAuctionRecord>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Una lettura di data di modifica costa microsecondi; la lettura piu'
|
||||||
|
// la deserializzazione che evita ne costa decine di millisecondi.
|
||||||
|
var stamp = File.GetLastWriteTimeUtc(FilePath);
|
||||||
|
if (_cache != null && stamp == _cacheStamp) return _cache;
|
||||||
|
|
||||||
var json = File.ReadAllText(FilePath);
|
var json = File.ReadAllText(FilePath);
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
|
||||||
return new List<CompletedAuctionRecord>();
|
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<List<CompletedAuctionRecord>>(json)
|
var list = string.IsNullOrWhiteSpace(json)
|
||||||
?? new List<CompletedAuctionRecord>();
|
? new List<CompletedAuctionRecord>()
|
||||||
|
: JsonSerializer.Deserialize<List<CompletedAuctionRecord>>(json)
|
||||||
|
?? new List<CompletedAuctionRecord>();
|
||||||
|
|
||||||
|
_cache = list;
|
||||||
|
_cacheStamp = stamp;
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[STATS] Errore lettura storico: {ex.Message}");
|
Console.WriteLine($"[STATS] Errore lettura storico: {ex.Message}");
|
||||||
|
|
||||||
|
// Non si sa cosa ci sia sul disco: la prossima volta si rilegge.
|
||||||
|
_cache = null;
|
||||||
return new List<CompletedAuctionRecord>();
|
return new List<CompletedAuctionRecord>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,10 +316,16 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
var json = JsonSerializer.Serialize(list, JsonOptions);
|
var json = JsonSerializer.Serialize(list, JsonOptions);
|
||||||
File.WriteAllText(FilePath, json);
|
File.WriteAllText(FilePath, json);
|
||||||
|
|
||||||
|
// La copia in memoria e' esattamente quello che si e' appena scritto:
|
||||||
|
// tenerla evita di rileggere il file al primo uso successivo.
|
||||||
|
_cache = list;
|
||||||
|
_cacheStamp = File.GetLastWriteTimeUtc(FilePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[STATS] Errore salvataggio storico: {ex.Message}");
|
Console.WriteLine($"[STATS] Errore salvataggio storico: {ex.Message}");
|
||||||
|
_cache = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using AutoBidder.Models;
|
||||||
|
|
||||||
|
namespace AutoBidder.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Decide se un'asta conclusa può sparire da sola dall'elenco del monitor.
|
||||||
|
///
|
||||||
|
/// <para><b>Perché serve.</b> Le aste concluse restano nel monitor finché non le si
|
||||||
|
/// toglie a mano, e con l'aggiunta automatica accesa se ne accumulano centinaia: quelle
|
||||||
|
/// vive finiscono sepolte sotto quelle finite, che è il contrario di quello che serve
|
||||||
|
/// guardando un cruscotto.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Perché non basta togliere tutto.</b> Un'asta su cui si è puntato è l'unica
|
||||||
|
/// che vale la pena riguardare: quanto è costata, se il ritiro dal duello è scattato al
|
||||||
|
/// momento giusto, se il prezzo finale sta dove ci si aspettava. Farla sparire da sola
|
||||||
|
/// significa perdere l'occasione di controllarla, e i soldi erano veri. Le altre — quelle
|
||||||
|
/// solo osservate — non hanno niente da dire che lo storico non dica meglio.</para>
|
||||||
|
///
|
||||||
|
/// <para>Da qui la forma della regola: si toglie di serie, si trattiene ciò su cui c'è
|
||||||
|
/// stato un esborso. Classe pura: nessuno stato, nessun orologio, nessun disco.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class FinishedAuctionCleanup
|
||||||
|
{
|
||||||
|
/// <summary>Cosa fare delle aste concluse, e cosa risparmiare.</summary>
|
||||||
|
public sealed class Options
|
||||||
|
{
|
||||||
|
/// <summary>Togliere da sole le aste concluse. Acceso di serie.</summary>
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trattenere le aste su cui si è puntato, per poterle controllare a mano.
|
||||||
|
/// Acceso di serie: sono quelle costate qualcosa.
|
||||||
|
/// </summary>
|
||||||
|
public bool KeepWithMyBids { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trattenere le aste vinte. Acceso di serie: c'è da confermare l'acquisto su
|
||||||
|
/// Bidoo, e una vittoria sparita dall'elenco è il modo più semplice di
|
||||||
|
/// dimenticarsene.
|
||||||
|
/// </summary>
|
||||||
|
public bool KeepWon { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trattenere le aste finite con un errore o senza esito leggibile: sono
|
||||||
|
/// esattamente quelle da guardare. Acceso di serie.
|
||||||
|
/// </summary>
|
||||||
|
public bool KeepUnclear { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Esito della decisione, col motivo per il registro.</summary>
|
||||||
|
public readonly record struct Verdict(bool Remove, string Reason);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quest'asta conclusa si può togliere dall'elenco?
|
||||||
|
///
|
||||||
|
/// <para>L'ordine dei controlli è quello che conta: prima le ragioni per
|
||||||
|
/// <b>trattenere</b>, poi la rimozione. Un'asta vinta su cui si è anche puntato deve
|
||||||
|
/// restare per il primo motivo che si incontra, non per l'ultimo, così il messaggio
|
||||||
|
/// nel registro dice la cosa più importante.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static Verdict Decide(bool conclusa, bool vinta, int puntateMie, bool fineVista, Options options)
|
||||||
|
{
|
||||||
|
options ??= new Options();
|
||||||
|
|
||||||
|
if (!options.Enabled)
|
||||||
|
return new Verdict(false, "rimozione automatica spenta");
|
||||||
|
|
||||||
|
if (!conclusa)
|
||||||
|
return new Verdict(false, "asta non conclusa");
|
||||||
|
|
||||||
|
if (options.KeepWon && vinta)
|
||||||
|
return new Verdict(false, "vinta: resta in elenco per la conferma su Bidoo");
|
||||||
|
|
||||||
|
if (options.KeepWithMyBids && puntateMie > 0)
|
||||||
|
return new Verdict(false,
|
||||||
|
$"trattenuta: ci hai speso {puntateMie} puntate, controllala a mano");
|
||||||
|
|
||||||
|
if (options.KeepUnclear && !fineVista)
|
||||||
|
return new Verdict(false, "trattenuta: la fine non è stata vista, esito da controllare");
|
||||||
|
|
||||||
|
return new Verdict(true, "conclusa senza puntate mie: tolta dall'elenco");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Comodità per il motore: legge dall'asta i quattro numeri che servono.
|
||||||
|
///
|
||||||
|
/// <para>Le puntate si prendono dal massimo fra il contatore dell'asta e quello di
|
||||||
|
/// sessione: uno dei due può essere a zero a seconda di come l'asta è entrata nel
|
||||||
|
/// monitor, e trattenere per eccesso è l'errore giusto da fare.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static Verdict Decide(AuctionInfo auction, bool vinta, Options options)
|
||||||
|
{
|
||||||
|
if (auction == null) return new Verdict(false, "nessuna asta");
|
||||||
|
|
||||||
|
var perAsta = auction.BidsUsedOnThisAuction ?? 0;
|
||||||
|
var puntate = perAsta > auction.SessionBidCount ? perAsta : auction.SessionBidCount;
|
||||||
|
|
||||||
|
return Decide(
|
||||||
|
conclusa: !string.IsNullOrWhiteSpace(auction.ConcludedOutcome),
|
||||||
|
vinta: vinta,
|
||||||
|
puntateMie: puntate,
|
||||||
|
fineVista: auction.ObservedToEnd,
|
||||||
|
options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,21 @@ namespace AutoBidder.Utilities
|
|||||||
/// dossier non possono dire di piu', perche' Bidoo dichiara la scadenza al secondo
|
/// dossier non possono dire di piu', perche' Bidoo dichiara la scadenza al secondo
|
||||||
/// intero.</para>
|
/// intero.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int DefaultBidBeforeDeadlineMs { get; set; } = 1000;
|
///
|
||||||
|
/// <para><b>Aggiornato a 500 ms.</b> Le due ragioni per stare larghi sono cadute
|
||||||
|
/// entrambe. La prima — il p99,9 del ping a 444 ms — era misurata quando il tetto di
|
||||||
|
/// 40 richieste al secondo faceva accodare le chiamate: con il tetto tolto il ping
|
||||||
|
/// misurato sui dossier nuovi sta fra 47 e 78 ms, mediana 55. La seconda era la
|
||||||
|
/// paura della concorrenza fitta sotto i due secondi, ma i tipi di puntata dicono
|
||||||
|
/// il contrario: il 75,3% delle puntate avversarie è automatica e scatta a 2 s
|
||||||
|
/// esatti, quindi sotto quella soglia resta solo chi punta a mano.</para>
|
||||||
|
///
|
||||||
|
/// <para>A 500 ms la puntata parte con circa nove volte il ping di margine, e cade
|
||||||
|
/// nella metà di secondo in cui l'autopuntata del sito ha già sparato. Più in là non
|
||||||
|
/// conviene andare: Bidoo dichiara la scadenza al secondo intero, quindi sotto i
|
||||||
|
/// 500 ms si starebbe scommettendo sull'arrotondamento.</para>
|
||||||
|
/// </summary>
|
||||||
|
public int DefaultBidBeforeDeadlineMs { get; set; } = 500;
|
||||||
public double DefaultMinPrice { get; set; } = 0;
|
public double DefaultMinPrice { get; set; } = 0;
|
||||||
public double DefaultMaxPrice { get; set; } = 0;
|
public double DefaultMaxPrice { get; set; } = 0;
|
||||||
public int DefaultMaxClicks { get; set; } = 0;
|
public int DefaultMaxClicks { get; set; } = 0;
|
||||||
@@ -82,7 +96,18 @@ namespace AutoBidder.Utilities
|
|||||||
/// Tetto complessivo di richieste al secondo verso Bidoo (le puntate non sono soggette
|
/// Tetto complessivo di richieste al secondo verso Bidoo (le puntate non sono soggette
|
||||||
/// al limite: hanno corsia preferenziale). Default: 40.
|
/// al limite: hanno corsia preferenziale). Default: 40.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double MaxRequestsPerSecond { get; set; } = 40;
|
/// <summary>
|
||||||
|
/// Tetto di richieste al secondo verso Bidoo. <b>0 = nessun tetto</b>, ed è il
|
||||||
|
/// predefinito: il ritmo lo detta la rete, e a frenare è il server con 429/503.
|
||||||
|
///
|
||||||
|
/// <para>Il vecchio predefinito era 40, ed era il vero collo di bottiglia. Misurato
|
||||||
|
/// sui registri di tre giorni: con fino a 31 aste seguite insieme la distribuzione
|
||||||
|
/// delle richieste era tagliata netta sul tetto (p90 39, p99 41), e ogni asta
|
||||||
|
/// veniva interrogata ogni ~596 ms invece dei 220 configurati per la finestra
|
||||||
|
/// critica. Con il timer a un secondo restava un solo sguardo prima della
|
||||||
|
/// scadenza: da lì le puntate perse e il tempo che non scorreva.</para>
|
||||||
|
/// </summary>
|
||||||
|
public double MaxRequestsPerSecond { get; set; } = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Porta la granularità del timer di sistema a 1 ms mentre il monitor è in funzione.
|
/// Porta la granularità del timer di sistema a 1 ms mentre il monitor è in funzione.
|
||||||
@@ -536,6 +561,62 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HardcodedAntiCollisionEnabled { get; set; } = false;
|
public bool HardcodedAntiCollisionEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
// ── Fascia oraria sospesa ────────────────────────────────────────
|
||||||
|
// Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il
|
||||||
|
// doppio che fra le 10 e le 13.
|
||||||
|
|
||||||
|
// ── Rimozione automatica delle aste concluse ─────────────────────
|
||||||
|
// Vedi FinishedAuctionCleanup: si toglie di serie, si trattiene cio' su cui c'e'
|
||||||
|
// stato un esborso.
|
||||||
|
|
||||||
|
/// <summary>Togliere da sole dall'elenco le aste concluse. Acceso di serie.</summary>
|
||||||
|
public bool AutoRemoveFinished { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Trattenere le aste su cui hai puntato, per controllarle a mano.</summary>
|
||||||
|
public bool AutoRemoveKeepWithMyBids { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Trattenere le aste vinte: c'è da confermare l'acquisto su Bidoo.</summary>
|
||||||
|
public bool AutoRemoveKeepWon { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Trattenere le aste di cui non si è vista la fine: esito da controllare.</summary>
|
||||||
|
public bool AutoRemoveKeepUnclear { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione dello schema delle impostazioni, per le migrazioni una tantum.
|
||||||
|
///
|
||||||
|
/// <para>Un file scritto da una versione precedente non ha questo campo e vale
|
||||||
|
/// quindi 0. Serve perché un predefinito nuovo <b>non arriva</b> a chi ha già un
|
||||||
|
/// file salvato: il valore su disco vince sempre, ed è giusto che sia così, tranne
|
||||||
|
/// quando il vecchio valore veniva da una misura che si è poi rivelata sbagliata.
|
||||||
|
/// Vedi <see cref="Migra"/>.</para>
|
||||||
|
/// </summary>
|
||||||
|
public int SettingsSchemaVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sospendere le puntate nella fascia indicata. Acceso di serie.
|
||||||
|
/// L'asta resta comunque Attiva e riprende da sola: vedi <see cref="BiddingHours"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool QuietHoursEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Prima ora della fascia sospesa, inclusa.</summary>
|
||||||
|
public int QuietHoursStart { get; set; } = BiddingHours.DefaultStart;
|
||||||
|
|
||||||
|
/// <summary>Prima ora in cui si torna a puntare.</summary>
|
||||||
|
public int QuietHoursEnd { get; set; } = BiddingHours.DefaultEnd;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ritirarsi quando dall'altra parte c'è un'autopuntata armata. Acceso di serie.
|
||||||
|
/// Vedi <see cref="AutoBidDuel"/> per il meccanismo e i numeri misurati.
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoBidDuelWithdrawEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Risposte automatiche di fila oltre le quali ci si ritira. Predefinito 5.
|
||||||
|
/// Più basso risparmia prima ma rischia di scambiare per macchina un avversario
|
||||||
|
/// umano regolare; più alto è prudente e costa puntate.
|
||||||
|
/// </summary>
|
||||||
|
public int AutoBidDuelResponses { get; set; } = AutoBidDuel.RispostePerConcludere;
|
||||||
|
|
||||||
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||||
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
||||||
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||||
@@ -779,6 +860,36 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
private const int CACHE_TTL_MS = 2000;
|
private const int CACHE_TTL_MS = 2000;
|
||||||
|
|
||||||
|
/// <summary>Schema corrente. Alzarlo fa girare <see cref="Migra"/> una volta sola.</summary>
|
||||||
|
public const int SchemaCorrente = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Porta avanti un file scritto da una versione precedente. Restituisce true se
|
||||||
|
/// qualcosa è cambiato, così chi chiama sa che va risalvato.
|
||||||
|
///
|
||||||
|
/// <para><b>Perché serve.</b> Il valore su disco vince sui predefiniti, ed è
|
||||||
|
/// giusto: sono scelte dell'utente. Ma due di quei valori non erano scelte, erano
|
||||||
|
/// conseguenze di misure sbagliate — il tetto di 40 richieste al secondo e
|
||||||
|
/// l'anticipo di 1000 ms erano stati tarati quando il tetto stesso accodava le
|
||||||
|
/// chiamate e gonfiava il ping a 444 ms. Senza migrazione, chi aggiorna si ritrova
|
||||||
|
/// le correzioni di oggi già annullate dal proprio file, e conclude che non
|
||||||
|
/// funzionano.</para>
|
||||||
|
///
|
||||||
|
/// <para>Si tocca solo chi ha ancora esattamente il vecchio valore. Chi lo aveva
|
||||||
|
/// già cambiato di suo se lo tiene: quella è una scelta, e non spetta a un
|
||||||
|
/// aggiornamento ribaltarla.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static bool Migra(AppSettings s)
|
||||||
|
{
|
||||||
|
if (s == null || s.SettingsSchemaVersion >= SchemaCorrente) return false;
|
||||||
|
|
||||||
|
if (Math.Abs(s.MaxRequestsPerSecond - 40) < 0.001) s.MaxRequestsPerSecond = 0;
|
||||||
|
if (s.DefaultBidBeforeDeadlineMs == 1000) s.DefaultBidBeforeDeadlineMs = 500;
|
||||||
|
|
||||||
|
s.SettingsSchemaVersion = SchemaCorrente;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static AppSettings Load()
|
public static AppSettings Load()
|
||||||
{
|
{
|
||||||
// Percorso comune: nessun lucchetto, nessuna allocazione.
|
// Percorso comune: nessun lucchetto, nessuna allocazione.
|
||||||
@@ -794,11 +905,20 @@ namespace AutoBidder.Utilities
|
|||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
AppSettings loaded;
|
AppSettings loaded;
|
||||||
|
var daRisalvare = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
loaded = File.Exists(_file)
|
var esiste = File.Exists(_file);
|
||||||
|
|
||||||
|
loaded = esiste
|
||||||
? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(_file)) ?? new AppSettings()
|
? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(_file)) ?? new AppSettings()
|
||||||
: new AppSettings();
|
: new AppSettings();
|
||||||
|
|
||||||
|
// Un file nuovo nasce già allo schema corrente: la migrazione riguarda
|
||||||
|
// solo chi arriva da una versione precedente.
|
||||||
|
if (!esiste) loaded.SettingsSchemaVersion = SchemaCorrente;
|
||||||
|
else daRisalvare = Migra(loaded);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -806,6 +926,15 @@ namespace AutoBidder.Utilities
|
|||||||
loaded = _cached ?? new AppSettings();
|
loaded = _cached ?? new AppSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scritto qui e non dentro Save: Load è sul percorso caldo e non deve
|
||||||
|
// toccare il disco, ma una migrazione capita una volta sola nella vita
|
||||||
|
// del file, e lasciarla non salvata la farebbe ripetere a ogni avvio.
|
||||||
|
if (daRisalvare)
|
||||||
|
{
|
||||||
|
try { File.WriteAllText(_file, JsonSerializer.Serialize(loaded, new JsonSerializerOptions { WriteIndented = true })); }
|
||||||
|
catch { /* si riproverà al prossimo avvio */ }
|
||||||
|
}
|
||||||
|
|
||||||
_cached = loaded;
|
_cached = loaded;
|
||||||
Interlocked.Exchange(ref _cacheExpiryTicks, DateTime.UtcNow.AddMilliseconds(CACHE_TTL_MS).Ticks);
|
Interlocked.Exchange(ref _cacheExpiryTicks, DateTime.UtcNow.AddMilliseconds(CACHE_TTL_MS).Ticks);
|
||||||
return loaded;
|
return loaded;
|
||||||
|
|||||||
@@ -100,6 +100,17 @@ namespace AutoBidder.Utilities
|
|||||||
/// <summary>Doppioni con lo stesso id: si tiene il più recente.</summary>
|
/// <summary>Doppioni con lo stesso id: si tiene il più recente.</summary>
|
||||||
public bool RemoveDuplicates { get; set; } = true;
|
public bool RemoveDuplicates { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copertura minima richiesta, da 0 a 1: sotto questa soglia l'asta è stata
|
||||||
|
/// agganciata a metà e va tolta. Null = non filtrare sulla copertura.
|
||||||
|
///
|
||||||
|
/// <para>È il criterio per «non seguita dall'inizio alla fine». Non serve
|
||||||
|
/// nessun campo nuovo nei record: la copertura si ricava dal prezzo finale,
|
||||||
|
/// vedi <see cref="AuctionIntegrity.ObservedCoverage(double,int)"/>. Vale
|
||||||
|
/// quindi anche per lo storico raccolto prima che questa regola esistesse.</para>
|
||||||
|
/// </summary>
|
||||||
|
public double? MinObservedCoverage { get; set; }
|
||||||
|
|
||||||
/// <summary>Aste chiuse prima di questa data. Null = nessun limite.</summary>
|
/// <summary>Aste chiuse prima di questa data. Null = nessun limite.</summary>
|
||||||
public DateTime? OlderThan { get; set; }
|
public DateTime? OlderThan { get; set; }
|
||||||
}
|
}
|
||||||
@@ -161,6 +172,15 @@ namespace AutoBidder.Utilities
|
|||||||
if (options.RemoveWithoutValue && r.BuyNowPrice is not > 0)
|
if (options.RemoveWithoutValue && r.BuyNowPrice is not > 0)
|
||||||
Mark(r, "senza valore del prodotto");
|
Mark(r, "senza valore del prodotto");
|
||||||
|
|
||||||
|
if (options.MinObservedCoverage is { } soglia)
|
||||||
|
{
|
||||||
|
// Copertura non calcolabile = asta senza prezzo finale: qui vale come
|
||||||
|
// "non seguita", perché non c'è modo di dire il contrario.
|
||||||
|
var copertura = AuctionIntegrity.ObservedCoverage(r);
|
||||||
|
if (copertura is null || copertura.Value < soglia)
|
||||||
|
Mark(r, "seguita solo in parte");
|
||||||
|
}
|
||||||
|
|
||||||
if (options.OlderThan is { } limite && r.EndedAt < limite)
|
if (options.OlderThan is { } limite && r.EndedAt < limite)
|
||||||
Mark(r, "più vecchia del limite");
|
Mark(r, "più vecchia del limite");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -500,6 +500,27 @@ namespace AutoBidder.ViewModels
|
|||||||
OnPropertyChanged(nameof(LatencyDisplay)); // ? AGGIUNTO - latenza cambia quando si attiva/disattiva
|
OnPropertyChanged(nameof(LatencyDisplay)); // ? AGGIUNTO - latenza cambia quando si attiva/disattiva
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rinfresca il solo tempo mancante, senza che sia arrivata una risposta nuova.
|
||||||
|
///
|
||||||
|
/// <para>Il valore era già giusto — <see cref="TimerDisplay"/> lo ricava dalla
|
||||||
|
/// scadenza ancorata all'orologio del server, non dall'ultimo poll — ma nessuno lo
|
||||||
|
/// richiedeva finché non arrivava un'interrogazione. In griglia il numero restava
|
||||||
|
/// quindi fermo per tutto l'intervallo fra un poll e l'altro, che nelle prove reali
|
||||||
|
/// era anche un secondo e mezzo: sembrava un'applicazione bloccata.</para>
|
||||||
|
///
|
||||||
|
/// <para>Si notificano solo le tre proprietà che dipendono dal tempo. Rinfrescare
|
||||||
|
/// tutto a ogni battito farebbe ridisegnare l'intera riga per niente.</para>
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshTimeDisplay()
|
||||||
|
{
|
||||||
|
if (_lastState == null || IsFinished) return;
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(TimerDisplay));
|
||||||
|
OnPropertyChanged(nameof(TimerUrgency));
|
||||||
|
OnPropertyChanged(nameof(SortRemainingMs));
|
||||||
|
}
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
protected void OnPropertyChanged(string propertyName)
|
protected void OnPropertyChanged(string propertyName)
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
; ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
; AutoBidder — script di installazione (Inno Setup 6)
|
||||||
|
;
|
||||||
|
; Non si compila a mano: lo lancia build/Release.proj, che prima pubblica
|
||||||
|
; l'eseguibile e poi passa qui versione e percorsi con /D. Compilarlo da solo
|
||||||
|
; produrrebbe un pacchetto con la versione sbagliata, perché il numero vive
|
||||||
|
; nel .csproj.
|
||||||
|
;
|
||||||
|
; dotnet msbuild build/Release.proj -t:Pacchetto
|
||||||
|
;
|
||||||
|
; L'applicazione è un unico .exe self-contained: l'installazione copia quel file,
|
||||||
|
; crea i collegamenti e registra la voce per "App installate". I dati dell'utente
|
||||||
|
; stanno in %LocalAppData%\AutoBidder e NON vengono toccati né dall'aggiornamento
|
||||||
|
; né dalla disinstallazione — sono aste, statistiche e sessione, cioè mesi di
|
||||||
|
; raccolta che nessun disinstallatore ha il diritto di buttare via.
|
||||||
|
; ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#ifndef AppVersion
|
||||||
|
#define AppVersion "0.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SourceExe
|
||||||
|
#define SourceExe "..\bin\publish\win-x64\AutoBidder.exe"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef OutputDir
|
||||||
|
#define OutputDir "..\bin\installer"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define AppName "AutoBidder"
|
||||||
|
#define AppPublisher "Alberto Balbo"
|
||||||
|
#define AppExeName "AutoBidder.exe"
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
; L'AppId identifica il prodotto fra una versione e l'altra: cambiarlo farebbe
|
||||||
|
; comparire due voci in "App installate" invece di un aggiornamento.
|
||||||
|
AppId={{9F2C6E14-8B3D-4A57-9E10-6C4B0D7A1F52}
|
||||||
|
AppName={#AppName}
|
||||||
|
AppVersion={#AppVersion}
|
||||||
|
AppVerName={#AppName} {#AppVersion}
|
||||||
|
AppPublisher={#AppPublisher}
|
||||||
|
VersionInfoVersion={#AppVersion}
|
||||||
|
|
||||||
|
; Installazione per utente: niente richiesta di amministratore, e i dati stanno
|
||||||
|
; comunque nel profilo di chi la usa.
|
||||||
|
PrivilegesRequired=lowest
|
||||||
|
DefaultDirName={autopf}\{#AppName}
|
||||||
|
DefaultGroupName={#AppName}
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
DisableDirPage=auto
|
||||||
|
|
||||||
|
OutputDir={#OutputDir}
|
||||||
|
OutputBaseFilename=AutoBidder_{#AppVersion}
|
||||||
|
SetupIconFile=..\Icon\favicon.ico
|
||||||
|
UninstallDisplayIcon={app}\{#AppExeName}
|
||||||
|
UninstallDisplayName={#AppName} {#AppVersion}
|
||||||
|
|
||||||
|
; L'eseguibile è già compresso (single-file compresso): comprimerlo di nuovo
|
||||||
|
; costa minuti di build e guadagna poco.
|
||||||
|
Compression=lzma2/normal
|
||||||
|
SolidCompression=no
|
||||||
|
WizardStyle=modern
|
||||||
|
ArchitecturesAllowed=x64compatible
|
||||||
|
ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
|
|
||||||
|
[Languages]
|
||||||
|
Name: "italiano"; MessagesFile: "compiler:Languages\Italian.isl"
|
||||||
|
|
||||||
|
[Tasks]
|
||||||
|
Name: "desktopicon"; Description: "Crea un collegamento sul desktop"; GroupDescription: "Collegamenti:"
|
||||||
|
Name: "avvioautomatico"; Description: "Avvia AutoBidder all'accesso a Windows"; GroupDescription: "Avvio:"; Flags: unchecked
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: "{#SourceExe}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
|
||||||
|
Name: "{group}\Disinstalla {#AppName}"; Filename: "{uninstallexe}"
|
||||||
|
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
|
[Registry]
|
||||||
|
Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; \
|
||||||
|
ValueType: string; ValueName: "{#AppName}"; ValueData: """{app}\{#AppExeName}"""; \
|
||||||
|
Flags: uninsdeletevalue; Tasks: avvioautomatico
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{app}\{#AppExeName}"; Description: "Avvia {#AppName}"; \
|
||||||
|
Flags: nowait postinstall skipifsilent
|
||||||
|
|
||||||
|
[UninstallDelete]
|
||||||
|
; Solo quello che ha creato l'installazione. I dati restano.
|
||||||
|
Type: dirifempty; Name: "{app}"
|
||||||
|
|
||||||
|
[Code]
|
||||||
|
{ Un'installazione sopra una copia in esecuzione lascerebbe il vecchio eseguibile
|
||||||
|
al suo posto senza dirlo, e l'utente si ritroverebbe la versione precedente
|
||||||
|
convinto di aver aggiornato. Meglio fermarsi e chiedere. }
|
||||||
|
function IsAppRunning(): Boolean;
|
||||||
|
var
|
||||||
|
ResultCode: Integer;
|
||||||
|
begin
|
||||||
|
Result := Exec('cmd.exe',
|
||||||
|
'/C tasklist /FI "IMAGENAME eq {#AppExeName}" | find /I "{#AppExeName}"',
|
||||||
|
'', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function InitializeSetup(): Boolean;
|
||||||
|
begin
|
||||||
|
Result := True;
|
||||||
|
|
||||||
|
if IsAppRunning() then
|
||||||
|
begin
|
||||||
|
if MsgBox('AutoBidder è in esecuzione e sta forse seguendo delle aste.' + #13#10#13#10 +
|
||||||
|
'Chiudilo prima di continuare, altrimenti il file non può essere sostituito.' + #13#10#13#10 +
|
||||||
|
'Riprovo?', mbConfirmation, MB_YESNO) = IDYES then
|
||||||
|
Result := not IsAppRunning()
|
||||||
|
else
|
||||||
|
Result := False;
|
||||||
|
|
||||||
|
if not Result then
|
||||||
|
MsgBox('Installazione annullata: AutoBidder è ancora aperto.', mbInformation, MB_OK);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Catena di verifica, pacchetto e rilascio
|
||||||
|
|
||||||
|
Tutto quello che serve a controllare, impacchettare e pubblicare AutoBidder sta in questa
|
||||||
|
cartella. La radice del progetto non contiene script.
|
||||||
|
|
||||||
|
| File | Cos'è |
|
||||||
|
|---|---|
|
||||||
|
| `Release.proj` | La catena. Un solo file MSBuild, nessuno script. |
|
||||||
|
| `AutoBidder.iss` | Lo script di Inno Setup. Non si compila a mano: lo lancia `Release.proj`. |
|
||||||
|
| `gitea.example.json` | Modello per `gitea.json` (che è escluso dal controllo di versione). |
|
||||||
|
| `sostituiti/` | I vecchi script PowerShell, tenuti finché non si fa la pulizia. Non servono più. |
|
||||||
|
|
||||||
|
## Da VS Code
|
||||||
|
|
||||||
|
**Terminale ▸ Esegui attività…**
|
||||||
|
|
||||||
|
| Attività | Cosa fa |
|
||||||
|
|---|---|
|
||||||
|
| `verifica` | Compila e lancia i test. |
|
||||||
|
| `backtest` | Rigioca i dossier delle aste concluse. |
|
||||||
|
| `backtest (giro veloce, 400 aste)` | Come sopra, su un campione. |
|
||||||
|
| `crea installatore` | Chiede la versione, verifica, pubblica, esegue Inno Setup. |
|
||||||
|
| `crea installatore (senza rieseguire i test)` | Solo pubblicazione e installatore. |
|
||||||
|
| `rilascia su Gitea` | Tutto quanto sopra, più tag e release con i file allegati. |
|
||||||
|
|
||||||
|
## Da riga di comando
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet msbuild build/Release.proj -t:Verifica
|
||||||
|
dotnet msbuild build/Release.proj -t:Backtest -p:MaxDossier=400
|
||||||
|
dotnet msbuild build/Release.proj -t:Pacchetto
|
||||||
|
dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=4.13.0 -p:Note="Cosa cambia"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Proprietà | Predefinito | A cosa serve |
|
||||||
|
|---|---|---|
|
||||||
|
| `Versione` | vuoto | Vuoto = incrementa la minor (4.12.0 → 4.13.0). Altrimenti la scrive, se ha la forma `X.Y.Z`. |
|
||||||
|
| `Note` | vuoto | Note di rilascio. |
|
||||||
|
| `SaltaVerifica` | `false` | Non rieseguire i test. |
|
||||||
|
| `Sovrascrivi` | `false` | Sostituisci una release Gitea con lo stesso tag. |
|
||||||
|
| `Bozza` | `false` | Crea la release come bozza. |
|
||||||
|
| `MaxDossier` | `0` | Quanti dossier leggere nel backtest (0 = tutti). |
|
||||||
|
| `CartellaDossier` | `%LocalAppData%\AutoBidder\Dati\Registri\Aste` | Dove stanno i dossier. |
|
||||||
|
|
||||||
|
## Chi chiede la versione
|
||||||
|
|
||||||
|
MSBuild non può chiedere niente a nessuno: è un motore di compilazione. La domanda la fa
|
||||||
|
l'attività di VS Code (`inputs` in `.vscode/tasks.json`) e passa la risposta in
|
||||||
|
`-p:Versione=`. Lasciando il campo vuoto si prende la minor successiva, che è il caso
|
||||||
|
normale di fine sessione.
|
||||||
|
|
||||||
|
**La versione si scrive solo dopo che i test sono passati.** Il contrario sembra più
|
||||||
|
naturale — decidi il numero, poi costruisci — ma lascia il `.csproj` alzato quando la
|
||||||
|
verifica fallisce, e il tentativo dopo riparte da lì: il numero sale senza che sia mai
|
||||||
|
esistito un pacchetto con quella versione.
|
||||||
|
|
||||||
|
## Gitea
|
||||||
|
|
||||||
|
Servono quattro valori. Le variabili d'ambiente hanno la precedenza sul file, così una
|
||||||
|
macchina condivisa può rilasciare senza scrivere un token su disco:
|
||||||
|
|
||||||
|
- `GITEA_URL`, `GITEA_OWNER`, `GITEA_REPO`, `GITEA_TOKEN`
|
||||||
|
- oppure `build/gitea.json`, copiato da `gitea.example.json`
|
||||||
|
|
||||||
|
Il token si crea in Gitea da *Impostazioni ▸ Applicazioni ▸ Genera nuovo token*, con il
|
||||||
|
permesso `repository: read and write`.
|
||||||
|
|
||||||
|
Nella release vengono caricati **sia l'installatore sia l'eseguibile nudo**: chi non vuole
|
||||||
|
installare niente deve continuare a poter scaricare il solo `.exe`.
|
||||||
|
|
||||||
|
## Una trappola già pagata
|
||||||
|
|
||||||
|
`dotnet test` e `dotnet publish` lanciati **da dentro** MSBuild ereditano l'ambiente del
|
||||||
|
processo padre. La compilazione WPF crea un progetto temporaneo (`_wpftmp.csproj`) e con
|
||||||
|
`MSBUILD_EXE_PATH` puntata al build in corso non genera più le classi parziali dello XAML:
|
||||||
|
si ottengono decine di *«AuctionMonitorControl non contiene una definizione di …»* che non
|
||||||
|
hanno niente a che vedere col codice.
|
||||||
|
|
||||||
|
Per questo gli `Exec` azzerano `MSBUILD_EXE_PATH` e `MSBuildLoadMicrosoftTargetsReadOnly`.
|
||||||
|
**Solo quelle due**: la ricetta che gira in rete azzera anche `MSBuildExtensionsPath` e
|
||||||
|
`MSBuildSDKsPath`, e così il figlio perde la posizione dell'SDK — *«l'SDK Microsoft.NET.Sdk
|
||||||
|
specificato non è stato trovato»*. Serve isolare il motore, non nascondergli dove abita.
|
||||||
|
|
||||||
|
## Prerequisiti
|
||||||
|
|
||||||
|
- .NET SDK 10
|
||||||
|
- [Inno Setup 6](https://jrsoftware.org/isinfo.php) — `winget install -e --id JRSoftware.InnoSetup`
|
||||||
@@ -0,0 +1,647 @@
|
|||||||
|
<!--
|
||||||
|
═══════════════════════════════════════════════════════════════════════════
|
||||||
|
AutoBidder — catena di verifica, pacchetto e rilascio
|
||||||
|
═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Un solo file, nessuno script. Si richiama con `dotnet msbuild`, dalle attività
|
||||||
|
di VS Code (Terminale ▸ Esegui attività…) oppure a mano:
|
||||||
|
|
||||||
|
dotnet msbuild build/Release.proj -t:Verifica
|
||||||
|
dotnet msbuild build/Release.proj -t:Backtest
|
||||||
|
dotnet msbuild build/Release.proj -t:Pacchetto
|
||||||
|
dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=4.12.0
|
||||||
|
|
||||||
|
── Perché MSBuild e non uno script ──────────────────────────────────────
|
||||||
|
La catena vive accanto al codice che rilascia ed è versionata con lui: fra
|
||||||
|
sei mesi, ripescato un tag, questo file ricostruisce quel pacchetto e non
|
||||||
|
quello di oggi. La logica non banale (leggere e riscrivere la versione,
|
||||||
|
parlare con Gitea) sta in attività C# in linea: si legge come codice, non
|
||||||
|
come una successione di comandi.
|
||||||
|
|
||||||
|
── Da dove viene la versione ────────────────────────────────────────────
|
||||||
|
Dal tag git, e da nient'altro. Il .csproj non viene mai riscritto: il numero
|
||||||
|
arriva a `dotnet publish` come proprietà da riga di comando, quindi tag,
|
||||||
|
eseguibile, installatore e release portano lo stesso numero per costruzione,
|
||||||
|
non per disciplina.
|
||||||
|
|
||||||
|
Il modo previsto è taggare e poi rilasciare:
|
||||||
|
|
||||||
|
git tag v4.14.0
|
||||||
|
dotnet msbuild build/Release.proj -t:Rilascia
|
||||||
|
|
||||||
|
Se HEAD non ha un tag di versione la catena lo crea da sé — con il numero
|
||||||
|
passato in `-p:Versione=`, oppure la minor successiva all'ultimo tag — e lo
|
||||||
|
fa in fondo, quando l'installatore esiste davvero. Un giro andato male non
|
||||||
|
lascia dietro un tag per una versione che non è mai stata costruita.
|
||||||
|
|
||||||
|
Il numero nel .csproj resta quello delle compilazioni di sviluppo, e non
|
||||||
|
c'entra più con i rilasci.
|
||||||
|
-->
|
||||||
|
<Project DefaultTargets="Pacchetto" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Radice>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..'))</Radice>
|
||||||
|
<Csproj>$(Radice)\AutoBidder.csproj</Csproj>
|
||||||
|
<TestProj>$(Radice)\Tests\AutoBidder.Tests.csproj</TestProj>
|
||||||
|
<Iss>$(MSBuildThisFileDirectory)AutoBidder.iss</Iss>
|
||||||
|
|
||||||
|
<CartellaPubblicazione>$(Radice)\bin\publish\win-x64</CartellaPubblicazione>
|
||||||
|
<CartellaPacchetti>$(Radice)\bin\installer</CartellaPacchetti>
|
||||||
|
|
||||||
|
<!-- ── Perché una cartella a parte per verifica e backtest ─────────────
|
||||||
|
Due ragioni, e servono entrambe.
|
||||||
|
|
||||||
|
La prima: l'applicazione può essere aperta mentre si lavora, e tiene
|
||||||
|
bloccato AutoBidder.exe — la compilazione si fermerebbe su MSB3027.
|
||||||
|
|
||||||
|
La seconda: l'opzione artifacts-path sposta anche gli INTERMEDI, non
|
||||||
|
solo il risultato. La sola -o li lascia nella obj/ condivisa, e la
|
||||||
|
compilazione WPF — che genera un progetto temporaneo `_wpftmp.csproj` a
|
||||||
|
ogni giro — ogni tanto ci trovava stato altrui e smetteva di produrre
|
||||||
|
le classi parziali dello XAML. Il sintomo era una raffica di
|
||||||
|
"AuctionMonitorControl non contiene una definizione di ...", a giri
|
||||||
|
alterni, senza che il codice fosse cambiato. -->
|
||||||
|
<CartellaProve>$([System.IO.Path]::GetTempPath())AutoBidder.Verifica</CartellaProve>
|
||||||
|
|
||||||
|
<!-- Serve solo quando HEAD non è ancora taggato: è il numero del tag da
|
||||||
|
creare. Vuoto = la minor successiva all'ultimo tag. Vedi VersioneDaTag. -->
|
||||||
|
<Versione Condition="'$(Versione)' == ''"></Versione>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Note di rilascio. Vuote = solo il numero di versione.
|
||||||
|
|
||||||
|
Arrivano da una variabile d'ambiente, non da -p:. MSBuild spezza il valore
|
||||||
|
di una proprietà sulle virgole: `-p:Note=uno, due` diventa la proprietà
|
||||||
|
Note=uno più l'opzione " due", e si finisce su MSB1006 "proprietà non
|
||||||
|
valida". Una nota di rilascio in italiano contiene quasi sempre una
|
||||||
|
virgola, quindi il passaggio per riga di comando è inutilizzabile.
|
||||||
|
L'ambiente non ha questo problema, e MSBuild legge le variabili
|
||||||
|
d'ambiente come proprietà.
|
||||||
|
|
||||||
|
-p:Note= resta accettato per chi lo passa a mano senza virgole.
|
||||||
|
-->
|
||||||
|
<Note Condition="'$(Note)' == ''">$(AUTOBIDDER_NOTE)</Note>
|
||||||
|
|
||||||
|
<!-- ── Perché serve azzerare queste variabili ──────────────────────────
|
||||||
|
`dotnet test` e `dotnet publish` lanciati da dentro MSBuild ereditano
|
||||||
|
l'ambiente del processo padre. La compilazione WPF crea un progetto
|
||||||
|
temporaneo (_wpftmp.csproj) e con quelle variabili puntate al build in
|
||||||
|
corso non genera più le classi parziali dello XAML: si ottengono decine
|
||||||
|
di "AuctionMonitorControl non contiene una definizione di ..." che non
|
||||||
|
hanno niente a che vedere col codice.
|
||||||
|
|
||||||
|
Si azzerano SOLO queste due. Togliere anche MSBuildExtensionsPath o
|
||||||
|
MSBuildSDKsPath — la ricetta che gira in rete — fa perdere al figlio la
|
||||||
|
posizione dell'SDK: "l'SDK Microsoft.NET.Sdk specificato non è stato
|
||||||
|
trovato". Serve isolare il motore, non nascondergli dove abita. -->
|
||||||
|
<AmbientePulito>MSBUILD_EXE_PATH=;MSBuildLoadMicrosoftTargetsReadOnly=</AmbientePulito>
|
||||||
|
|
||||||
|
<SaltaVerifica Condition="'$(SaltaVerifica)' == ''">false</SaltaVerifica>
|
||||||
|
<Sovrascrivi Condition="'$(Sovrascrivi)' == ''">false</Sovrascrivi>
|
||||||
|
<Bozza Condition="'$(Bozza)' == ''">false</Bozza>
|
||||||
|
|
||||||
|
<!-- Un rilascio da albero sporco produce un tag che non corrisponde a
|
||||||
|
nessuno stato ricostruibile: è esattamente la garanzia che il tag come
|
||||||
|
fonte unica dovrebbe dare. Si può forzare, ma va detto. -->
|
||||||
|
<ConsentiModifiche Condition="'$(ConsentiModifiche)' == ''">false</ConsentiModifiche>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- ═════════════════════ Attività in linea ═════════════════════ -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Decide quale versione si sta costruendo, senza toccare niente.
|
||||||
|
|
||||||
|
Il tag su HEAD, se c'è, comanda: è la fonte. Se non c'è se ne propone uno —
|
||||||
|
il numero chiesto, o la minor successiva al tag più alto esistente — che
|
||||||
|
verrà creato solo a pacchetto pronto, da CreaTag.
|
||||||
|
-->
|
||||||
|
<UsingTask TaskName="VersioneDaTag" TaskFactory="RoslynCodeTaskFactory"
|
||||||
|
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||||
|
<ParameterGroup>
|
||||||
|
<TagsHead ParameterType="System.String" />
|
||||||
|
<TagsTutti ParameterType="System.String" />
|
||||||
|
<Richiesta ParameterType="System.String" />
|
||||||
|
<Csproj ParameterType="System.String" Required="true" />
|
||||||
|
<Versione ParameterType="System.String" Output="true" />
|
||||||
|
<Tag ParameterType="System.String" Output="true" />
|
||||||
|
<DaCreare ParameterType="System.String" Output="true" />
|
||||||
|
</ParameterGroup>
|
||||||
|
<Task>
|
||||||
|
<Using Namespace="System" />
|
||||||
|
<Using Namespace="System.IO" />
|
||||||
|
<Using Namespace="System.Collections.Generic" />
|
||||||
|
<Using Namespace="System.Text.RegularExpressions" />
|
||||||
|
<Code Type="Fragment" Language="cs">
|
||||||
|
<![CDATA[
|
||||||
|
// ConsoleToMSBuild unisce le righe con il punto e virgola; git le separa
|
||||||
|
// con a-capo. Si accettano entrambi e non si fanno domande.
|
||||||
|
Func<string, string[]> spezza = s =>
|
||||||
|
(s ?? "").Split(new[] { ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
var forma = new Regex(@"^v(\d+)\.(\d+)\.(\d+)$");
|
||||||
|
|
||||||
|
var suHead = new List<string>();
|
||||||
|
foreach (var t in spezza(TagsHead))
|
||||||
|
if (forma.IsMatch(t.Trim())) suHead.Add(t.Trim());
|
||||||
|
|
||||||
|
if (suHead.Count > 1)
|
||||||
|
{
|
||||||
|
Log.LogError(
|
||||||
|
"HEAD ha piu' di un tag di versione (" + string.Join(", ", suHead) + ").\n" +
|
||||||
|
"Non si puo' sapere quale sia il rilascio: tienine uno solo con git tag -d <tag>");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chiesta = (Richiesta ?? "").Trim();
|
||||||
|
if (chiesta.Length > 0 && !Regex.IsMatch(chiesta, @"^\d+\.\d+\.\d+$"))
|
||||||
|
{
|
||||||
|
// Un refuso qui produrrebbe un tag e un pacchetto sbagliati.
|
||||||
|
Log.LogError("Versione '" + chiesta + "' non valida: serve la forma X.Y.Z");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suHead.Count == 1)
|
||||||
|
{
|
||||||
|
var dalTag = suHead[0].Substring(1);
|
||||||
|
|
||||||
|
if (chiesta.Length > 0 && chiesta != dalTag)
|
||||||
|
{
|
||||||
|
Log.LogError(
|
||||||
|
"HEAD e' gia' taggato " + suHead[0] + ", ma e' stata chiesta la versione " + chiesta + ".\n" +
|
||||||
|
"Il tag e' la fonte: o rilasci " + dalTag + " lasciando vuota la versione,\n" +
|
||||||
|
"oppure togli il tag con git tag -d " + suHead[0] + " e rilancia.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Versione = dalTag;
|
||||||
|
Tag = suHead[0];
|
||||||
|
DaCreare = "false";
|
||||||
|
Log.LogMessage(MessageImportance.High, " versione " + Versione + " — dal tag " + Tag + " su HEAD");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chiesta.Length > 0)
|
||||||
|
{
|
||||||
|
Versione = chiesta;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// La minor successiva al tag piu' alto: cosi' il numero proposto e'
|
||||||
|
// sempre libero, anche se il tag piu' alto sta su un altro ramo.
|
||||||
|
int maggiore = -1, minore = -1;
|
||||||
|
foreach (var t in spezza(TagsTutti))
|
||||||
|
{
|
||||||
|
var m = forma.Match(t.Trim());
|
||||||
|
if (!m.Success) continue;
|
||||||
|
|
||||||
|
int ma = int.Parse(m.Groups[1].Value), mi = int.Parse(m.Groups[2].Value);
|
||||||
|
if (ma > maggiore || (ma == maggiore && mi > minore)) { maggiore = ma; minore = mi; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maggiore < 0)
|
||||||
|
{
|
||||||
|
// Primo rilascio: non c'e' nessun tag da cui ripartire, e l'unico
|
||||||
|
// numero che esiste e' quello delle compilazioni di sviluppo.
|
||||||
|
var testo = File.Exists(Csproj) ? File.ReadAllText(Csproj) : "";
|
||||||
|
var m = Regex.Match(testo, @"<Version>(\d+)\.(\d+)\.\d+</Version>");
|
||||||
|
if (!m.Success)
|
||||||
|
{
|
||||||
|
Log.LogError(
|
||||||
|
"Nessun tag di versione nel repository e <Version> illeggibile in " + Csproj + ".\n" +
|
||||||
|
"Indica la versione con -p:Versione=X.Y.Z");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
maggiore = int.Parse(m.Groups[1].Value);
|
||||||
|
minore = int.Parse(m.Groups[2].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Versione = maggiore + "." + (minore + 1) + ".0";
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag = "v" + Versione;
|
||||||
|
DaCreare = "true";
|
||||||
|
Log.LogMessage(MessageImportance.High, " versione " + Versione + " — il tag " + Tag + " sara' creato a pacchetto pronto");
|
||||||
|
]]>
|
||||||
|
</Code>
|
||||||
|
</Task>
|
||||||
|
</UsingTask>
|
||||||
|
|
||||||
|
<UsingTask TaskName="TrovaInnoSetup" TaskFactory="RoslynCodeTaskFactory"
|
||||||
|
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||||
|
<ParameterGroup>
|
||||||
|
<Percorso ParameterType="System.String" Output="true" />
|
||||||
|
</ParameterGroup>
|
||||||
|
<Task>
|
||||||
|
<Using Namespace="System" />
|
||||||
|
<Using Namespace="System.IO" />
|
||||||
|
<Code Type="Fragment" Language="cs">
|
||||||
|
<![CDATA[
|
||||||
|
var candidati = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Inno Setup 6\ISCC.exe"),
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Inno Setup 6\ISCC.exe"),
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Inno Setup 6\ISCC.exe"),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var c in candidati)
|
||||||
|
if (File.Exists(c)) { Percorso = c; break; }
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(Percorso))
|
||||||
|
Log.LogError("Inno Setup 6 non trovato. Installalo con: winget install -e --id JRSoftware.InnoSetup");
|
||||||
|
]]>
|
||||||
|
</Code>
|
||||||
|
</Task>
|
||||||
|
</UsingTask>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Gitea si raggiunge con curl, di serie in Windows 10 e 11.
|
||||||
|
|
||||||
|
L'alternativa naturale sarebbe HttpClient in un'attività C# in linea, ma
|
||||||
|
RoslynCodeTaskFactory referenzia solo gli assembly di base: System.Net.Http e
|
||||||
|
System.Text.Json andrebbero indicati per percorso assoluto, dentro il runtime
|
||||||
|
condiviso, con il numero di versione nel mezzo. Un percorso che oggi funziona e
|
||||||
|
al prossimo aggiornamento di .NET no. curl non ha questo problema.
|
||||||
|
|
||||||
|
Il token NON passa mai dalla riga di comando: sta in un file di configurazione
|
||||||
|
di curl, che viene cancellato subito dopo. Gli Exec hanno EchoOff perché un
|
||||||
|
registro di compilazione è la classica cosa che si incolla in una chat.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<UsingTask TaskName="LeggiConfigGitea" TaskFactory="RoslynCodeTaskFactory"
|
||||||
|
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||||
|
<ParameterGroup>
|
||||||
|
<Percorso ParameterType="System.String" Required="true" />
|
||||||
|
<Url ParameterType="System.String" Output="true" />
|
||||||
|
<Owner ParameterType="System.String" Output="true" />
|
||||||
|
<Repo ParameterType="System.String" Output="true" />
|
||||||
|
<Token ParameterType="System.String" Output="true" />
|
||||||
|
</ParameterGroup>
|
||||||
|
<Task>
|
||||||
|
<Using Namespace="System" />
|
||||||
|
<Using Namespace="System.IO" />
|
||||||
|
<Using Namespace="System.Text.RegularExpressions" />
|
||||||
|
<Code Type="Fragment" Language="cs">
|
||||||
|
<![CDATA[
|
||||||
|
var testo = File.Exists(Percorso) ? File.ReadAllText(Percorso) : "";
|
||||||
|
|
||||||
|
// Le variabili d'ambiente hanno la precedenza sul file: una macchina
|
||||||
|
// condivisa deve poter rilasciare senza scrivere un token su disco, e un
|
||||||
|
// file dimenticato non deve vincere su una scelta esplicita.
|
||||||
|
Func<string,string,string> leggi = (env, campo) =>
|
||||||
|
{
|
||||||
|
var v = Environment.GetEnvironmentVariable(env);
|
||||||
|
if (!string.IsNullOrWhiteSpace(v)) return v.Trim();
|
||||||
|
|
||||||
|
var m = Regex.Match(testo, "\"" + campo + "\"\\s*:\\s*\"([^\"]*)\"");
|
||||||
|
return m.Success ? m.Groups[1].Value.Trim() : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
Url = leggi("GITEA_URL", "url").TrimEnd('/');
|
||||||
|
Owner = leggi("GITEA_OWNER", "owner");
|
||||||
|
Repo = leggi("GITEA_REPO", "repo");
|
||||||
|
Token = leggi("GITEA_TOKEN", "token");
|
||||||
|
|
||||||
|
if (Url.Length == 0 || Owner.Length == 0 || Repo.Length == 0 || Token.Length == 0)
|
||||||
|
{
|
||||||
|
Log.LogError(
|
||||||
|
"Configurazione di Gitea incompleta. Servono url, owner, repo, token:\n" +
|
||||||
|
" copia build/gitea.example.json in build/gitea.json e riempilo,\n" +
|
||||||
|
" oppure imposta GITEA_URL, GITEA_OWNER, GITEA_REPO, GITEA_TOKEN.\n" +
|
||||||
|
"Non e' stato costruito niente: si controlla prima di compilare, non dopo.\n" +
|
||||||
|
"Per il solo installatore, senza Gitea, usa il target Pacchetto.");
|
||||||
|
|
||||||
|
// Senza questo il target prosegue lo stesso: git tag, poi curl con
|
||||||
|
// l'indirizzo vuoto, e infine un "codice 3" che non dice niente a
|
||||||
|
// nessuno. Un errore va fermato dove si capisce ancora cos'era.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
]]>
|
||||||
|
</Code>
|
||||||
|
</Task>
|
||||||
|
</UsingTask>
|
||||||
|
|
||||||
|
<UsingTask TaskName="PreparaCorpoRelease" TaskFactory="RoslynCodeTaskFactory"
|
||||||
|
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||||
|
<ParameterGroup>
|
||||||
|
<Destinazione ParameterType="System.String" Required="true" />
|
||||||
|
<Tag ParameterType="System.String" Required="true" />
|
||||||
|
<Versione ParameterType="System.String" Required="true" />
|
||||||
|
<Note ParameterType="System.String" />
|
||||||
|
<Bozza ParameterType="System.Boolean" />
|
||||||
|
</ParameterGroup>
|
||||||
|
<Task>
|
||||||
|
<Using Namespace="System" />
|
||||||
|
<Using Namespace="System.IO" />
|
||||||
|
<Code Type="Fragment" Language="cs">
|
||||||
|
<![CDATA[
|
||||||
|
// Le note arrivano da un prompt: possono contenere virgolette, barre e
|
||||||
|
// a-capo. Scritte grezze romperebbero il JSON, o peggio lo cambierebbero.
|
||||||
|
Func<string,string> esc = t => (t ?? "")
|
||||||
|
.Replace("\\", "\\\\").Replace("\"", "\\\"")
|
||||||
|
.Replace("\r", "").Replace("\n", "\\n").Replace("\t", " ");
|
||||||
|
|
||||||
|
var note = string.IsNullOrWhiteSpace(Note) ? "Versione " + Versione + "." : Note;
|
||||||
|
|
||||||
|
File.WriteAllText(Destinazione,
|
||||||
|
"{\"tag_name\":\"" + esc(Tag) + "\"," +
|
||||||
|
"\"name\":\"AutoBidder " + esc(Versione) + "\"," +
|
||||||
|
"\"body\":\"" + esc(note) + "\"," +
|
||||||
|
"\"draft\":" + (Bozza ? "true" : "false") + "," +
|
||||||
|
"\"prerelease\":false}");
|
||||||
|
]]>
|
||||||
|
</Code>
|
||||||
|
</Task>
|
||||||
|
</UsingTask>
|
||||||
|
|
||||||
|
<UsingTask TaskName="LeggiIdRelease" TaskFactory="RoslynCodeTaskFactory"
|
||||||
|
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||||
|
<ParameterGroup>
|
||||||
|
<Risposta ParameterType="System.String" Required="true" />
|
||||||
|
<Id ParameterType="System.String" Output="true" />
|
||||||
|
<Errore ParameterType="System.String" Output="true" />
|
||||||
|
</ParameterGroup>
|
||||||
|
<Task>
|
||||||
|
<Using Namespace="System" />
|
||||||
|
<Using Namespace="System.IO" />
|
||||||
|
<Using Namespace="System.Text.RegularExpressions" />
|
||||||
|
<Code Type="Fragment" Language="cs">
|
||||||
|
<![CDATA[
|
||||||
|
var testo = File.Exists(Risposta) ? File.ReadAllText(Risposta) : "";
|
||||||
|
|
||||||
|
// L'id della release e' il primo campo "id" della risposta; quelli annidati
|
||||||
|
// (autore, allegati) vengono dopo. Si prende il primo e basta.
|
||||||
|
var m = Regex.Match(testo, "\"id\"\\s*:\\s*(\\d+)");
|
||||||
|
Id = m.Success ? m.Groups[1].Value : "";
|
||||||
|
|
||||||
|
if (Id.Length == 0)
|
||||||
|
{
|
||||||
|
var msg = Regex.Match(testo, "\"message\"\\s*:\\s*\"([^\"]*)\"");
|
||||||
|
Errore = msg.Success ? msg.Groups[1].Value
|
||||||
|
: (testo.Length > 200 ? testo.Substring(0, 200) : testo);
|
||||||
|
}
|
||||||
|
]]>
|
||||||
|
</Code>
|
||||||
|
</Task>
|
||||||
|
</UsingTask>
|
||||||
|
|
||||||
|
<!-- ═════════════════════ Verifica ═════════════════════ -->
|
||||||
|
|
||||||
|
<Target Name="Verifica" Condition="'$(SaltaVerifica)' != 'true'">
|
||||||
|
<Message Importance="High" Text="== Verifica (compilazione + test) ==" />
|
||||||
|
|
||||||
|
<!-- In una cartella a parte: l'applicazione puo' essere aperta e tenere
|
||||||
|
bloccato AutoBidder.exe. -->
|
||||||
|
<Exec Command="dotnet test "$(TestProj)" --nologo -v q --artifacts-path "$(CartellaProve)""
|
||||||
|
WorkingDirectory="$(Radice)"
|
||||||
|
EnvironmentVariables="$(AmbientePulito)" />
|
||||||
|
|
||||||
|
<Message Importance="High" Text=" tutto a posto" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!-- ═════════════════════ Rigiocata sui dossier ═════════════════════ -->
|
||||||
|
|
||||||
|
<Target Name="Backtest">
|
||||||
|
<PropertyGroup>
|
||||||
|
<CartellaDossier Condition="'$(CartellaDossier)' == ''">$(LOCALAPPDATA)\AutoBidder\Dati\Registri\Aste</CartellaDossier>
|
||||||
|
<MaxDossier Condition="'$(MaxDossier)' == ''">0</MaxDossier>
|
||||||
|
<Riepilogo Condition="'$(Riepilogo)' == ''">$(Radice)\bin\backtest-report.txt</Riepilogo>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Error Condition="!Exists('$(CartellaDossier)')"
|
||||||
|
Text="Cartella dei dossier non trovata: $(CartellaDossier). Indicala con -p:CartellaDossier=..., oppure accendi «Dossier per ogni asta» nelle Impostazioni." />
|
||||||
|
|
||||||
|
<Message Importance="High" Text="== Rigiocata sui dossier ==" />
|
||||||
|
<Message Importance="High" Text=" cartella : $(CartellaDossier)" />
|
||||||
|
|
||||||
|
<Exec WorkingDirectory="$(Radice)"
|
||||||
|
Command="dotnet test "$(TestProj)" --nologo -v q --artifacts-path "$(CartellaProve)" --filter "FullyQualifiedName~RealDossierBacktest" --logger "console;verbosity=detailed""
|
||||||
|
EnvironmentVariables="$(AmbientePulito);AUTOBIDDER_BACKTEST_DIR=$(CartellaDossier);AUTOBIDDER_BACKTEST_MAX=$(MaxDossier);AUTOBIDDER_BACKTEST_OUT=$(Riepilogo)" />
|
||||||
|
|
||||||
|
<Message Importance="High" Text=" riepilogo in $(Riepilogo)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!-- ═════════════════════ Eseguibile ═════════════════════ -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
I quattro numeri di versione arrivano da qui, non dal .csproj: le proprietà
|
||||||
|
da riga di comando sono globali e vincono su quelle scritte nel progetto.
|
||||||
|
Vanno passati tutti e quattro — il .csproj li assegna esplicitamente, quindi
|
||||||
|
il solo -p:Version non ne deriverebbe nessuno. Utilities/AppInfo li legge a
|
||||||
|
runtime, e vederne divergere uno significa un titolo finestra che mente.
|
||||||
|
-->
|
||||||
|
<Target Name="Pubblica" DependsOnTargets="DeterminaVersione">
|
||||||
|
<Message Importance="High" Text="== Pubblicazione dell'eseguibile ($(V)) ==" />
|
||||||
|
|
||||||
|
<!-- ── Perché gli intermedi vanno altrove ──────────────────────────────
|
||||||
|
La sola opzione -o sposta il risultato ma lascia gli INTERMEDI nella obj/
|
||||||
|
del progetto, che è la stessa che usa la compilazione di debug lanciata da
|
||||||
|
VS Code. La compilazione WPF genera un progetto temporaneo `_wpftmp.csproj`
|
||||||
|
a ogni giro, e quando ci trova dentro lo stato di un'altra configurazione
|
||||||
|
smette di produrre le classi parziali dello XAML: si ottiene una raffica di
|
||||||
|
CS2001 «file di origine MainWindow.g.cs non trovato», a giri alterni, senza
|
||||||
|
che il codice sia cambiato.
|
||||||
|
|
||||||
|
Era già il rimedio adottato per i test; qui era rimasto indietro, e bastava
|
||||||
|
pubblicare mentre si compilava in VS Code per rompere la compilazione di
|
||||||
|
debug. L'opzione artifacts-path sposta gli intermedi, la -o continua a
|
||||||
|
decidere dove finisce l'eseguibile: le due convivono. -->
|
||||||
|
<Exec WorkingDirectory="$(Radice)"
|
||||||
|
EnvironmentVariables="$(AmbientePulito)"
|
||||||
|
Command="dotnet publish "$(Csproj)" -c Release -r win-x64 --nologo -v q --artifacts-path "$(CartellaProve)" --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:PublishReadyToRun=true -p:PublishTrimmed=false -p:Version=$(V) -p:AssemblyVersion=$(V).0 -p:FileVersion=$(V).0 -p:InformationalVersion=$(V) -o "$(CartellaPubblicazione)"" />
|
||||||
|
|
||||||
|
<Error Condition="!Exists('$(CartellaPubblicazione)\AutoBidder.exe')"
|
||||||
|
Text="Pubblicazione fallita: AutoBidder.exe non trovato." />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!-- ═════════════════════ Pacchetto ═════════════════════ -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
L'ordine conta: si verifica, si costruisce, e il tag si crea per ultimo.
|
||||||
|
|
||||||
|
Il contrario sembra piu' naturale — decidi il numero, poi costruisci — ma
|
||||||
|
lascia dietro un tag quando qualcosa va storto, e il tentativo successivo
|
||||||
|
riparte da li'. Un giro andato male e il numero e' salito lo stesso, senza
|
||||||
|
che sia mai esistito un pacchetto con quella versione. Taggando in fondo,
|
||||||
|
ogni tag corrisponde a un installatore che esiste davvero.
|
||||||
|
-->
|
||||||
|
<Target Name="Pacchetto" DependsOnTargets="Verifica;DeterminaVersione;Pubblica">
|
||||||
|
<Message Importance="High" Text="== Creazione dell'installatore ==" />
|
||||||
|
|
||||||
|
<TrovaInnoSetup>
|
||||||
|
<Output TaskParameter="Percorso" PropertyName="Iscc" />
|
||||||
|
</TrovaInnoSetup>
|
||||||
|
|
||||||
|
<MakeDir Directories="$(CartellaPacchetti)" />
|
||||||
|
|
||||||
|
<Exec WorkingDirectory="$(MSBuildThisFileDirectory)"
|
||||||
|
Command=""$(Iscc)" /Qp "/DAppVersion=$(V)" "/DSourceExe=$(CartellaPubblicazione)\AutoBidder.exe" "/DOutputDir=$(CartellaPacchetti)" "$(Iss)"" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Error Condition="!Exists('$(Setup)')" Text="Installatore non trovato: $(Setup)" />
|
||||||
|
|
||||||
|
<!-- Adesso: il pacchetto c'e', il tag puo' esistere. -->
|
||||||
|
<CallTarget Targets="CreaTag" />
|
||||||
|
|
||||||
|
<Message Importance="High" Text=" " />
|
||||||
|
<Message Importance="High" Text="Pacchetto pronto:" />
|
||||||
|
<Message Importance="High" Text=" $(Setup)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!-- ═════════════════════ Versione e tag ═════════════════════ -->
|
||||||
|
|
||||||
|
<Target Name="DeterminaVersione">
|
||||||
|
<!-- Nessuno dei due comandi fallisce mai: senza tag l'uscita e' vuota, e
|
||||||
|
basta. `git describe` invece esce in errore, e qui non serve. -->
|
||||||
|
<Exec Command="git tag --points-at HEAD" WorkingDirectory="$(Radice)"
|
||||||
|
ConsoleToMSBuild="true" StandardOutputImportance="low">
|
||||||
|
<Output TaskParameter="ConsoleOutput" PropertyName="TagSuHead" />
|
||||||
|
</Exec>
|
||||||
|
|
||||||
|
<Exec Command="git tag --list v*" WorkingDirectory="$(Radice)"
|
||||||
|
ConsoleToMSBuild="true" StandardOutputImportance="low">
|
||||||
|
<Output TaskParameter="ConsoleOutput" PropertyName="TagEsistenti" />
|
||||||
|
</Exec>
|
||||||
|
|
||||||
|
<VersioneDaTag TagsHead="$(TagSuHead)" TagsTutti="$(TagEsistenti)"
|
||||||
|
Richiesta="$(Versione)" Csproj="$(Csproj)">
|
||||||
|
<Output TaskParameter="Versione" PropertyName="V" />
|
||||||
|
<Output TaskParameter="Tag" PropertyName="Tag" />
|
||||||
|
<Output TaskParameter="DaCreare" PropertyName="TagDaCreare" />
|
||||||
|
</VersioneDaTag>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="CreaTag" Condition="'$(TagDaCreare)' == 'true'">
|
||||||
|
<!-- Solo le modifiche ai file gia' tracciati: un appunto non versionato
|
||||||
|
accanto al progetto non cambia cosa viene compilato. -->
|
||||||
|
<Exec Command="git status --porcelain --untracked-files=no" WorkingDirectory="$(Radice)"
|
||||||
|
ConsoleToMSBuild="true" StandardOutputImportance="low">
|
||||||
|
<Output TaskParameter="ConsoleOutput" PropertyName="AlberoSporco" />
|
||||||
|
</Exec>
|
||||||
|
|
||||||
|
<Error Condition="'$(AlberoSporco)' != '' AND '$(ConsentiModifiche)' != 'true'"
|
||||||
|
Text="Ci sono modifiche non committate: il tag $(Tag) indicherebbe uno stato che non e' ricostruibile.%0ACommitta prima di rilasciare, oppure rilancia con -p:ConsentiModifiche=true se sai cosa stai facendo.%0A%0A$(AlberoSporco)" />
|
||||||
|
|
||||||
|
<Exec Command="git tag -a $(Tag) -m "AutoBidder $(V)"" WorkingDirectory="$(Radice)"
|
||||||
|
StandardOutputImportance="low" StandardErrorImportance="low" />
|
||||||
|
|
||||||
|
<Message Importance="High" Text=" tag $(Tag) creato" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!-- ═════════════════════ Rilascio ═════════════════════ -->
|
||||||
|
|
||||||
|
<!-- Prima di tutto il resto: un token mancante non deve costare due minuti di
|
||||||
|
compilazione per poi fermarsi all'ultimo passo. -->
|
||||||
|
<Target Name="ConfigGitea">
|
||||||
|
<LeggiConfigGitea Percorso="$(MSBuildThisFileDirectory)gitea.json">
|
||||||
|
<Output TaskParameter="Url" PropertyName="GUrl" />
|
||||||
|
<Output TaskParameter="Owner" PropertyName="GOwner" />
|
||||||
|
<Output TaskParameter="Repo" PropertyName="GRepo" />
|
||||||
|
<Output TaskParameter="Token" PropertyName="GToken" />
|
||||||
|
</LeggiConfigGitea>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="Rilascia" DependsOnTargets="ConfigGitea;Pacchetto">
|
||||||
|
<Message Importance="High" Text="== Pubblicazione su Gitea ==" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Api>$(GUrl)/api/v1/repos/$(GOwner)/$(GRepo)</Api>
|
||||||
|
<Tmp>$([System.IO.Path]::GetTempPath())AutoBidder.Rilascio</Tmp>
|
||||||
|
<CurlCfg>$(Tmp)\curl.cfg</CurlCfg>
|
||||||
|
<CorpoJson>$(Tmp)\release.json</CorpoJson>
|
||||||
|
<RispostaJson>$(Tmp)\risposta.json</RispostaJson>
|
||||||
|
<Setup>$(CartellaPacchetti)\AutoBidder_$(V).exe</Setup>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<MakeDir Directories="$(Tmp)" />
|
||||||
|
|
||||||
|
<!-- Il token vive qui e solo qui, per il tempo del rilascio. -->
|
||||||
|
<WriteLinesToFile File="$(CurlCfg)" Overwrite="true"
|
||||||
|
Lines="header = "Authorization: token $(GToken)"" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Il tag esiste gia' in locale: l'ha creato Pacchetto, o c'era prima. Qui va
|
||||||
|
spinto, e va verificato che ci sia arrivato.
|
||||||
|
|
||||||
|
Non e' una formalita'. Il push qui sotto spinge i tag, non i commit: se il
|
||||||
|
ramo e' indietro, il tag punta a un oggetto che il remoto non conosce e il
|
||||||
|
push viene rifiutato. A quel punto la creazione della release non
|
||||||
|
fallisce — Gitea, non trovando il tag, lo crea da se' sulla testa del ramo
|
||||||
|
predefinito. Verrebbe pubblicata una release che dichiara di essere il
|
||||||
|
commit X mentre il codice allegato e' il commit Y, e nessuno se ne
|
||||||
|
accorgerebbe. Meglio fermarsi e dire cosa manca.
|
||||||
|
|
||||||
|
I commit non si spingono da qui: quando spingere il ramo lo decide chi
|
||||||
|
lavora, non la catena di rilascio.
|
||||||
|
-->
|
||||||
|
<Exec Command="git push --tags" WorkingDirectory="$(Radice)" ContinueOnError="true"
|
||||||
|
StandardOutputImportance="low" StandardErrorImportance="low" />
|
||||||
|
|
||||||
|
<Exec Command="git ls-remote --tags origin refs/tags/$(Tag)" WorkingDirectory="$(Radice)"
|
||||||
|
ConsoleToMSBuild="true" ContinueOnError="true" StandardOutputImportance="low">
|
||||||
|
<Output TaskParameter="ConsoleOutput" PropertyName="TagSulRemoto" />
|
||||||
|
</Exec>
|
||||||
|
|
||||||
|
<Error Condition="'$(TagSulRemoto)' == ''"
|
||||||
|
Text="Il tag $(Tag) non e' arrivato sul remoto, quasi sempre perche' il ramo e' indietro.%0AGitea creerebbe la release sulla testa del ramo predefinito: il codice allegato non corrisponderebbe al commit dichiarato.%0A%0ASpingi il ramo e rilancia: git push" />
|
||||||
|
|
||||||
|
<Message Importance="High" Text=" tag $(Tag) sul remoto" />
|
||||||
|
|
||||||
|
<!-- Release gia' presente? -->
|
||||||
|
<Exec EchoOff="true" ContinueOnError="true" StandardOutputImportance="low"
|
||||||
|
Command="curl -s -K "$(CurlCfg)" -o "$(RispostaJson)" "$(Api)/releases/tags/$(Tag)"" />
|
||||||
|
|
||||||
|
<LeggiIdRelease Risposta="$(RispostaJson)">
|
||||||
|
<Output TaskParameter="Id" PropertyName="IdEsistente" />
|
||||||
|
</LeggiIdRelease>
|
||||||
|
|
||||||
|
<Error Condition="'$(IdEsistente)' != '' AND '$(Sovrascrivi)' != 'true'"
|
||||||
|
Text="La release $(Tag) esiste gia'. Alza la versione, oppure rilancia con -p:Sovrascrivi=true." />
|
||||||
|
|
||||||
|
<Exec Condition="'$(IdEsistente)' != ''" EchoOff="true" StandardOutputImportance="low"
|
||||||
|
Command="curl -s -K "$(CurlCfg)" -X DELETE "$(Api)/releases/$(IdEsistente)"" />
|
||||||
|
<Message Condition="'$(IdEsistente)' != ''" Importance="High"
|
||||||
|
Text=" release $(Tag) esistente: sostituita" />
|
||||||
|
|
||||||
|
<!-- Creazione -->
|
||||||
|
<PreparaCorpoRelease Destinazione="$(CorpoJson)" Tag="$(Tag)" Versione="$(V)"
|
||||||
|
Note="$(Note)" Bozza="$(Bozza)" />
|
||||||
|
|
||||||
|
<Exec EchoOff="true" StandardOutputImportance="low"
|
||||||
|
Command="curl -s -K "$(CurlCfg)" -X POST -H "Content-Type: application/json" --data-binary "@$(CorpoJson)" -o "$(RispostaJson)" "$(Api)/releases"" />
|
||||||
|
|
||||||
|
<LeggiIdRelease Risposta="$(RispostaJson)">
|
||||||
|
<Output TaskParameter="Id" PropertyName="IdRelease" />
|
||||||
|
<Output TaskParameter="Errore" PropertyName="ErroreRelease" />
|
||||||
|
</LeggiIdRelease>
|
||||||
|
|
||||||
|
<Error Condition="'$(IdRelease)' == ''"
|
||||||
|
Text="Creazione della release non riuscita: $(ErroreRelease)" />
|
||||||
|
|
||||||
|
<Message Importance="High" Text=" release creata" />
|
||||||
|
|
||||||
|
<!-- Allegati: sia l'installatore sia l'eseguibile nudo. Chi non vuole
|
||||||
|
installare niente deve continuare a poter scaricare il solo .exe. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Allegato Include="$(Setup)" />
|
||||||
|
<Allegato Include="$(CartellaPubblicazione)\AutoBidder.exe" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Exec Condition="Exists('%(Allegato.FullPath)')" EchoOff="true" StandardOutputImportance="low"
|
||||||
|
Command="curl -s -K "$(CurlCfg)" -X POST -F "attachment=@%(Allegato.FullPath)" "$(Api)/releases/$(IdRelease)/assets?name=%(Allegato.Filename)%(Allegato.Extension)"" />
|
||||||
|
|
||||||
|
<Message Importance="High" Text=" caricato %(Allegato.Filename)%(Allegato.Extension)"
|
||||||
|
Condition="Exists('%(Allegato.FullPath)')" />
|
||||||
|
|
||||||
|
<!-- Il token non deve sopravvivere al rilascio. -->
|
||||||
|
<Delete Files="$(CurlCfg);$(CorpoJson);$(RispostaJson)" ContinueOnError="true" />
|
||||||
|
|
||||||
|
<Message Importance="High" Text=" " />
|
||||||
|
<Message Importance="High" Text="Rilascio completato:" />
|
||||||
|
<Message Importance="High" Text=" $(GUrl)/$(GOwner)/$(GRepo)/releases/tag/$(Tag)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"_commento": "Copia questo file in gitea.json e metti il token. gitea.json e' escluso dal controllo di versione perche' contiene una credenziale. In alternativa usa le variabili d'ambiente GITEA_URL, GITEA_OWNER, GITEA_REPO, GITEA_TOKEN, che hanno la precedenza su questo file.",
|
||||||
|
|
||||||
|
"url": "http://192.168.30.23:3000",
|
||||||
|
"owner": "Alby96",
|
||||||
|
"repo": "Mimante",
|
||||||
|
|
||||||
|
"_token": "Gitea > Impostazioni > Applicazioni > Genera nuovo token, permesso 'repository: read and write'.",
|
||||||
|
"token": "INSERISCI_QUI_IL_TOKEN"
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Rigioca i dossier delle aste concluse e stampa quanto costerebbe ciascun anticipo.
|
||||||
|
#
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\backtest.ps1
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\backtest.ps1 -Max 200
|
||||||
|
#
|
||||||
|
# Il motore punta solo nei cicli che arrivano fino all'anticipo senza che nessun altro
|
||||||
|
# abbia puntato: la rigiocata conta quei cicli, quindi dice quante puntate si
|
||||||
|
# spenderebbero e quante ne bloccherebbero le strategie.
|
||||||
|
#
|
||||||
|
# NON dice se avresti vinto: una nostra puntata rimette in gioco l'asta, e la reazione
|
||||||
|
# degli avversari non e' registrata da nessuna parte.
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
# Cartella dei dossier. Vuota = quella in uso dall'applicazione.
|
||||||
|
[string] $Cartella = "",
|
||||||
|
|
||||||
|
# Quanti dossier al massimo (0 = tutti). I file sono grossi: per un giro veloce, 200.
|
||||||
|
[int] $Max = 0,
|
||||||
|
|
||||||
|
# Dove scrivere il riepilogo.
|
||||||
|
[string] $Report = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Set-Location $PSScriptRoot
|
||||||
|
|
||||||
|
if (-not $Cartella) {
|
||||||
|
# Stessa struttura di Utilities/AppPaths: Dati\Registri\Aste sotto %LocalAppData%.
|
||||||
|
$Cartella = Join-Path $env:LOCALAPPDATA "AutoBidder\Dati\Registri\Aste"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $Cartella)) {
|
||||||
|
Write-Host "Cartella dei dossier non trovata: $Cartella" -ForegroundColor Red
|
||||||
|
Write-Host "Indicala con -Cartella, oppure accendi 'Dossier per ogni asta' nelle Impostazioni." -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$conteggio = (Get-ChildItem -Path $Cartella -Filter *.jsonl -File | Measure-Object).Count
|
||||||
|
if ($conteggio -eq 0) {
|
||||||
|
Write-Host "Nessun dossier (*.jsonl) in $Cartella" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Report) {
|
||||||
|
$Report = Join-Path $PSScriptRoot "backtest-report.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "== Rigiocata sui dossier ==" -ForegroundColor Cyan
|
||||||
|
Write-Host " cartella : $Cartella"
|
||||||
|
Write-Host " dossier : $conteggio$(if ($Max -gt 0) { " (ne verranno letti $Max)" })"
|
||||||
|
Write-Host " riepilogo: $Report"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$env:AUTOBIDDER_BACKTEST_DIR = $Cartella
|
||||||
|
$env:AUTOBIDDER_BACKTEST_MAX = "$Max"
|
||||||
|
$env:AUTOBIDDER_BACKTEST_OUT = $Report
|
||||||
|
|
||||||
|
# L'applicazione potrebbe essere aperta e tenere bloccato AutoBidder.exe: si compila
|
||||||
|
# in una cartella a parte, come fa la verifica.
|
||||||
|
$out = Join-Path ([System.IO.Path]::GetTempPath()) "AutoBidder.Backtest"
|
||||||
|
|
||||||
|
& dotnet test Tests\AutoBidder.Tests.csproj --nologo -v q -o $out `
|
||||||
|
--filter "FullyQualifiedName~RealDossierBacktest" `
|
||||||
|
--logger "console;verbosity=detailed" 2>&1 |
|
||||||
|
Where-Object { $_ -notmatch '^\s*$' } |
|
||||||
|
ForEach-Object { Write-Host $_ }
|
||||||
|
|
||||||
|
$esito = $LASTEXITCODE
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
if (Test-Path $Report) {
|
||||||
|
Write-Host "== Riepilogo ==" -ForegroundColor Cyan
|
||||||
|
Get-Content $Report | ForEach-Object { Write-Host $_ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($esito -ne 0) {
|
||||||
|
Write-Host "`nLa rigiocata ha trovato dei problemi (vedi sopra)." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nRigiocata completata." -ForegroundColor Green
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<#
|
||||||
|
pubblica.ps1 - Crea AutoBidder come UNICO file .exe self-contained per Windows x64.
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\pubblica.ps1
|
||||||
|
|
||||||
|
Risultato:
|
||||||
|
bin\publish\win-x64\AutoBidder.exe (doppio click per avviare, nessuna installazione)
|
||||||
|
#>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
Set-Location $here
|
||||||
|
|
||||||
|
Write-Host "==> Pubblicazione AutoBidder (self-contained, single-file, win-x64)..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
dotnet publish "AutoBidder.csproj" -c Release -r win-x64 `
|
||||||
|
--self-contained true `
|
||||||
|
-p:PublishSingleFile=true `
|
||||||
|
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||||
|
-p:EnableCompressionInSingleFile=true `
|
||||||
|
-p:PublishReadyToRun=true `
|
||||||
|
-p:PublishTrimmed=false `
|
||||||
|
-o "bin\publish\win-x64"
|
||||||
|
|
||||||
|
$exe = Join-Path $here "bin\publish\win-x64\AutoBidder.exe"
|
||||||
|
if (Test-Path $exe) {
|
||||||
|
$sizeMB = [math]::Round((Get-Item $exe).Length / 1MB, 1)
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "==> FATTO. Eseguibile pronto:" -ForegroundColor Green
|
||||||
|
Write-Host " $exe ($sizeMB MB)" -ForegroundColor Green
|
||||||
|
Write-Host " Fai doppio click per avviare. Nessun software da installare." -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "==> ERRORE: eseguibile non trovato." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
<#
|
||||||
|
rilascia.ps1 — dalla sorgente al pacchetto pubblicato su Gitea, in un comando.
|
||||||
|
|
||||||
|
Passi:
|
||||||
|
1. verifica.ps1 (compila + test) — saltabile con -SaltaVerifica
|
||||||
|
2. pubblica l'eseguibile self-contained
|
||||||
|
3. compila l'installatore con Inno Setup
|
||||||
|
4. crea il tag, la release su Gitea e carica i file
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\rilascia.ps1
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\rilascia.ps1 -SoloPacchetto
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\rilascia.ps1 -Note "Cosa cambia"
|
||||||
|
|
||||||
|
Configurazione di Gitea (una volta sola): copia gitea.example.json in
|
||||||
|
gitea.json e riempilo. Il file è escluso dal controllo di versione perché
|
||||||
|
contiene un token. In alternativa, le variabili d'ambiente
|
||||||
|
GITEA_URL / GITEA_OWNER / GITEA_REPO / GITEA_TOKEN hanno la precedenza.
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
# Ferma dopo aver creato l'installatore, senza toccare Gitea.
|
||||||
|
[switch] $SoloPacchetto,
|
||||||
|
|
||||||
|
# Salta compilazione e test. Da usare solo se li hai appena eseguiti.
|
||||||
|
[switch] $SaltaVerifica,
|
||||||
|
|
||||||
|
# Testo delle note di rilascio. Vuoto = generate dai commit dall'ultimo tag.
|
||||||
|
[string] $Note = "",
|
||||||
|
|
||||||
|
# Contrassegna la release come bozza o come anteprima.
|
||||||
|
[switch] $Bozza,
|
||||||
|
[switch] $Anteprima,
|
||||||
|
|
||||||
|
# Sovrascrive una release con lo stesso tag invece di fermarsi.
|
||||||
|
[switch] $Sovrascrivi
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Set-Location $PSScriptRoot
|
||||||
|
|
||||||
|
function Titolo($t) { Write-Host "`n== $t ==" -ForegroundColor Cyan }
|
||||||
|
function Ok($t) { Write-Host " $t" -ForegroundColor Green }
|
||||||
|
function Nota($t) { Write-Host " $t" -ForegroundColor DarkGray }
|
||||||
|
function Errore($t) { Write-Host "`n$t" -ForegroundColor Red }
|
||||||
|
|
||||||
|
# ── 0. Versione ──────────────────────────────────────────────────────────────
|
||||||
|
# La versione ha una sola origine: il .csproj. Ripeterla qui vorrebbe dire
|
||||||
|
# vederle divergere al primo rilascio fatto di fretta.
|
||||||
|
[xml] $csproj = Get-Content .\AutoBidder.csproj
|
||||||
|
$versione = ($csproj.Project.PropertyGroup | Where-Object { $_.Version } | Select-Object -First 1).Version
|
||||||
|
|
||||||
|
if (-not $versione) {
|
||||||
|
Errore "Versione non trovata in AutoBidder.csproj (<Version>)."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = "v$versione"
|
||||||
|
Titolo "AutoBidder $versione"
|
||||||
|
|
||||||
|
# ── 1. Verifica ──────────────────────────────────────────────────────────────
|
||||||
|
if ($SaltaVerifica) {
|
||||||
|
Nota "verifica saltata su richiesta"
|
||||||
|
} else {
|
||||||
|
Titolo "Verifica (compilazione + test)"
|
||||||
|
& powershell -ExecutionPolicy Bypass -File .\verifica.ps1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Errore "Verifica fallita: niente da rilasciare."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 2. Eseguibile ────────────────────────────────────────────────────────────
|
||||||
|
Titolo "Pubblicazione dell'eseguibile"
|
||||||
|
|
||||||
|
$pubDir = Join-Path $PSScriptRoot "bin\publish\win-x64"
|
||||||
|
$exe = Join-Path $pubDir "AutoBidder.exe"
|
||||||
|
|
||||||
|
dotnet publish .\AutoBidder.csproj -c Release -r win-x64 --nologo -v q `
|
||||||
|
--self-contained true `
|
||||||
|
-p:PublishSingleFile=true `
|
||||||
|
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||||
|
-p:EnableCompressionInSingleFile=true `
|
||||||
|
-p:PublishReadyToRun=true `
|
||||||
|
-p:PublishTrimmed=false `
|
||||||
|
-o $pubDir
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $exe)) {
|
||||||
|
Errore "Pubblicazione fallita."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok ("AutoBidder.exe ({0:N1} MB)" -f ((Get-Item $exe).Length / 1MB))
|
||||||
|
|
||||||
|
# La versione scritta nell'eseguibile deve corrispondere: se qualcuno cambia il
|
||||||
|
# .csproj senza ricompilare, il pacchetto porterebbe un numero e il programma un altro.
|
||||||
|
$versioneExe = (Get-Item $exe).VersionInfo.ProductVersion
|
||||||
|
if ($versioneExe -and -not $versioneExe.StartsWith($versione)) {
|
||||||
|
Errore "L'eseguibile dichiara $versioneExe ma il progetto dice $versione."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 3. Installatore ──────────────────────────────────────────────────────────
|
||||||
|
Titolo "Creazione dell'installatore"
|
||||||
|
|
||||||
|
$iscc = @(
|
||||||
|
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe",
|
||||||
|
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
|
||||||
|
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
|
||||||
|
) | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $iscc) {
|
||||||
|
Errore "Inno Setup 6 non trovato."
|
||||||
|
Write-Host "Installalo con: winget install -e --id JRSoftware.InnoSetup" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$outDir = Join-Path $PSScriptRoot "bin\installer"
|
||||||
|
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||||||
|
|
||||||
|
& $iscc /Qp `
|
||||||
|
"/DAppVersion=$versione" `
|
||||||
|
"/DSourceExe=$exe" `
|
||||||
|
"/DOutputDir=$outDir" `
|
||||||
|
".\installer\AutoBidder.iss"
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Errore "Inno Setup ha restituito un errore."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$setup = Join-Path $outDir "AutoBidder-$versione-setup.exe"
|
||||||
|
if (-not (Test-Path $setup)) {
|
||||||
|
Errore "Installatore non trovato: $setup"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok ("{0} ({1:N1} MB)" -f (Split-Path $setup -Leaf), ((Get-Item $setup).Length / 1MB))
|
||||||
|
|
||||||
|
if ($SoloPacchetto) {
|
||||||
|
Write-Host "`nPacchetto pronto in $outDir" -ForegroundColor Green
|
||||||
|
Write-Host "Gitea non è stato toccato (-SoloPacchetto)." -ForegroundColor DarkGray
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 4. Gitea ─────────────────────────────────────────────────────────────────
|
||||||
|
Titolo "Pubblicazione su Gitea"
|
||||||
|
|
||||||
|
$cfgFile = Join-Path $PSScriptRoot "gitea.json"
|
||||||
|
$cfg = if (Test-Path $cfgFile) { Get-Content $cfgFile -Raw | ConvertFrom-Json } else { $null }
|
||||||
|
|
||||||
|
function Impostazione($nomeEnv, $campo) {
|
||||||
|
$v = [Environment]::GetEnvironmentVariable($nomeEnv)
|
||||||
|
if ($v) { return $v }
|
||||||
|
if ($cfg -and $cfg.PSObject.Properties.Name -contains $campo) { return $cfg.$campo }
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = Impostazione 'GITEA_URL' 'url'
|
||||||
|
$owner = Impostazione 'GITEA_OWNER' 'owner'
|
||||||
|
$repo = Impostazione 'GITEA_REPO' 'repo'
|
||||||
|
$token = Impostazione 'GITEA_TOKEN' 'token'
|
||||||
|
|
||||||
|
if (-not $url -or -not $owner -or -not $repo -or -not $token) {
|
||||||
|
Errore "Configurazione di Gitea incompleta."
|
||||||
|
Write-Host @"
|
||||||
|
Servono quattro valori: url, owner, repo, token.
|
||||||
|
|
||||||
|
1. copia gitea.example.json in gitea.json e riempilo, oppure
|
||||||
|
2. imposta GITEA_URL, GITEA_OWNER, GITEA_REPO, GITEA_TOKEN.
|
||||||
|
|
||||||
|
Il token si crea in Gitea da Impostazioni > Applicazioni > Genera nuovo token,
|
||||||
|
con il permesso 'repository: read and write'.
|
||||||
|
|
||||||
|
L'installatore è comunque pronto in:
|
||||||
|
$setup
|
||||||
|
"@ -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = $url.TrimEnd('/')
|
||||||
|
$api = "$url/api/v1/repos/$owner/$repo"
|
||||||
|
$headers = @{ Authorization = "token $token" }
|
||||||
|
|
||||||
|
Nota "$url/$owner/$repo tag $tag"
|
||||||
|
|
||||||
|
# Note di rilascio: se non le detta l'utente, si prendono dai commit.
|
||||||
|
if (-not $Note) {
|
||||||
|
$ultimo = & git describe --tags --abbrev=0 2>$null
|
||||||
|
$intervallo = if ($LASTEXITCODE -eq 0 -and $ultimo) { "$ultimo..HEAD" } else { "HEAD" }
|
||||||
|
$commit = & git log $intervallo --pretty=format:"- %s" --no-merges 2>$null
|
||||||
|
|
||||||
|
$Note = if ($commit) { ($commit | Out-String).Trim() } else { "Versione $versione." }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Il tag deve esistere sul server: la release ci si appoggia.
|
||||||
|
$rami = & git rev-parse --git-dir 2>$null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
$esiste = & git tag -l $tag
|
||||||
|
if (-not $esiste) {
|
||||||
|
& git tag -a $tag -m "AutoBidder $versione"
|
||||||
|
Ok "tag $tag creato"
|
||||||
|
}
|
||||||
|
|
||||||
|
$remoto = & git remote 2>$null | Select-Object -First 1
|
||||||
|
if ($remoto) {
|
||||||
|
& git push $remoto $tag 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) { Ok "tag inviato a $remoto" }
|
||||||
|
else { Nota "tag non inviato (probabilmente c'era già)" }
|
||||||
|
} else {
|
||||||
|
Nota "nessun remoto git configurato: la release userà il tag come nome"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Release già presente?
|
||||||
|
$esistente = $null
|
||||||
|
try {
|
||||||
|
$esistente = Invoke-RestMethod -Uri "$api/releases/tags/$tag" -Headers $headers -Method Get
|
||||||
|
} catch {
|
||||||
|
# 404 = non c'è, ed è il caso normale.
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($esistente) {
|
||||||
|
if (-not $Sovrascrivi) {
|
||||||
|
Errore "La release $tag esiste già su Gitea."
|
||||||
|
Write-Host "Alza <Version> in AutoBidder.csproj, oppure rilancia con -Sovrascrivi." -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Nota "release $tag esistente: verrà sostituita"
|
||||||
|
Invoke-RestMethod -Uri "$api/releases/$($esistente.id)" -Headers $headers -Method Delete | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$corpo = @{
|
||||||
|
tag_name = $tag
|
||||||
|
name = "AutoBidder $versione"
|
||||||
|
body = $Note
|
||||||
|
draft = [bool]$Bozza
|
||||||
|
prerelease = [bool]$Anteprima
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
try {
|
||||||
|
$release = Invoke-RestMethod -Uri "$api/releases" -Headers $headers -Method Post `
|
||||||
|
-ContentType 'application/json' -Body $corpo
|
||||||
|
} catch {
|
||||||
|
Errore "Creazione della release non riuscita: $($_.Exception.Message)"
|
||||||
|
if ($_.ErrorDetails.Message) { Write-Host $_.ErrorDetails.Message -ForegroundColor DarkGray }
|
||||||
|
Write-Host "`nL'installatore resta disponibile in:`n $setup" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok "release creata"
|
||||||
|
|
||||||
|
# ── 5. File allegati ─────────────────────────────────────────────────────────
|
||||||
|
# Si carica sia l'installatore sia l'eseguibile nudo: chi non vuole installare
|
||||||
|
# nulla continua a poter scaricare il solo .exe, che è come l'app è nata.
|
||||||
|
function Carica($percorso) {
|
||||||
|
$nome = Split-Path $percorso -Leaf
|
||||||
|
$uri = "$api/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($nome))"
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Uri $uri -Headers $headers -Method Post -Form @{
|
||||||
|
attachment = Get-Item $percorso
|
||||||
|
} | Out-Null
|
||||||
|
Ok ("caricato {0} ({1:N1} MB)" -f $nome, ((Get-Item $percorso).Length / 1MB))
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Errore "Caricamento di $nome non riuscito: $($_.Exception.Message)"
|
||||||
|
if ($_.ErrorDetails.Message) { Write-Host $_.ErrorDetails.Message -ForegroundColor DarkGray }
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tuttoOk = (Carica $setup) -and (Carica $exe)
|
||||||
|
|
||||||
|
if (-not $tuttoOk) {
|
||||||
|
Errore "Release creata ma senza tutti i file. Caricali a mano da $url/$owner/$repo/releases"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nRilascio completato." -ForegroundColor Green
|
||||||
|
Write-Host " $url/$owner/$repo/releases/tag/$tag" -ForegroundColor Green
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Verifica rapida: compila tutto e lancia i test.
|
||||||
|
# Da eseguire dopo ogni modifica, prima di avviare l'applicazione.
|
||||||
|
#
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\verifica.ps1
|
||||||
|
#
|
||||||
|
# Restituisce 0 se tutto è a posto, 1 altrimenti: utilizzabile in un hook.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
Set-Location $PSScriptRoot
|
||||||
|
|
||||||
|
Write-Host "== Compilazione ==" -ForegroundColor Cyan
|
||||||
|
$build = & dotnet build AutoBidder.sln --nologo -v q 2>&1
|
||||||
|
$buildOk = $LASTEXITCODE -eq 0
|
||||||
|
|
||||||
|
$problemi = $build | Select-String -Pattern 'error|warning CS'
|
||||||
|
if ($problemi) { $problemi | ForEach-Object { Write-Host $_.Line -ForegroundColor Yellow } }
|
||||||
|
|
||||||
|
if (-not $buildOk) {
|
||||||
|
Write-Host "COMPILAZIONE FALLITA" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "compilazione ok, 0 errori" -ForegroundColor Green
|
||||||
|
|
||||||
|
Write-Host "`n== Test ==" -ForegroundColor Cyan
|
||||||
|
$test = & dotnet test Tests\AutoBidder.Tests.csproj --nologo -v q --no-build 2>&1
|
||||||
|
$testOk = $LASTEXITCODE -eq 0
|
||||||
|
|
||||||
|
$riga = $test | Select-String -Pattern 'Superato!|Non superato|Failed!|Passed!'
|
||||||
|
if ($riga) { $riga | ForEach-Object { Write-Host $_.Line } }
|
||||||
|
|
||||||
|
if (-not $testOk) {
|
||||||
|
# In caso di rosso servono i dettagli, non il riassunto.
|
||||||
|
$test | Select-String -Pattern 'Assert|Error Message|Stack Trace|\[FAIL\]' |
|
||||||
|
Select-Object -First 30 | ForEach-Object { Write-Host $_.Line -ForegroundColor Red }
|
||||||
|
Write-Host "`nTEST FALLITI" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nTutto a posto." -ForegroundColor Green
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user