From 27417ca2ef1ac08de89a759646a023376b1f9d41 Mon Sep 17 00:00:00 2001 From: AntonYPost Date: Wed, 22 May 2024 16:19:34 +0300 Subject: [PATCH 1/5] Add WithProperties engine option # Conflicts: # onpremise/file_watch.go --- onpremise/onpremise.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/onpremise/onpremise.go b/onpremise/onpremise.go index f8c8f11..adeb1dd 100644 --- a/onpremise/onpremise.go +++ b/onpremise/onpremise.go @@ -42,6 +42,7 @@ type Engine struct { fileExternallyChangedCount int filePullerStarted bool fileWatcherStarted bool + managerProperties string } const ( @@ -243,6 +244,16 @@ func WithRandomization(seconds int) EngineOptions { } } +// WithProperties sets properties that the engine checks for +// default is "" which will include all possible properties +func WithProperties(properties string) EngineOptions { + return func(cfg *Engine) error { + cfg.managerProperties = properties + + return nil + } +} + func New(config *dd.ConfigHash, opts ...EngineOptions) (*Engine, error) { engine := &Engine{ logger: logWrapper{ @@ -493,7 +504,7 @@ func (e *Engine) reloadManager(filePath string) error { if e.manager == nil { e.manager = dd.NewResourceManager() // init manager from file - err := dd.InitManagerFromFile(e.manager, *e.config, "", filePath) + err := dd.InitManagerFromFile(e.manager, *e.config, e.managerProperties, filePath) if err != nil { return fmt.Errorf("failed to init manager from file: %w", err) From 67da34c6c3cef7427bddb9d8e7910c6c08aac301 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 22 May 2024 20:42:45 +0200 Subject: [PATCH 2/5] WithProperties engine option list instead of string --- onpremise/onpremise.go | 21 +++++++++------- onpremise/onpremise_test.go | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/onpremise/onpremise.go b/onpremise/onpremise.go index adeb1dd..00076cb 100644 --- a/onpremise/onpremise.go +++ b/onpremise/onpremise.go @@ -6,6 +6,7 @@ import ( "net/url" "os" "path/filepath" + "strings" "sync" "time" @@ -50,10 +51,10 @@ const ( ) var ( - ErrNoDataFileProvided = errors.New("no data file provided") - ErrTooManyRetries = errors.New("too many retries to pull data file") - ErrFileNotModified = errors.New("data file not modified") - ErrLicenceKeyAndProductRequired = errors.New("license key and product are required") + ErrNoDataFileProvided = errors.New("no data file provided") + ErrTooManyRetries = errors.New("too many retries to pull data file") + ErrFileNotModified = errors.New("data file not modified") + ErrLicenseKeyRequired = errors.New("auto update set to true, no custom URL specified, license key is required") ) // run starts the engine @@ -246,10 +247,11 @@ func WithRandomization(seconds int) EngineOptions { // WithProperties sets properties that the engine checks for // default is "" which will include all possible properties -func WithProperties(properties string) EngineOptions { +func WithProperties(properties []string) EngineOptions { return func(cfg *Engine) error { - cfg.managerProperties = properties - + if properties != nil { + cfg.managerProperties = strings.Join(properties, ",") + } return nil } } @@ -272,7 +274,8 @@ func New(config *dd.ConfigHash, opts ...EngineOptions) (*Engine, error) { isCreateTempDataCopyEnabled: true, tempDataDir: "", //default 10 minutes - randomization: 10 * 60 * 1000, + randomization: 10 * 60 * 1000, + managerProperties: "", } for _, opt := range opts { @@ -426,7 +429,7 @@ func (e *Engine) appendProduct() error { func (e *Engine) validateAndAppendUrlParams() error { if e.isDefaultDataFileUrl() && !e.hasDefaultDistributorParams() && e.isAutoUpdateEnabled { - return ErrLicenceKeyAndProductRequired + return ErrLicenseKeyRequired } else if e.isDefaultDataFileUrl() && e.isAutoUpdateEnabled { err := e.appendLicenceKey() if err != nil { diff --git a/onpremise/onpremise_test.go b/onpremise/onpremise_test.go index 87e8fbb..fc3877c 100644 --- a/onpremise/onpremise_test.go +++ b/onpremise/onpremise_test.go @@ -91,3 +91,52 @@ func TestCustomProvider(t *testing.T) { } } } + +func testProperties(t *testing.T, properties []string, values []string, noValueProps []string) { + config := dd.NewConfigHash(dd.Balanced) + engine, err := New( + config, + WithDataFile("../51Degrees-LiteV4.1.hash"), + WithAutoUpdate(false), + WithProperties(properties), + ) + + if err != nil { + t.Fatalf("Error creating engine: %v", err) + } + result, err := engine.Process([]Evidence{ + { + dd.HttpHeaderString, + "User-Agent", + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.72 Mobile Safari/537.36"}, + }) + if err != nil { + t.Errorf("Error processing request: %v", err) + } + defer result.Free() + for i := 0; i < len(properties); i++ { + value, _ := result.ValuesString(properties[i], "") + if value != values[i] { + t.Errorf("Error processing request: expected %s, got %s", values[i], value) + } + } + + for i := 0; i < len(noValueProps); i++ { + value, _ := result.ValuesString(noValueProps[i], "") + if value != "" { + t.Errorf("Error processing request: expected no value, got %s", value) + } + } +} + +func TestWithPropertiesNoPlatform(t *testing.T) { + testProperties(t, []string{"IsMobile", "BrowserName"}, []string{"True", "Chrome Mobile"}, []string{"PlatformName"}) +} + +func TestWithPropertiesPlatform(t *testing.T) { + testProperties(t, []string{"PlatformName"}, []string{"Android"}, []string{"IsMobile", "BrowserName"}) +} + +func TestPropertiesDefault(t *testing.T) { + testProperties(t, nil, []string{"True", "Chrome Mobile", "Android"}, nil) +} From bf0dd037748d39a94fda462e7de99fa0b2ad1292 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 22 May 2024 22:10:15 +0200 Subject: [PATCH 3/5] .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83ef576 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +assets +51Degrees-LiteV4.1.hash From c32ae62d9e50cf7f33967c270b1863c20fae4524 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 22 May 2024 22:59:43 +0200 Subject: [PATCH 4/5] WithConfigHash to encapsulate config, add comments improved tests --- onpremise/file_pulling_test.go | 12 ----- onpremise/file_watch_test.go | 5 -- onpremise/onpremise.go | 99 +++++++++++++++++++++------------- onpremise/onpremise_test.go | 63 +++++++++++++--------- 4 files changed, 100 insertions(+), 79 deletions(-) diff --git a/onpremise/file_pulling_test.go b/onpremise/file_pulling_test.go index b12b53d..416a518 100644 --- a/onpremise/file_pulling_test.go +++ b/onpremise/file_pulling_test.go @@ -132,9 +132,6 @@ func newMockUncompressedDataFileServer(timeout time.Duration) *httptest.Server { func TestFilePulling(t *testing.T) { server := newMockDataFileServer(10 * time.Second) defer server.Close() - - config := dd.NewConfigHash(dd.Balanced) - tempFile, err := unzipAndSaveToTempFile("test_file_pulling_test.hash") if err != nil { t.Fatalf("Error creating temp file: %v", err) @@ -142,7 +139,6 @@ func TestFilePulling(t *testing.T) { defer os.Remove(tempFile.Name()) engine, err := New( - config, WithDataUpdateUrl( server.URL+"/datafile", ), @@ -266,8 +262,6 @@ func TestIsUpdateOnStartDisabled(t *testing.T) { server := newMockDataFileServer(10 * time.Second) defer server.Close() - config := dd.NewConfigHash(dd.Balanced) - tempFile, err := unzipAndSaveToTempFile("TestIsUpdateOnStartDisabled.hash") if err != nil { t.Fatalf("Error creating temp file: %v", err) @@ -276,7 +270,6 @@ func TestIsUpdateOnStartDisabled(t *testing.T) { defer os.Remove(tempFile.Name()) engine, err := New( - config, WithDataUpdateUrl( server.URL+"/datafile", ), @@ -302,8 +295,6 @@ func TestToggleAutoUpdate(t *testing.T) { server := newMockDataFileServer(10 * time.Second) defer server.Close() - config := dd.NewConfigHash(dd.Balanced) - tempFile, err := unzipAndSaveToTempFile("TestToggleAutoUpdate.hash") if err != nil { t.Fatalf("Error creating temp file: %v", err) @@ -312,7 +303,6 @@ func TestToggleAutoUpdate(t *testing.T) { defer os.Remove(tempFile.Name()) engine, err := New( - config, WithDataUpdateUrl( server.URL+"/datafile", ), @@ -339,7 +329,6 @@ func TestUncompressedDataUrl(t *testing.T) { server := newMockUncompressedDataFileServer(10 * time.Second) defer server.Close() - config := dd.NewConfigHash(dd.Balanced) tempFile, err := unzipAndSaveToTempFile("TestUncompressedDataUrl.hash") if err != nil { t.Fatalf("Error creating temp file: %v", err) @@ -348,7 +337,6 @@ func TestUncompressedDataUrl(t *testing.T) { defer os.Remove(tempFile.Name()) engine, err := New( - config, WithDataUpdateUrl(server.URL+"/datafile"), WithPollingInterval(2), WithDataFile(tempFile.Name()), diff --git a/onpremise/file_watch_test.go b/onpremise/file_watch_test.go index 78520bd..7cb3aa5 100644 --- a/onpremise/file_watch_test.go +++ b/onpremise/file_watch_test.go @@ -43,8 +43,6 @@ func unzipAndSaveToTempFile(name string) (*os.File, error) { } func TestExternalFileChangedReplace(t *testing.T) { - config := dd.NewConfigHash(dd.Balanced) - tempFile, err := unzipAndSaveToTempFile("TestExternalFileChangedReplace.hash") if err != nil { t.Fatalf("Error creating temp file: %v", err) @@ -53,7 +51,6 @@ func TestExternalFileChangedReplace(t *testing.T) { defer tempFile.Close() engine, err := New( - config, WithDataFile("TestExternalFileChangedReplace.hash"), WithFileWatch(true), WithAutoUpdate(false), @@ -154,7 +151,6 @@ func TestExternalFileChangedMv(t *testing.T) { if runtime.GOOS == "windows" { return } - config := dd.NewConfigHash(dd.Balanced) tempDir, err := os.MkdirTemp("", "TestExternalFileChangedMv") if err != nil { @@ -169,7 +165,6 @@ func TestExternalFileChangedMv(t *testing.T) { defer tempFile.Close() engine, err := New( - config, WithDataFile(originalFileName), WithFileWatch(true), WithAutoUpdate(false), diff --git a/onpremise/onpremise.go b/onpremise/onpremise.go index 00076cb..4dda2bd 100644 --- a/onpremise/onpremise.go +++ b/onpremise/onpremise.go @@ -14,6 +14,12 @@ import ( "github.com/google/uuid" ) +// Engine is an implementation of the on-premise (based on a local data file) device detection. It encapsulates +// the automatic data file updates feature - to periodically fetch and reload the new data file. +// File system watcher feature allows to monitor for changes to the local data file and reload it when it changes. +// Custom URL can be used to fetch data files, the polling interval is configurable +// The 51degrees distributor service can also be used with a licenseKey +// For more information see With... options and examples type Engine struct { logger logWrapper fileWatcher fileWatcher @@ -54,7 +60,7 @@ var ( ErrNoDataFileProvided = errors.New("no data file provided") ErrTooManyRetries = errors.New("too many retries to pull data file") ErrFileNotModified = errors.New("data file not modified") - ErrLicenseKeyRequired = errors.New("auto update set to true, no custom URL specified, license key is required") + ErrLicenseKeyRequired = errors.New("auto update set to true, no custom URL specified, license key is required, set it using WithLicenseKey") ) // run starts the engine @@ -93,6 +99,18 @@ func WithDataFile(path string) EngineOptions { } } +// WithConfigHash allows to configure the Hash matching algorithm. +// See dd.ConfigHash type for all available settings: +// PerformanceProfile, Drift, Difference, Concurrency +// By default initialized with dd.Balanced performance profile +// dd.NewConfigHash(dd.Balanced) +func WithConfigHash(configHash *dd.ConfigHash) EngineOptions { + return func(cfg *Engine) error { + cfg.config = configHash + return nil + } +} + // WithLicenseKey sets the license key to use when pulling the data file // this option can only be used when using the default data file url from 51Degrees, it will be appended as a query parameter func WithLicenseKey(key string) EngineOptions { @@ -105,8 +123,8 @@ func WithLicenseKey(key string) EngineOptions { } } -// WithProduct sets the product to use when pulling the data file -// this option can only be used when using the default data file url from 51Degrees, it will be appended as a query parameter +// WithProduct sets the product to use when pulling the data file when distributor service is used +// licenseKey has to be provided using WithLicenseKey func WithProduct(product string) EngineOptions { return func(cfg *Engine) error { if !cfg.isDefaultDataFileUrl() { @@ -118,7 +136,7 @@ func WithProduct(product string) EngineOptions { } } -// WithDataUpdateUrl sets the URL to pull data from and the interval in milliseconds +// WithDataUpdateUrl sets a custom URL to download the data file from func WithDataUpdateUrl(urlStr string) EngineOptions { return func(cfg *Engine) error { _, err := url.ParseRequestURI(urlStr) @@ -132,7 +150,7 @@ func WithDataUpdateUrl(urlStr string) EngineOptions { } } -// WithMaxRetries sets the maximum number of retries to pull the data file +// WithMaxRetries sets the maximum number of retries to pull the data file if request fails func WithMaxRetries(retries int) EngineOptions { return func(cfg *Engine) error { cfg.maxRetries = retries @@ -168,10 +186,8 @@ func WithCustomLogger(logger LogWriter) EngineOptions { } } -// WithFileWatch enables or disables file watching -// in case 3rd party updates the data file on file system -// engine will automatically reload the data file -// default is true +// WithFileWatch enables or disables file watching in case 3rd party updates the data file +// engine will automatically reload the data file. Default is true func WithFileWatch(enabled bool) EngineOptions { return func(cfg *Engine) error { cfg.isFileWatcherEnabled = enabled @@ -180,8 +196,8 @@ func WithFileWatch(enabled bool) EngineOptions { } // WithUpdateOnStart enables or disables update on start +// if enabled, engine will pull the data file from the distributor (or custom URL) once initialized // default is false -// if enabled, engine will pull the data file from the distributor on start of the engine func WithUpdateOnStart(enabled bool) EngineOptions { return func(cfg *Engine) error { cfg.isUpdateOnStartEnabled = enabled @@ -192,9 +208,8 @@ func WithUpdateOnStart(enabled bool) EngineOptions { // WithAutoUpdate enables or disables auto update // default is true -// if enabled, engine will automatically pull the data file from the distributor -// if disabled, engine will not pull the data file from the distributor -// options like WithDataUpdateUrl, WithLicenseKey will be ignored since auto update is disabled +// if enabled, engine will automatically pull the data file from the distributor or custom URL +// if disabled options like WithDataUpdateUrl, WithLicenseKey will be ignored func WithAutoUpdate(enabled bool) EngineOptions { return func(cfg *Engine) error { cfg.isAutoUpdateEnabled = enabled @@ -205,9 +220,9 @@ func WithAutoUpdate(enabled bool) EngineOptions { // WithTempDataCopy enables or disables creating a temp copy of the data file // default is true -// if enabled, engine will create a temp copy of the data file and use it to initialize the manager +// if enabled, engine will create a temp copy of the data file and use it for detection rather than original data file // if disabled, engine will use the original data file to initialize the manager -// this is useful when 3rd party updates the data file on file system +// this is useful when 3rd party updates the data file on the file system func WithTempDataCopy(enabled bool) EngineOptions { return func(cfg *Engine) error { cfg.isCreateTempDataCopyEnabled = enabled @@ -216,9 +231,9 @@ func WithTempDataCopy(enabled bool) EngineOptions { } } -// SetTempDataDir sets the directory to store the temp data file -// default is current directory -func SetTempDataDir(dir string) EngineOptions { +// WithTempDataDir sets the directory to store the temp data file +// default is system temp directory +func WithTempDataDir(dir string) EngineOptions { return func(cfg *Engine) error { dirFileInfo, err := os.Stat(dir) if err != nil { @@ -237,7 +252,7 @@ func SetTempDataDir(dir string) EngineOptions { // WithRandomization sets the randomization time in seconds // default is 10 minutes // if set, when scheduling the file pulling, it will add randomization time to the interval -// this is useful to avoid all engines pulling the data file at the same time in case of multiple engines/instances +// this is useful to avoid multiple engines pulling the data file at the same time in case of multiple engines/instances func WithRandomization(seconds int) EngineOptions { return func(cfg *Engine) error { cfg.randomization = seconds * 1000 @@ -245,7 +260,7 @@ func WithRandomization(seconds int) EngineOptions { } } -// WithProperties sets properties that the engine checks for +// WithProperties sets properties that the engine retrieves from the data file for each device detection result instance // default is "" which will include all possible properties func WithProperties(properties []string) EngineOptions { return func(cfg *Engine) error { @@ -256,26 +271,26 @@ func WithProperties(properties []string) EngineOptions { } } -func New(config *dd.ConfigHash, opts ...EngineOptions) (*Engine, error) { +// New creates an instance of the on-premise device detection engine. WithDataFile must be provided +// to specify the path to the data file, otherwise initialization will fail +func New(opts ...EngineOptions) (*Engine, error) { engine := &Engine{ logger: logWrapper{ logger: DefaultLogger, enabled: true, }, - config: config, - stopCh: make(chan *sync.WaitGroup), - fileSynced: false, - dataFileUrl: defaultDataFileUrl, - //default 15 minutes - dataFilePullEveryMs: 30 * 60 * 1000, + config: nil, + stopCh: make(chan *sync.WaitGroup), + fileSynced: false, + dataFileUrl: defaultDataFileUrl, + dataFilePullEveryMs: 30 * 60 * 1000, // default 30 minutes isFileWatcherEnabled: true, isUpdateOnStartEnabled: false, isAutoUpdateEnabled: true, isCreateTempDataCopyEnabled: true, tempDataDir: "", - //default 10 minutes - randomization: 10 * 60 * 1000, - managerProperties: "", + randomization: 10 * 60 * 1000, // default 10 minutes + managerProperties: "", } for _, opt := range opts { @@ -322,14 +337,20 @@ func New(config *dd.ConfigHash, opts ...EngineOptions) (*Engine, error) { return engine, nil } +// Evidence struct encapsulates the evidence provided as input to the Process function +// Evidence is usually an HTTP header, thus Prefix would be dd.HttpHeaderString, +// but can also be a query param (dd.HttpEvidenceQuery) or a cookie (dd.HttpEvidenceCookie) type Evidence struct { Prefix dd.EvidencePrefix Key string Value string } -func (e *Engine) Process(evidences []Evidence) (*dd.ResultsHash, error) { - evidenceHash, err := mapEvidence(evidences) +// Process detects the device from the provided evidence list +// returns the dd.ResultsHash object from which various device properties +// are retrieved +func (e *Engine) Process(evidenceList []Evidence) (*dd.ResultsHash, error) { + evidenceHash, err := mapEvidence(evidenceList) if err != nil { return nil, err } @@ -344,10 +365,10 @@ func (e *Engine) Process(evidences []Evidence) (*dd.ResultsHash, error) { return results, nil } -func mapEvidence(evidences []Evidence) (*dd.Evidence, error) { - evidenceHash := dd.NewEvidenceHash(uint32(len(evidences))) +func mapEvidence(evidenceList []Evidence) (*dd.Evidence, error) { + evidenceHash := dd.NewEvidenceHash(uint32(len(evidenceList))) - for _, evidence := range evidences { + for _, evidence := range evidenceList { err := evidenceHash.Add(evidence.Prefix, evidence.Key, evidence.Value) if err != nil { return nil, fmt.Errorf("failed to add evidence: %w", err) @@ -357,6 +378,8 @@ func mapEvidence(evidences []Evidence) (*dd.Evidence, error) { return evidenceHash, nil } +// Stop has to be called to free all the resources of the engine +// before the instance goes out of scope func (e *Engine) Stop() { num := 0 if e.isAutoUpdateEnabled && e.filePullerStarted { @@ -382,7 +405,7 @@ func (e *Engine) Stop() { if e.manager != nil { e.manager.Free() } else { - e.logger.Printf("manager is nil") + e.logger.Printf("stopping engine, manager is nil") } if e.isCreateTempDataCopyEnabled { @@ -391,6 +414,7 @@ func (e *Engine) Stop() { } } +// GetHttpHeaderKeys returns all HTTP headers that can be used as evidence for device detection func (e *Engine) GetHttpHeaderKeys() []dd.EvidenceKey { return e.manager.HttpHeaderKeys } @@ -507,6 +531,9 @@ func (e *Engine) reloadManager(filePath string) error { if e.manager == nil { e.manager = dd.NewResourceManager() // init manager from file + if e.config == nil { + e.config = dd.NewConfigHash(dd.Balanced) + } err := dd.InitManagerFromFile(e.manager, *e.config, e.managerProperties, filePath) if err != nil { diff --git a/onpremise/onpremise_test.go b/onpremise/onpremise_test.go index fc3877c..378ecf6 100644 --- a/onpremise/onpremise_test.go +++ b/onpremise/onpremise_test.go @@ -1,7 +1,6 @@ package onpremise import ( - "fmt" "os" "testing" "time" @@ -15,11 +14,13 @@ func TestCustomProvider(t *testing.T) { cases := []struct { name string + datafile string engineOptions []EngineOptions expectedError string }{ { - name: "with license key and custom url", + name: "with license key and custom url", + datafile: "withLicenseKey.hash", engineOptions: []EngineOptions{ WithLicenseKey("123"), WithDataUpdateUrl(mockServer.URL + "/datafile"), @@ -29,7 +30,8 @@ func TestCustomProvider(t *testing.T) { expectedError: "", }, { - name: "with product and custom url", + name: "with product and custom url", + datafile: "withProductCustomURL.hash", engineOptions: []EngineOptions{ WithProduct("MyProduct"), WithDataUpdateUrl(mockServer.URL + "/datafile"), @@ -39,7 +41,8 @@ func TestCustomProvider(t *testing.T) { expectedError: "", }, { - name: "Invalid url", + name: "Invalid url", + datafile: "invalidURL.hash", engineOptions: []EngineOptions{ WithDataUpdateUrl("dsoahdsakjhd"), WithPollingInterval(2), @@ -48,7 +51,8 @@ func TestCustomProvider(t *testing.T) { expectedError: `parse "dsoahdsakjhd": invalid URI for request`, }, { - name: "with custom url", + name: "with custom url", + datafile: "withCustomURL.hash", engineOptions: []EngineOptions{ WithDataUpdateUrl(mockServer.URL + "/datafile"), WithPollingInterval(2), @@ -56,46 +60,53 @@ func TestCustomProvider(t *testing.T) { }, expectedError: "", }, - { - name: "no data file", - engineOptions: []EngineOptions{ - WithDataUpdateUrl(mockServer.URL + "/datafile"), - WithPollingInterval(2), - WithFileWatch(false), - }, - expectedError: ErrNoDataFileProvided.Error(), - }, } for _, c := range cases { - tempFile, err := unzipAndSaveToTempFile(fmt.Sprintf("test_%s.hash", c.name)) - if err != nil { - t.Fatalf("Error creating temp file: %v", err) + defer func(datafile string) { + if datafile != "" { + err := os.Remove(datafile) + if err != nil { + t.Fatalf("failed to remove %s: %s", datafile, err) + } + } + }(c.datafile) + + if c.datafile != "" { + tempFile, err := unzipAndSaveToTempFile(c.datafile) + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + tempFile.Close() } - options := append(c.engineOptions, WithDataFile(tempFile.Name())) + options := append(c.engineOptions, WithDataFile(c.datafile)) - defer tempFile.Close() - defer os.Remove(tempFile.Name()) - config := dd.NewConfigHash(dd.Balanced) engine, err := New( - config, options..., ) if engine != nil { engine.Stop() } - <-time.After(3 * time.Second) - if err != nil && err.Error() != c.expectedError { + //<-time.After(3 * time.Second) + if (err != nil && err.Error() != c.expectedError) || (err == nil && c.expectedError != "") { t.Errorf("Failed case %s: %v", c.name, err) } } } +func TestNoDataFileProvided(t *testing.T) { + engine, err := New() + if engine != nil { + t.Errorf("expected engine to be nil") + } + if err != ErrNoDataFileProvided { + t.Errorf("expected ErrNoDataFileProvided") + } +} + func testProperties(t *testing.T, properties []string, values []string, noValueProps []string) { - config := dd.NewConfigHash(dd.Balanced) engine, err := New( - config, WithDataFile("../51Degrees-LiteV4.1.hash"), WithAutoUpdate(false), WithProperties(properties), From d1e7c74cdf0ae112603754bbcc4e08449a3cd601 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 22 May 2024 23:05:07 +0200 Subject: [PATCH 5/5] update README --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e42200c..c1548ee 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,9 @@ go env -w CGO_ENABLED=1 This module name `device-detection-go` at path `github.com/51Degrees/device-detection-go/v4` -This Go Lite version contains only one single package: -- `dd` +This Go Lite version contains the following packages: +- `dd` - a lower level API wrapping the C device detection library +- `onpremise` - a higher level Engine API providing device detection and [automatic data file updates](https://51degrees.com/documentation/4.4/_features__automatic_datafile_updates.html) ## Build and Usage @@ -77,12 +78,12 @@ The amalgamation is produced automatically and regularly by the [Nightly Package ## Test -Unit tests can be run with `go test` within `dd` directory. +Unit tests can be run with `go test ./...` from the root dir. ## APIs To view APIs and their descriptions, users can use `go doc` in the package directory. -- First navigate to folder `dd`. +- First navigate to `dd` or `onpremise` dir. - Then run the below to display all APIs, structures and their descriptions. ``` go doc -all