0.1.3-prod
reduce timeouts added tests and mock interface
This commit is contained in:
154
main.go
154
main.go
@@ -18,22 +18,22 @@ import (
|
|||||||
const (
|
const (
|
||||||
configFileName = "connect.json"
|
configFileName = "connect.json"
|
||||||
serviceConfigName = "service.json"
|
serviceConfigName = "service.json"
|
||||||
outputDir = "date"
|
|
||||||
logsDir = "logs"
|
logsDir = "logs"
|
||||||
comSearchTimeout = 200 * time.Millisecond // Уменьшенный таймаут
|
comSearchTimeout = 200 * time.Millisecond
|
||||||
tcpSearchTimeout = 150 * time.Millisecond
|
tcpSearchTimeout = 200 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Глобальная переменная для пути вывода. Это позволяет подменять ее в тестах.
|
||||||
|
var outputDir = "date"
|
||||||
|
|
||||||
// --- СТРУКТУРЫ ДЛЯ ПАРСИНГА КОНФИГУРАЦИОННЫХ ФАЙЛОВ ---
|
// --- СТРУКТУРЫ ДЛЯ ПАРСИНГА КОНФИГУРАЦИОННЫХ ФАЙЛОВ ---
|
||||||
|
|
||||||
// ConfigFile соответствует структуре файла connect.json
|
|
||||||
type ConfigFile struct {
|
type ConfigFile struct {
|
||||||
Timeout int `json:"timeout_to_ip_port"`
|
Timeout int `json:"timeout_to_ip_port"`
|
||||||
Shtrih []ConnectionSettings `json:"shtrih"`
|
Shtrih []ConnectionSettings `json:"shtrih"`
|
||||||
Atol []interface{} `json:"atol"`
|
Atol []interface{} `json:"atol"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionSettings описывает один блок настроек подключения для Штрих-М
|
|
||||||
type ConnectionSettings struct {
|
type ConnectionSettings struct {
|
||||||
TypeConnect int32 `json:"type_connect"`
|
TypeConnect int32 `json:"type_connect"`
|
||||||
ComPort string `json:"com_port"`
|
ComPort string `json:"com_port"`
|
||||||
@@ -42,19 +42,15 @@ type ConnectionSettings struct {
|
|||||||
IPPort string `json:"ip_port"`
|
IPPort string `json:"ip_port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceFile используется для чтения настроек логирования из service.json
|
|
||||||
type ServiceFile struct {
|
type ServiceFile struct {
|
||||||
Service ServiceConfig `json:"service"`
|
Service ServiceConfig `json:"service"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceConfig содержит параметры логирования
|
|
||||||
type ServiceConfig struct {
|
type ServiceConfig struct {
|
||||||
LogLevel string `json:"log_level"`
|
LogLevel string `json:"log_level"`
|
||||||
LogDays int `json:"log_days"`
|
LogDays int `json:"log_days"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PolledDevice связывает конфигурацию, использованную для подключения,
|
|
||||||
// с фискальной информацией, полученной от устройства.
|
|
||||||
type PolledDevice struct {
|
type PolledDevice struct {
|
||||||
Config shtrih.Config
|
Config shtrih.Config
|
||||||
Info *shtrih.FiscalInfo
|
Info *shtrih.FiscalInfo
|
||||||
@@ -81,7 +77,6 @@ func main() {
|
|||||||
log.Println("Работа приложения завершена.")
|
log.Println("Работа приложения завершена.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupLogger настраивает запись логов в файл для стационарного режима.
|
|
||||||
func setupLogger() {
|
func setupLogger() {
|
||||||
data, err := os.ReadFile(serviceConfigName)
|
data, err := os.ReadFile(serviceConfigName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -95,13 +90,11 @@ func setupLogger() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем значения по умолчанию, если в файле их нет
|
|
||||||
logDays := serviceFile.Service.LogDays
|
logDays := serviceFile.Service.LogDays
|
||||||
if logDays <= 0 {
|
if logDays <= 0 {
|
||||||
logDays = 7
|
logDays = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем папку для логов, если ее нет
|
|
||||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||||
log.Printf("Ошибка создания директории для логов '%s': %v. Логирование продолжится в консоль.", logsDir, err)
|
log.Printf("Ошибка создания директории для логов '%s': %v. Логирование продолжится в консоль.", logsDir, err)
|
||||||
return
|
return
|
||||||
@@ -109,28 +102,25 @@ func setupLogger() {
|
|||||||
|
|
||||||
logFilePath := filepath.Join(logsDir, "shtrih-scanner.log")
|
logFilePath := filepath.Join(logsDir, "shtrih-scanner.log")
|
||||||
|
|
||||||
// Настраиваем ротацию логов
|
|
||||||
lumberjackLogger := &lumberjack.Logger{
|
lumberjackLogger := &lumberjack.Logger{
|
||||||
Filename: logFilePath,
|
Filename: logFilePath,
|
||||||
MaxSize: 5, // мегабайты
|
MaxSize: 5,
|
||||||
MaxBackups: 10,
|
MaxBackups: 10,
|
||||||
MaxAge: logDays, // дни
|
MaxAge: logDays,
|
||||||
Compress: true, // сжимать старые файлы
|
Compress: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем вывод логов и в файл, и в консоль
|
|
||||||
log.SetOutput(io.MultiWriter(os.Stdout, lumberjackLogger))
|
log.SetOutput(io.MultiWriter(os.Stdout, lumberjackLogger))
|
||||||
log.Printf("Логирование настроено. Уровень: %s, ротация: %d дней. Файл: %s", serviceFile.Service.LogLevel, logDays, logFilePath)
|
log.Printf("Логирование настроено. Уровень: %s, ротация: %d дней. Файл: %s", serviceFile.Service.LogLevel, logDays, logFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runConfigMode(data []byte) {
|
func runConfigMode(data []byte) {
|
||||||
// Первым делом настраиваем логирование для стационарного режима!
|
|
||||||
setupLogger()
|
setupLogger()
|
||||||
|
|
||||||
var configFile ConfigFile
|
var configFile ConfigFile
|
||||||
if err := json.Unmarshal(data, &configFile); err != nil {
|
if err := json.Unmarshal(data, &configFile); err != nil {
|
||||||
log.Printf("Ошибка парсинга JSON из '%s': %v. Переключаюсь на режим автопоиска.", configFileName, err)
|
log.Printf("Ошибка парсинга JSON из '%s': %v. Переключаюсь на режим автопоиска.", configFileName, err)
|
||||||
runDiscoveryMode() // В случае ошибки автопоиск будет логировать только в консоль
|
runDiscoveryMode()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +137,8 @@ func runConfigMode(data []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
processDevices(configs)
|
// Передаем конструктор реального драйвера shtrih.New
|
||||||
|
processDevices(configs, shtrih.New)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDiscoveryMode() {
|
func runDiscoveryMode() {
|
||||||
@@ -162,18 +153,22 @@ func runDiscoveryMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Найдено %d устройств. Начинаю сбор информации...", len(configs))
|
log.Printf("Найдено %d устройств. Начинаю сбор информации...", len(configs))
|
||||||
polledDevices := processDevices(configs)
|
// Передаем конструктор реального драйвера shtrih.New
|
||||||
|
polledDevices := processDevices(configs, shtrih.New)
|
||||||
|
|
||||||
if len(polledDevices) > 0 {
|
if len(polledDevices) > 0 {
|
||||||
saveConfiguration(polledDevices)
|
saveConfiguration(polledDevices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func processDevices(configs []shtrih.Config) []PolledDevice {
|
// processDevices принимает функцию-фабрику `newDriverFunc` для создания драйвера.
|
||||||
|
// Это позволяет подменять реальный драйвер на мок-драйвер в тестах.
|
||||||
|
func processDevices(configs []shtrih.Config, newDriverFunc func(shtrih.Config) shtrih.Driver) []PolledDevice {
|
||||||
var polledDevices []PolledDevice
|
var polledDevices []PolledDevice
|
||||||
for _, config := range configs {
|
for _, config := range configs {
|
||||||
log.Printf("--- Опрашиваю устройство: %+v ---", config)
|
log.Printf("--- Опрашиваю устройство: %+v ---", config)
|
||||||
driver := shtrih.New(config)
|
// Используем переданную функцию-фабрику для создания драйвера
|
||||||
|
driver := newDriverFunc(config)
|
||||||
|
|
||||||
if err := driver.Connect(); err != nil {
|
if err := driver.Connect(); err != nil {
|
||||||
log.Printf("Не удалось подключиться к устройству: %v", err)
|
log.Printf("Не удалось подключиться к устройству: %v", err)
|
||||||
@@ -242,36 +237,67 @@ func processDevices(configs []shtrih.Config) []PolledDevice {
|
|||||||
|
|
||||||
// --- ФУНКЦИИ ДЛЯ РАБОТЫ С ФАЙЛАМИ ---
|
// --- ФУНКЦИИ ДЛЯ РАБОТЫ С ФАЙЛАМИ ---
|
||||||
|
|
||||||
|
// findSourceWorkstationData ищет в папке /date файл с данными о рабочей станции.
|
||||||
|
// Логика поиска:
|
||||||
|
// 1. Ищет "идеальный" донор: файл с "hostname", но без "modelName". Если находит - сразу возвращает его.
|
||||||
|
// 2. Если идеальный не найден, ищет "первый подходящий": любой файл с "hostname", даже если там есть "modelName".
|
||||||
func findSourceWorkstationData() map[string]interface{} {
|
func findSourceWorkstationData() map[string]interface{} {
|
||||||
files, err := os.ReadDir(outputDir)
|
files, err := os.ReadDir(outputDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var firstCandidate map[string]interface{} // Переменная для хранения "первого подходящего" кандидата
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".json" {
|
if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
|
||||||
filePath := filepath.Join(outputDir, file.Name())
|
continue
|
||||||
data, err := os.ReadFile(filePath)
|
}
|
||||||
if err != nil {
|
|
||||||
log.Printf("Предупреждение: не удалось прочитать файл-донор %s: %v", filePath, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var content map[string]interface{}
|
filePath := filepath.Join(outputDir, file.Name())
|
||||||
if err := json.Unmarshal(data, &content); err != nil {
|
data, err := os.ReadFile(filePath)
|
||||||
log.Printf("Предупреждение: не удалось распарсить JSON из файла-донора %s: %v", filePath, err)
|
if err != nil {
|
||||||
continue
|
log.Printf("Предупреждение: не удалось прочитать файл-донор %s: %v", filePath, err)
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
_, hasModelName := content["modelName"]
|
var content map[string]interface{}
|
||||||
_, hasHostname := content["hostname"]
|
if err := json.Unmarshal(data, &content); err != nil {
|
||||||
if !hasModelName && hasHostname {
|
log.Printf("Предупреждение: не удалось распарсить JSON из файла-донора %s: %v", filePath, err)
|
||||||
log.Printf("Найден файл-донор с данными о рабочей станции: %s", filePath)
|
continue
|
||||||
return content
|
}
|
||||||
}
|
|
||||||
|
// Проверяем наличие ключевых полей
|
||||||
|
_, hasModelName := content["modelName"]
|
||||||
|
_, hasHostname := content["hostname"]
|
||||||
|
|
||||||
|
// Если у файла нет hostname, он нам точно не интересен
|
||||||
|
if !hasHostname {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сценарий 1: Найден "идеальный" донор (без modelName)
|
||||||
|
if !hasModelName {
|
||||||
|
log.Printf("Найден идеальный файл-донор с данными о рабочей станции: %s", filePath)
|
||||||
|
return content // Сразу возвращаем его
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сценарий 2: Файл не идеальный, но подходит как кандидат (есть и hostname, и modelName)
|
||||||
|
// Сохраняем только самого первого кандидата из списка файлов.
|
||||||
|
if firstCandidate == nil {
|
||||||
|
firstCandidate = content
|
||||||
|
log.Printf("Найден файл-кандидат на роль донора (будет использован, если не найдется идеальный): %s", filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// После проверки всех файлов, если мы так и не вернули идеального донора,
|
||||||
|
// используем первого подходящего кандидата, которого нашли.
|
||||||
|
if firstCandidate != nil {
|
||||||
|
log.Println("Идеальный донор не найден, используется первый подходящий файл-кандидат.")
|
||||||
|
return firstCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если мы дошли до сюда, значит не было найдено ни одного файла с полем "hostname".
|
||||||
log.Println("В папке /date не найдено файлов-доноров. Будут использованы базовые данные.")
|
log.Println("В папке /date не найдено файлов-доноров. Будут использованы базовые данные.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -281,47 +307,57 @@ func updateTimestampInFile(filePath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ошибка чтения файла: %w", err)
|
return fmt.Errorf("ошибка чтения файла: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var content map[string]interface{}
|
var content map[string]interface{}
|
||||||
if err := json.Unmarshal(data, &content); err != nil {
|
if err := json.Unmarshal(data, &content); err != nil {
|
||||||
return fmt.Errorf("ошибка парсинга JSON: %w", err)
|
return fmt.Errorf("ошибка парсинга JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем оба поля времени
|
|
||||||
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
content["current_time"] = currentTime
|
content["current_time"] = currentTime
|
||||||
content["v_time"] = currentTime
|
content["v_time"] = currentTime
|
||||||
|
|
||||||
updatedData, err := json.MarshalIndent(content, "", " ")
|
updatedData, err := json.MarshalIndent(content, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ошибка маршалинга JSON: %w", err)
|
return fmt.Errorf("ошибка маршалинга JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(filePath, updatedData, 0644)
|
return os.WriteFile(filePath, updatedData, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saveNewMergedInfo объединяет данные ККТ и данные рабочей станции (в виде map) и сохраняет в новый JSON-файл.
|
||||||
|
// Данные от ККТ имеют приоритет и перезаписывают одноименные поля из данных донора.
|
||||||
func saveNewMergedInfo(kktInfo *shtrih.FiscalInfo, wsData map[string]interface{}, filePath string) error {
|
func saveNewMergedInfo(kktInfo *shtrih.FiscalInfo, wsData map[string]interface{}, filePath string) error {
|
||||||
|
// Шаг 1: Преобразуем данные от нашего ККТ (Штрих) в map.
|
||||||
var kktMap map[string]interface{}
|
var kktMap map[string]interface{}
|
||||||
kktJSON, _ := json.Marshal(kktInfo)
|
kktJSON, _ := json.Marshal(kktInfo)
|
||||||
json.Unmarshal(kktJSON, &kktMap)
|
json.Unmarshal(kktJSON, &kktMap)
|
||||||
|
|
||||||
// Добавляем актуальные временные метки в данные рабочей станции
|
// Шаг 2: Создаем итоговую карту. Начинаем с данных донора, чтобы они были "внизу".
|
||||||
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
// Мы делаем копию wsData, чтобы не изменять оригинальную карту, которая может быть использована в других итерациях.
|
||||||
wsData["current_time"] = currentTime
|
finalMap := make(map[string]interface{})
|
||||||
wsData["v_time"] = currentTime
|
|
||||||
|
|
||||||
for key, value := range wsData {
|
for key, value := range wsData {
|
||||||
kktMap[key] = value
|
finalMap[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(kktMap, "serialNumber")
|
// Шаг 3: "Накладываем" данные от нашего ККТ поверх.
|
||||||
kktMap["serialNumber"] = kktInfo.SerialNumber
|
// Все совпадающие ключи будут перезаписаны значениями от Штриха.
|
||||||
|
for key, value := range kktMap {
|
||||||
|
// Пропускаем пустые значения от ККТ, чтобы случайно не затереть
|
||||||
|
// хорошее значение из донора пустым.
|
||||||
|
if s, ok := value.(string); ok && s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
finalMap[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Шаг 4: Устанавливаем актуальные временные метки. Они всегда должны быть свежими.
|
||||||
|
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
finalMap["current_time"] = currentTime
|
||||||
|
finalMap["v_time"] = currentTime
|
||||||
|
|
||||||
|
// Шаг 5: Создаем директорию и сохраняем файл.
|
||||||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||||
return fmt.Errorf("не удалось создать директорию '%s': %w", outputDir, err)
|
return fmt.Errorf("не удалось создать директорию '%s': %w", outputDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalJSON, err := json.MarshalIndent(kktMap, "", " ")
|
finalJSON, err := json.MarshalIndent(finalMap, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ошибка маршалинга итогового JSON: %w", err)
|
return fmt.Errorf("ошибка маршалинга итогового JSON: %w", err)
|
||||||
}
|
}
|
||||||
@@ -344,25 +380,20 @@ func saveConfiguration(polledDevices []PolledDevice) {
|
|||||||
configFile = ConfigFile{}
|
configFile = ConfigFile{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var newShtrihSettings []ConnectionSettings
|
var newShtrihSettings []ConnectionSettings
|
||||||
for _, pd := range polledDevices {
|
for _, pd := range polledDevices {
|
||||||
newShtrihSettings = append(newShtrihSettings, convertConfigToSettings(pd.Config))
|
newShtrihSettings = append(newShtrihSettings, convertConfigToSettings(pd.Config))
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile.Shtrih = newShtrihSettings
|
configFile.Shtrih = newShtrihSettings
|
||||||
|
|
||||||
updatedData, err := json.MarshalIndent(configFile, "", " ")
|
updatedData, err := json.MarshalIndent(configFile, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Ошибка: не удалось преобразовать конфигурацию в JSON: %v", err)
|
log.Printf("Ошибка: не удалось преобразовать конфигурацию в JSON: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(configFileName, updatedData, 0644); err != nil {
|
if err := os.WriteFile(configFileName, updatedData, 0644); err != nil {
|
||||||
log.Printf("Ошибка: не удалось записать конфигурацию в файл '%s': %v", configFileName, err)
|
log.Printf("Ошибка: не удалось записать конфигурацию в файл '%s': %v", configFileName, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Конфигурация успешно сохранена в '%s'.", configFileName)
|
log.Printf("Конфигурация успешно сохранена в '%s'.", configFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +404,6 @@ func convertSettingsToConfigs(settings []ConnectionSettings) []shtrih.Config {
|
|||||||
baudRateMap := map[string]int32{
|
baudRateMap := map[string]int32{
|
||||||
"115200": 6, "57600": 5, "38400": 4, "19200": 3, "9600": 2, "4800": 1,
|
"115200": 6, "57600": 5, "38400": 4, "19200": 3, "9600": 2, "4800": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range settings {
|
for _, s := range settings {
|
||||||
config := shtrih.Config{ConnectionType: s.TypeConnect, Password: 30}
|
config := shtrih.Config{ConnectionType: s.TypeConnect, Password: 30}
|
||||||
switch s.TypeConnect {
|
switch s.TypeConnect {
|
||||||
@@ -412,9 +442,7 @@ func convertConfigToSettings(config shtrih.Config) ConnectionSettings {
|
|||||||
baudRateReverseMap := map[int32]string{
|
baudRateReverseMap := map[int32]string{
|
||||||
6: "115200", 5: "57600", 4: "38400", 3: "19200", 2: "9600", 1: "4800",
|
6: "115200", 5: "57600", 4: "38400", 3: "19200", 2: "9600", 1: "4800",
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := ConnectionSettings{TypeConnect: config.ConnectionType}
|
settings := ConnectionSettings{TypeConnect: config.ConnectionType}
|
||||||
|
|
||||||
switch config.ConnectionType {
|
switch config.ConnectionType {
|
||||||
case 0:
|
case 0:
|
||||||
settings.ComPort = config.ComName
|
settings.ComPort = config.ComName
|
||||||
|
|||||||
187
main_test.go
Normal file
187
main_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"shtrih-kkt/pkg/shtrih"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadCanonicalKKTData загружает эталонные данные ККТ из файла для тестов.
|
||||||
|
// Путь к файлу указывается относительно корня проекта.
|
||||||
|
func loadCanonicalKKTData(t *testing.T, path string) *shtrih.FiscalInfo {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Критическая ошибка: не удалось прочитать канонический файл данных '%s': %v", path, err)
|
||||||
|
}
|
||||||
|
var info shtrih.FiscalInfo
|
||||||
|
if err := json.Unmarshal(data, &info); err != nil {
|
||||||
|
t.Fatalf("Критическая ошибка: не удалось распарсить JSON из канонического файла '%s': %v", path, err)
|
||||||
|
}
|
||||||
|
return &info
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProcessDevices_NewFileFromDonor проверяет сценарий, когда для ККТ еще нет
|
||||||
|
// файла, но есть файл-донор с данными о рабочей станции.
|
||||||
|
func TestProcessDevices_NewFileFromDonor(t *testing.T) {
|
||||||
|
// --- Arrange (Подготовка) ---
|
||||||
|
|
||||||
|
// 1. Создаем временную директорию для теста, чтобы не засорять реальную папку `date`.
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalOutputDir := outputDir
|
||||||
|
outputDir = tempDir
|
||||||
|
defer func() { outputDir = originalOutputDir }()
|
||||||
|
|
||||||
|
// 2. Загружаем канонические данные для мок-драйвера из файла.
|
||||||
|
// Этот файл должен быть создан заранее и помещен в pkg/shtrih/testdata/
|
||||||
|
mockKKTData := loadCanonicalKKTData(t, "pkg/shtrih/testdata/canonical_kkt_data.json")
|
||||||
|
|
||||||
|
// 3. Создаем файл-донор с уникальными данными о рабочей станции во временной папке.
|
||||||
|
donorData := map[string]interface{}{
|
||||||
|
"hostname": "DONOR-PC",
|
||||||
|
"teamviewer_id": "999888777",
|
||||||
|
"vc": "3.0-donor-test",
|
||||||
|
}
|
||||||
|
donorBytes, _ := json.Marshal(donorData)
|
||||||
|
donorFilePath := filepath.Join(tempDir, "donor.json")
|
||||||
|
if err := os.WriteFile(donorFilePath, donorBytes, 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать тестовый файл-донор: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Создаем "фабрику", которая вернет мок-драйвер с нашими каноническими данными.
|
||||||
|
mockDriverFactory := func(config shtrih.Config) shtrih.Driver {
|
||||||
|
return shtrih.NewMockDriver(mockKKTData, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Готовим входные данные для processDevices.
|
||||||
|
testConfigs := []shtrih.Config{{ConnectionType: 6, IPAddress: "127.0.0.1"}}
|
||||||
|
|
||||||
|
// --- Act (Действие) ---
|
||||||
|
|
||||||
|
processDevices(testConfigs, mockDriverFactory)
|
||||||
|
|
||||||
|
// --- Assert (Проверка) ---
|
||||||
|
|
||||||
|
// 1. Проверяем, что был создан правильный JSON-файл.
|
||||||
|
expectedFileName := mockKKTData.SerialNumber + ".json"
|
||||||
|
resultFilePath := filepath.Join(tempDir, expectedFileName)
|
||||||
|
if _, err := os.Stat(resultFilePath); os.IsNotExist(err) {
|
||||||
|
t.Fatalf("Ожидалось, что будет создан файл '%s', но он не найден.", resultFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Читаем созданный файл и проверяем его содержимое.
|
||||||
|
resultBytes, err := os.ReadFile(resultFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Не удалось прочитать результирующий файл '%s': %v", resultFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(resultBytes, &resultMap); err != nil {
|
||||||
|
t.Fatalf("Не удалось распарсить JSON из результирующего файла: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Проверяем, что данные из ККТ и донора корректно слились.
|
||||||
|
// Проверка поля от ККТ.
|
||||||
|
if resultMap["modelName"] != mockKKTData.ModelName {
|
||||||
|
t.Errorf("Поле 'modelName' неверно. Ожидалось '%s', получено '%v'", mockKKTData.ModelName, resultMap["modelName"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка полей от донора.
|
||||||
|
if resultMap["hostname"] != donorData["hostname"] {
|
||||||
|
t.Errorf("Поле 'hostname' из донора не было добавлено. Ожидалось '%s', получено '%v'", donorData["hostname"], resultMap["hostname"])
|
||||||
|
}
|
||||||
|
if resultMap["vc"] != donorData["vc"] {
|
||||||
|
t.Errorf("Поле 'vc' из донора не было добавлено. Ожидалось '%s', получено '%v'", donorData["vc"], resultMap["vc"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка автоматически сгенерированных полей времени.
|
||||||
|
if _, ok := resultMap["current_time"]; !ok {
|
||||||
|
t.Error("Отсутствует обязательное поле 'current_time'.")
|
||||||
|
}
|
||||||
|
if _, ok := resultMap["v_time"]; !ok {
|
||||||
|
t.Error("Отсутствует обязательное поле 'v_time'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindSourceWorkstationData_FileHandling проверяет непосредственно логику
|
||||||
|
// поиска и чтения донор-файла в смоделированной файловой структуре.
|
||||||
|
func TestFindSourceWorkstationData_FileHandling(t *testing.T) {
|
||||||
|
|
||||||
|
// --- Сценарий 1: В папке /date есть правильный донор-файл ---
|
||||||
|
t.Run("when valid donor file exists", func(t *testing.T) {
|
||||||
|
// Arrange: Готовим файловую систему
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalOutputDir := outputDir
|
||||||
|
outputDir = tempDir // Указываем, что наша папка "date" находится во временной директории
|
||||||
|
defer func() { outputDir = originalOutputDir }()
|
||||||
|
|
||||||
|
// Создаем донор-файл с уникальными данными для проверки
|
||||||
|
donorData := map[string]interface{}{
|
||||||
|
"hostname": "REAL-DONOR-PC",
|
||||||
|
"vc": "v_from_real_file",
|
||||||
|
}
|
||||||
|
donorBytes, _ := json.Marshal(donorData)
|
||||||
|
if err := os.WriteFile(filepath.Join(tempDir, "donor_to_find.json"), donorBytes, 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать тестовый файл-донор: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем "файл-ловушку" от другого ККТ, который должен быть проигнорирован
|
||||||
|
kktTrapData := map[string]interface{}{
|
||||||
|
"modelName": "SOME-OTHER-KKT",
|
||||||
|
"serialNumber": "TRAP000001",
|
||||||
|
"hostname": "FAKE-HOSTNAME",
|
||||||
|
}
|
||||||
|
trapBytes, _ := json.Marshal(kktTrapData)
|
||||||
|
if err := os.WriteFile(filepath.Join(tempDir, "000001.json"), trapBytes, 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать файл-ловушку: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act: Вызываем тестируемую функцию
|
||||||
|
resultMap := findSourceWorkstationData()
|
||||||
|
|
||||||
|
// Assert: Проверяем результат
|
||||||
|
if resultMap == nil {
|
||||||
|
t.Fatal("findSourceWorkstationData() вернула nil, хотя ожидались данные из донора.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultMap["hostname"] != "REAL-DONOR-PC" {
|
||||||
|
t.Errorf("Hostname из донора прочитан неверно. Ожидалось 'REAL-DONOR-PC', получено '%v'", resultMap["hostname"])
|
||||||
|
}
|
||||||
|
if resultMap["vc"] != "v_from_real_file" {
|
||||||
|
t.Errorf("Поле 'vc' из донора прочитано неверно. Ожидалось 'v_from_real_file', получено '%v'", resultMap["vc"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- Сценарий 2: В папке /date нет подходящих файлов ---
|
||||||
|
t.Run("when no valid donor file exists", func(t *testing.T) {
|
||||||
|
// Arrange: Готовим файловую систему
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalOutputDir := outputDir
|
||||||
|
outputDir = tempDir
|
||||||
|
defer func() { outputDir = originalOutputDir }()
|
||||||
|
|
||||||
|
// Создаем только "файл-ловушку", который не должен считаться донором
|
||||||
|
kktTrapData := map[string]interface{}{
|
||||||
|
"modelName": "SOME-OTHER-KKT",
|
||||||
|
"serialNumber": "TRAP000001",
|
||||||
|
"hostname": "FAKE-HOSTNAME",
|
||||||
|
}
|
||||||
|
trapBytes, _ := json.Marshal(kktTrapData)
|
||||||
|
if err := os.WriteFile(filepath.Join(tempDir, "000001.json"), trapBytes, 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать файл-ловушку: %v", err)
|
||||||
|
}
|
||||||
|
// Создаем текстовый файл, который тоже должен быть проигнорирован
|
||||||
|
if err := os.WriteFile(filepath.Join(tempDir, "readme.txt"), []byte("info"), 0644); err != nil {
|
||||||
|
t.Fatalf("Не удалось создать текстовый файл: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act: Вызываем тестируемую функцию
|
||||||
|
resultMap := findSourceWorkstationData()
|
||||||
|
|
||||||
|
// Assert: Проверяем, что функция ничего не нашла
|
||||||
|
if resultMap != nil {
|
||||||
|
t.Fatal("findSourceWorkstationData() вернула данные, хотя в папке не было валидных доноров.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
package shtrih
|
package shtrih
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -127,3 +129,50 @@ func TestMockDriver_GetInfoError(t *testing.T) {
|
|||||||
t.Error("GetFiscalInfo() вернул данные вместе с ошибкой, хотя должен был вернуть nil.")
|
t.Error("GetFiscalInfo() вернул данные вместе с ошибкой, хотя должен был вернуть nil.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadMockDataFromFile читает "канонический" JSON-файл и преобразует его
|
||||||
|
// в структуру FiscalInfo для использования в мок-драйвере.
|
||||||
|
func loadMockDataFromFile(filePath string) (*FiscalInfo, error) {
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось прочитать файл с мок-данными '%s': %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var info FiscalInfo
|
||||||
|
if err := json.Unmarshal(data, &info); err != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось распарсить JSON из файла '%s': %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMockDriver_WithDataFromFile проверяет полный цикл работы мок-драйвера,
|
||||||
|
// используя для этого данные, загруженные из реального JSON-файла.
|
||||||
|
func TestMockDriver_WithDataFromFile(t *testing.T) {
|
||||||
|
// Arrange 1: Загружаем данные из нашего "золотого" файла.
|
||||||
|
// Путь указывается относительно корня пакета.
|
||||||
|
canonicalData, err := loadMockDataFromFile("testdata/canonical_kkt_data.json")
|
||||||
|
if err != nil {
|
||||||
|
// Если файл не найден или поврежден, тест должен провалиться.
|
||||||
|
t.Fatalf("Подготовка теста провалилась: не удалось загрузить мок-данные: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrange 2: Создаем мок-драйвер, передавая ему загруженные данные.
|
||||||
|
driver := NewMockDriver(canonicalData, nil, nil)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
if err := driver.Connect(); err != nil {
|
||||||
|
t.Fatalf("Connect() вернул неожиданную ошибку: %v", err)
|
||||||
|
}
|
||||||
|
defer driver.Disconnect()
|
||||||
|
|
||||||
|
info, err := driver.GetFiscalInfo()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetFiscalInfo() вернул неожиданную ошибку: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert: Сверяем, что драйвер вернул в точности те данные, что были в файле.
|
||||||
|
if !reflect.DeepEqual(info, canonicalData) {
|
||||||
|
t.Errorf("Данные, возвращенные мок-драйвером, не совпадают с данными из файла.\nПолучено: %+v\nОжидалось: %+v", info, canonicalData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user