├── .editorconfig
├── .github
└── workflows
│ ├── publish-package.yml
│ └── release.yml
├── .gitignore
├── .gitmodules
├── .luacheckrc
├── .luacov
├── .travis.yml
├── LICENSE
├── README.md
├── bin
├── run-tests.server.lua
└── spec.lua
├── default.project.json
├── foreman.toml
├── lib
├── MockDataStoreService
│ ├── MockDataStoreConstants.lua
│ ├── MockDataStoreManager.lua
│ ├── MockDataStorePages.lua
│ ├── MockDataStoreUtils.lua
│ ├── MockGlobalDataStore.lua
│ ├── MockOrderedDataStore.lua
│ └── init.lua
└── init.lua
├── place.project.json
├── rotriever.toml
├── spec
├── MockDataStoreService
│ ├── MockDataStoreConstants.spec.lua
│ ├── MockDataStorePages.spec.lua
│ ├── MockDataStoreUtils.spec.lua
│ ├── MockGlobalDataStore.spec.lua
│ ├── MockOrderedDataStore.spec.lua
│ ├── Test.lua
│ └── init.spec.lua
└── init.spec.lua
└── wally.toml
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = false
8 |
9 | [*.{lua,md}]
10 | indent_style = tab
11 |
12 | [*.json]
13 | indent_style = spaces
14 | indent_width = 2
--------------------------------------------------------------------------------
/.github/workflows/publish-package.yml:
--------------------------------------------------------------------------------
1 | name : Publish system packages to registry
2 | on:
3 | release:
4 | types: [published]
5 |
6 | jobs:
7 | package-publish:
8 | name: Publish packages to wally registry
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout repository
12 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c
13 |
14 | - name: Setup foreman
15 | uses: rojo-rbx/setup-foreman@62bc697705339a6049f74c9d0ff6d39cffc993e5
16 | with:
17 | token: ${{ secrets.GITHUB_TOKEN }}
18 |
19 | - name: Publish wally packages
20 | env:
21 | WALLY_AUTH: ${{ secrets.WALLY_AUTH }}
22 | run: |
23 | mkdir -p ~/.wally
24 | echo "$WALLY_AUTH" > ~/.wally/auth.toml
25 | wally publish
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name : MockDataStoreService release
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 | workflow_dispatch:
7 | jobs:
8 | release:
9 | name: Create MockDataStoreService release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v2.3.0
14 | with:
15 | submodules: recursive
16 |
17 | - name: Setup foreman
18 | uses: rojo-rbx/setup-foreman@v1
19 | with:
20 | token: ${{ secrets.GITHUB_TOKEN }}
21 |
22 | - name: Build default.project.json
23 | run: |
24 | rojo build default.project.json --output MockDataStoreService.rbxmx
25 |
26 | - name: Create release
27 | id: create_release
28 | uses: actions/create-release@v1.1.0
29 | env:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 | with:
32 | tag_name: ${{ github.ref }}
33 | release_name: ${{ github.ref }}
34 | draft: true
35 |
36 | - name: Upload MockDataStoreService.rbxmx
37 | uses: actions/upload-release-asset@v1.0.2
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 | with:
41 | upload_url: ${{ steps.create_release.outputs.upload_url }}
42 | asset_path: MockDataStoreService.rbxmx
43 | asset_name: MockDataStoreService.rbxmx
44 | asset_content_type: application/xml
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /luacov.*
2 | /*.rbxlx
3 | /*.rbxmx
4 | /*.rbxm
5 | /*.rbxl
6 | /*.lock
7 | /Packages
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "vendor/testez"]
2 | path = vendor/testez
3 | url = https://github.com/Roblox/testez
4 |
--------------------------------------------------------------------------------
/.luacheckrc:
--------------------------------------------------------------------------------
1 | stds.roblox = {
2 | globals = {
3 | "game"
4 | },
5 |
6 | read_globals = {
7 | -- Global objects
8 | "script",
9 |
10 | -- Global functions
11 | "spawn",
12 | "delay",
13 | "warn",
14 | "wait",
15 | "tick",
16 | "typeof",
17 | "settings",
18 |
19 | -- Global Namespaces
20 | "Enum",
21 | "debug",
22 | "utf8",
23 |
24 | math = {
25 | fields = {
26 | "clamp",
27 | "sign"
28 | }
29 | },
30 |
31 | -- Global types
32 | "Instance",
33 | "Vector2",
34 | "Vector3",
35 | "CFrame",
36 | "Color3",
37 | "UDim",
38 | "UDim2",
39 | "Rect",
40 | "TweenInfo",
41 | "Random"
42 | }
43 | }
44 |
45 | stds.testez = {
46 | read_globals = {
47 | "describe",
48 | "it", "itFOCUS", "itSKIP",
49 | "FOCUS", "SKIP", "HACK_NO_XPCALL",
50 | "expect",
51 | }
52 | }
53 |
54 | ignore = { "111" }
55 |
56 | std = "lua51+roblox"
57 |
58 | files["**/*.spec.lua"] = {
59 | std = "+testez",
60 | }
--------------------------------------------------------------------------------
/.luacov:
--------------------------------------------------------------------------------
1 | return {
2 | include = {
3 | "^lib",
4 | },
5 | exclude = {
6 | "%.spec$",
7 | },
8 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # http://kiki.to/blog/2016/02/04/talk-continuous-integration-with-lua/
2 | language: python
3 | sudo: false
4 |
5 | branches:
6 | only:
7 | - master
8 |
9 | env:
10 | - LUA="lua=5.1"
11 |
12 | before_install:
13 | - pip install hererocks
14 | - hererocks lua_install -r^ --$LUA
15 | - export PATH=$PATH:$PWD/lua_install/bin
16 |
17 | install:
18 | - luarocks install luafilesystem
19 | - luarocks install busted
20 | - luarocks install luacov
21 | - luarocks install luacov-coveralls
22 | - luarocks install luacheck
23 |
24 | script:
25 | - luacheck lib
26 | # - lua -lluacov bin/spec.lua
27 |
28 | extra_css:
29 | - extra.css
30 |
31 | #after_success:
32 | # - luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2018 buildthomas
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
MockDataStoreService
2 |
10 |
11 |
12 | Emulation of Roblox's DataStoreService for seamless offline development & testing
13 |
14 |
15 |
16 |
17 | This is a set of modules that emulates datastores in Lua rather than using the actual service. This is useful for testing in offline projects / local place files with code/frameworks that need to have access to datastores.
18 |
19 | The MockDataStoreService behaves exactly like DataStoreService: it has the same API, and will also work even if the place is not currently published to a game with Studio API access enabled (it will act as a DataStoreService with no data stored on the back-end, unless you import some data from a json string manually using the module).
20 |
21 | A small top-level helper module is provided (DataStoreService) that automatically detects and selects which datastores should be used (real datastores for published games with API access, mock datastores for offline games / published games without API access).
22 |
23 | It is recommended to use this code in the structure that it is provided in, and to simply call `require(path.to.DataStoreService)` instead of `game:GetService("DataStoreService")` anywhere in your code to use it properly.
24 |
25 | -----
26 |
27 | **Usage:**
28 |
29 | ```lua
30 | local DataStoreService = require(the.path.to.DataStoreService)
31 |
32 | -- Use as actual DataStoreService, i.e.:
33 |
34 | local gds = DataStoreService:GetGlobalDataStore()
35 | local ds = DataStoreService:GetDataStore("TestName", "TestScope")
36 | local ods = DataStoreService:GetOrderedDataStore("TestName")
37 |
38 | local value = ds:GetAsync("TestKey")
39 | ds:SetAsync("TestKey", "TestValue")
40 | local value = ds:IncrementAsync("IntegerKey", 3)
41 | local value = ds:UpdateAsync("UpdateKey", function(oldValue) return newValue end)
42 | local value = ds:RemoveAsync("TestKey")
43 | local connection = ds:OnUpdate("UpdateKey", function(value) print(value) end)
44 |
45 | local pages = ods:GetSortedAsync(true, 50, 1, 100)
46 | repeat
47 | for _, pair in ipairs(pages:GetCurrentPage()) do
48 | local key, value = pair.key, pair.value
49 | -- (...)
50 | end
51 | until pages.IsFinished or pages:AdvanceToNextPageAsync()
52 |
53 | local budget = DataStoreService:GetRequestBudgetForRequestType(
54 | Enum.DataStoreRequestType.UpdateAsync
55 | )
56 |
57 | -- Import/export data to a specific datastore:
58 |
59 | ds:ImportFromJSON({ -- feed table or json string representing contents of datastore
60 | TestKey = "Hello world!"; -- a key value pair
61 | AnotherKey = {a = 1, b = 2}; -- another key value pair
62 | -- (...)
63 | })
64 |
65 | print(ds:ExportToJSON())
66 |
67 | -- Import/export entirety of DataStoreService:
68 |
69 | DataStoreService:ImportFromJSON({ -- feed table or json string
70 | DataStore = { -- regular datastores
71 | TestName = { -- name of datastore
72 | TestScope = { -- scope of datastore
73 | TestKey = "Hello world!"; -- a key value pair
74 | AnotherKey = {1,2,3}; -- another key value pair
75 | -- (...)
76 | }
77 | }
78 | };
79 | GlobalDataStore = { -- the (one) globaldatastore
80 | TestKey = "Hello world!"; -- a key value pair
81 | AnotherKey = {1,2,3}; -- another key value pair
82 | -- (...)
83 | };
84 | OrderedDataStore = { -- ordered datastores
85 | TestName = { -- name of ordered datastore
86 | TestScope = { -- scope of ordered datastore
87 | TestKey = 15; -- a key value pair
88 | AnotherKey = 3; -- another key value pair
89 | -- (...)
90 | }
91 | }
92 | };
93 | }
94 |
95 | print(DataStoreService:ExportToJSON())
96 |
97 | ```
98 |
99 | Review the API of datastores here:
100 |
101 | -
102 | -
103 | -
104 | -
105 |
106 | -----
107 |
108 | **Features:**
109 |
110 | - Identical API and near-identical behavior compared to real DataStoreService.
111 | - Error messages are more verbose/accurate than the ones generated by actual datastores, which makes development/bug-fixing easier.
112 | - Throws descriptive errors for attempts at storing invalid data, telling you exactly which part of the data is invalid. (credit to @Corecii's helper function)
113 | - Emulates the yielding of datastore requests (waits a random amount of time before returning from the call).
114 | - Extra API for json-exporting/importing contents of one/all datastores for easy testing.
115 | - All operations safely deep-copy values where necessary (not possible to alter values in the datastore by externally altering tables, etc).
116 | - Enforces the "6 seconds between writes on the same key" rule.
117 | - Enforces datastore budgets correctly: budget are set and increased at the rates of the actual service, requests will be throttled if the budget is exceeded, and if there are too many throttled requests in the queue then new requests will error instead of throttling.
118 | - Functionality for simulating errors at a certain rate (similar to deprecated/removed Diabolical Mode that Studio used to have).
119 |
120 | -----
121 |
122 | **TODOs:**
123 |
124 | - Add more test cases for budgeting
125 | - Refine existing tests
126 |
--------------------------------------------------------------------------------
/bin/run-tests.server.lua:
--------------------------------------------------------------------------------
1 | -- luacheck: globals __LEMUR__
2 |
3 | local ServerStorage = game:GetService("ServerStorage")
4 |
5 | local TestEZ = require(ServerStorage.TestEZ)
6 |
7 | local results = TestEZ.TestBootstrap:run(ServerStorage.TestDataStoreService, TestEZ.Reporters.TextReporter)
8 |
9 | if __LEMUR__ then
10 | if results.failureCount > 0 then
11 | os.exit(1)
12 | end
13 | end
--------------------------------------------------------------------------------
/bin/spec.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | Loads our library and all of its dependencies, then runs tests using TestEZ.
3 |
4 | Currently unused
5 | ]]
6 |
7 | -- If you add any dependencies, add them to this table so they'll be loaded!
8 | local LOAD_MODULES = {
9 | {"lib", "DataStoreService"},
10 | {"spec", "TestDataStoreService"},
11 | {"vendor/testez/lib", "TestEZ"},
12 | }
13 |
14 | -- This makes sure we can load Lemur and other libraries that depend on init.lua
15 | package.path = package.path .. ";?/init.lua"
16 |
17 | -- If this fails, make sure you've cloned all Git submodules of this repo!
18 | local lemur = require("vendor.lemur")
19 |
20 | -- Create a virtual Roblox tree
21 | local habitat = lemur.Habitat.new()
22 |
23 | -- We'll put all of our library code and dependencies here
24 | local Root = habitat.game:GetService("ServerStorage")
25 | Root.Name = "Root"
26 |
27 | -- Load all of the modules specified above
28 | for _, module in ipairs(LOAD_MODULES) do
29 | local container = habitat:loadFromFs(module[1])
30 | container.Name = module[2]
31 | container.Parent = Root
32 | end
33 |
34 | local runTests = habitat:loadFromFs("bin/run-tests.server.lua")
35 |
36 | -- When Lemur implements a proper scheduling interface, we'll use that instead.
37 | habitat:require(runTests)
--------------------------------------------------------------------------------
/default.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "DataStoreService",
3 | "tree": {
4 | "$path": "lib"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/foreman.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | rojo = {source = "Roblox/rojo", version = "6.0.0-rc.1"}
3 | wally = {source = "upliftgames/wally", version = "=0.3.1"}
--------------------------------------------------------------------------------
/lib/MockDataStoreService/MockDataStoreConstants.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | MockDataStoreConstants.lua
3 | Contains all constants used by the entirety of MockDataStoreService and its sub-classes.
4 |
5 | This module is licensed under APLv2, refer to the LICENSE file or:
6 | https://github.com/buildthomas/MockDataStoreService/blob/master/LICENSE
7 | ]]
8 |
9 | return {
10 |
11 | LOGGING_ENABLED = false; -- Verbose logging of transactions to output
12 | LOGGING_FUNCTION = warn; -- Function for logging messages
13 |
14 | MAX_LENGTH_KEY = 50; -- Max number of chars in key string
15 | MAX_LENGTH_NAME = 50; -- Max number of chars in name string
16 | MAX_LENGTH_SCOPE = 50; -- Max number of chars in scope string
17 | MAX_LENGTH_DATA = 4194301; -- Max number of chars in (encoded) data strings
18 |
19 | MAX_PAGE_SIZE = 100; -- Max page size for GetSortedAsync
20 |
21 | YIELD_TIME_MIN = 0.2; -- Random yield time values for set/get/update/remove/getsorted
22 | YIELD_TIME_MAX = 0.5;
23 |
24 | YIELD_TIME_UPDATE_MIN = 0.2; -- Random yield times from events from OnUpdate
25 | YIELD_TIME_UPDATE_MAX = 0.5;
26 |
27 | WRITE_COOLDOWN = 6.0; -- Amount of cooldown time between writes on the same key in a particular datastore
28 |
29 | GET_COOLDOWN = 5.0; -- Amount of cooldown time that a recent interaction with a key is considered fresh
30 |
31 | THROTTLE_QUEUE_SIZE = 30; -- Amount of requests that can be throttled at once (additional requests will error)
32 |
33 | SIMULATE_ERROR_RATE = 0; -- Rate at which requests will throw errors for testing (0 = never, 1 = always)
34 |
35 | BUDGETING_ENABLED = true; -- Whether budgets are enforced and calculated
36 |
37 | BUDGET_GETASYNC = { -- Budget constant storing structure
38 | START = 100; -- Starting budget
39 | RATE = 60; -- Added budget per minute
40 | RATE_PLR = 10; -- Additional added budget per minute per player
41 | MAX_FACTOR = 3; -- The maximum budget as a factor of (rate + rate_plr * #players)
42 | };
43 |
44 | BUDGET_GETSORTEDASYNC = {
45 | START = 10;
46 | RATE = 5;
47 | RATE_PLR = 2;
48 | MAX_FACTOR = 3;
49 | };
50 |
51 | BUDGET_ONUPDATE = {
52 | START = 30;
53 | RATE = 30;
54 | RATE_PLR = 5;
55 | MAX_FACTOR = 1;
56 | };
57 |
58 | BUDGET_SETINCREMENTASYNC = {
59 | START = 100;
60 | RATE = 60;
61 | RATE_PLR = 10;
62 | MAX_FACTOR = 3;
63 | };
64 |
65 | BUDGET_SETINCREMENTSORTEDASYNC = {
66 | START = 50;
67 | RATE = 30;
68 | RATE_PLR = 5;
69 | MAX_FACTOR = 3;
70 | };
71 |
72 | BUDGET_BASE = 60; -- Modifiers used for budget increases on OnClose
73 | BUDGET_ONCLOSE_BASE = 150;
74 |
75 | BUDGET_UPDATE_INTERVAL = 1.0; -- Time interval in seconds at which budgets are updated (do not put too low)
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/lib/MockDataStoreService/MockDataStoreManager.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | MockDataStoreManager.lua
3 | This module does bookkeeping of data, interfaces and request limits used by MockDataStoreService and its sub-classes.
4 |
5 | This module is licensed under APLv2, refer to the LICENSE file or:
6 | https://github.com/buildthomas/MockDataStoreService/blob/master/LICENSE
7 | ]]
8 |
9 | local MockDataStoreManager = {}
10 |
11 | local Utils = require(script.Parent.MockDataStoreUtils)
12 | local Constants = require(script.Parent.MockDataStoreConstants)
13 | local HttpService = game:GetService("HttpService") -- for json encode/decode
14 | local Players = game:GetService("Players") -- for restoring budgets
15 | local RunService = game:GetService("RunService") -- for checking if running context is on server
16 |
17 | local ConstantsMapping = {
18 | [Enum.DataStoreRequestType.GetAsync] = Constants.BUDGET_GETASYNC;
19 | [Enum.DataStoreRequestType.GetSortedAsync] = Constants.BUDGET_GETSORTEDASYNC;
20 | [Enum.DataStoreRequestType.OnUpdate] = Constants.BUDGET_ONUPDATE;
21 | [Enum.DataStoreRequestType.SetIncrementAsync] = Constants.BUDGET_SETINCREMENTASYNC;
22 | [Enum.DataStoreRequestType.SetIncrementSortedAsync] = Constants.BUDGET_SETINCREMENTSORTEDASYNC;
23 | }
24 |
25 | -- Bookkeeping of all data:
26 | local Data = {
27 | GlobalDataStore = {};
28 | DataStore = {};
29 | OrderedDataStore = {};
30 | }
31 |
32 | -- Bookkeeping of all active GlobalDataStore/OrderedDataStore interfaces indexed by data table:
33 | local Interfaces = {}
34 |
35 | -- Request limit bookkeeping:
36 | local Budgets = {}
37 |
38 | local budgetRequestQueues = {
39 | [Enum.DataStoreRequestType.GetAsync] = {};
40 | [Enum.DataStoreRequestType.GetSortedAsync] = {};
41 | [Enum.DataStoreRequestType.OnUpdate] = {};
42 | [Enum.DataStoreRequestType.SetIncrementAsync] = {};
43 | [Enum.DataStoreRequestType.SetIncrementSortedAsync] = {};
44 | }
45 |
46 | local function initBudget()
47 | for requestType, const in pairs(ConstantsMapping) do
48 | Budgets[requestType] = const.START
49 | end
50 | Budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
51 | Budgets[Enum.DataStoreRequestType.GetAsync],
52 | Budgets[Enum.DataStoreRequestType.SetIncrementAsync]
53 | )
54 | end
55 |
56 | local function updateBudget(req, const, dt, n)
57 | if not Constants.BUDGETING_ENABLED then
58 | return
59 | end
60 | local rate = const.RATE + n * const.RATE_PLR
61 | Budgets[req] = math.min(
62 | Budgets[req] + dt * rate,
63 | const.MAX_FACTOR * rate
64 | )
65 | end
66 |
67 | local function stealBudget(budget)
68 | if not Constants.BUDGETING_ENABLED then
69 | return
70 | end
71 | for _, requestType in pairs(budget) do
72 | if Budgets[requestType] then
73 | Budgets[requestType] = math.max(0, Budgets[requestType] - 1)
74 | end
75 | end
76 | Budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
77 | Budgets[Enum.DataStoreRequestType.GetAsync],
78 | Budgets[Enum.DataStoreRequestType.SetIncrementAsync]
79 | )
80 | end
81 |
82 | local function checkBudget(budget)
83 | if not Constants.BUDGETING_ENABLED then
84 | return true
85 | end
86 | for _, requestType in pairs(budget) do
87 | if Budgets[requestType] and Budgets[requestType] < 1 then
88 | return false
89 | end
90 | end
91 | return true
92 | end
93 |
94 | local isFrozen = false
95 |
96 | if RunService:IsServer() then
97 | -- Only do budget/throttle updating on server (in case package required on client)
98 |
99 | initBudget()
100 |
101 | task.spawn(function() -- Thread that increases budgets and de-throttles requests periodically
102 | local lastCheck = tick()
103 | while task.wait(Constants.BUDGET_UPDATE_INTERVAL) do
104 | local now = tick()
105 | local dt = (now - lastCheck) / 60
106 | lastCheck = now
107 | local n = #Players:GetPlayers()
108 |
109 | if not isFrozen then
110 | for requestType, const in pairs(ConstantsMapping) do
111 | updateBudget(requestType, const, dt, n)
112 | end
113 | Budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
114 | Budgets[Enum.DataStoreRequestType.GetAsync],
115 | Budgets[Enum.DataStoreRequestType.SetIncrementAsync]
116 | )
117 | end
118 |
119 | for _, budgetRequestQueue in pairs(budgetRequestQueues) do
120 | for i = #budgetRequestQueue, 1, -1 do
121 | local request = budgetRequestQueue[i]
122 |
123 | local thread = request.Thread
124 | local budget = request.Budget
125 | local key = request.Key
126 | local lock = request.Lock
127 | local cache = request.Cache
128 |
129 | if not (lock and (lock[key] or tick() - (cache[key] or 0) < Constants.WRITE_COOLDOWN)) and checkBudget(budget) then
130 | table.remove(budgetRequestQueue, i)
131 | stealBudget(budget)
132 | coroutine.resume(thread)
133 | end
134 | end
135 | end
136 | end
137 | end)
138 |
139 | game:BindToClose(function()
140 | for requestType, const in pairs(ConstantsMapping) do
141 | Budgets[requestType] = math.max(
142 | Budgets[requestType],
143 | Constants.BUDGET_ONCLOSE_BASE * (const.RATE / Constants.BUDGET_BASE)
144 | )
145 | end
146 | Budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
147 | Budgets[Enum.DataStoreRequestType.GetAsync],
148 | Budgets[Enum.DataStoreRequestType.SetIncrementAsync]
149 | )
150 | end)
151 |
152 | end
153 |
154 | function MockDataStoreManager.GetGlobalData()
155 | return Data.GlobalDataStore
156 | end
157 |
158 | function MockDataStoreManager.GetData(name, scope)
159 | assert(type(name) == "string")
160 | assert(type(scope) == "string")
161 |
162 | if not Data.DataStore[name] then
163 | Data.DataStore[name] = {}
164 | end
165 | if not Data.DataStore[name][scope] then
166 | Data.DataStore[name][scope] = {}
167 | end
168 |
169 | return Data.DataStore[name][scope]
170 | end
171 |
172 | function MockDataStoreManager.GetOrderedData(name, scope)
173 | assert(type(name) == "string")
174 | assert(type(scope) == "string")
175 |
176 | if not Data.OrderedDataStore[name] then
177 | Data.OrderedDataStore[name] = {}
178 | end
179 | if not Data.OrderedDataStore[name][scope] then
180 | Data.OrderedDataStore[name][scope] = {}
181 | end
182 |
183 | return Data.OrderedDataStore[name][scope]
184 | end
185 |
186 | function MockDataStoreManager.GetDataInterface(data)
187 | return Interfaces[data]
188 | end
189 |
190 | function MockDataStoreManager.SetDataInterface(data, interface)
191 | assert(type(data) == "table")
192 | assert(type(interface) == "table")
193 |
194 | Interfaces[data] = interface
195 | end
196 |
197 | function MockDataStoreManager.GetBudget(requestType)
198 | if Constants.BUDGETING_ENABLED then
199 | return math.floor(Budgets[requestType] or 0)
200 | else
201 | return math.huge
202 | end
203 | end
204 |
205 | function MockDataStoreManager.SetBudget(requestType, budget)
206 | assert(type(budget) == "number")
207 | budget = math.max(budget, 0)
208 |
209 | if requestType == Enum.DataStoreRequestType.UpdateAsync then
210 | Budgets[Enum.DataStoreRequestType.SetIncrementAsync] = budget
211 | Budgets[Enum.DataStoreRequestType.GetAsync] = budget
212 | end
213 |
214 | if Budgets[requestType] then
215 | Budgets[requestType] = budget
216 | end
217 | end
218 |
219 | function MockDataStoreManager.ResetBudget()
220 | initBudget()
221 | end
222 |
223 | function MockDataStoreManager.FreezeBudgetUpdates()
224 | isFrozen = true
225 | end
226 |
227 | function MockDataStoreManager.ThawBudgetUpdates()
228 | isFrozen = false
229 | end
230 |
231 | function MockDataStoreManager.YieldForWriteLockAndBudget(callback, key, writeLock, writeCache, budget)
232 | assert(type(callback) == "function")
233 | assert(type(key) == "string")
234 | assert(type(writeLock) == "table")
235 | assert(type(writeCache) == "table")
236 | assert(#budget > 0)
237 |
238 | local mainRequestType = budget[1]
239 |
240 | if #budgetRequestQueues[mainRequestType] >= Constants.THROTTLE_QUEUE_SIZE then
241 | return false -- no room in throttle queue
242 | end
243 |
244 | callback() -- would i.e. trigger a warning in output
245 |
246 | table.insert(budgetRequestQueues[mainRequestType], 1, {
247 | Key = key;
248 | Lock = writeLock;
249 | Cache = writeCache;
250 | Thread = coroutine.running();
251 | Budget = budget;
252 | })
253 | coroutine.yield()
254 |
255 | return true
256 | end
257 |
258 | function MockDataStoreManager.YieldForBudget(callback, budget)
259 | assert(type(callback) == "function")
260 | assert(#budget > 0)
261 |
262 | local mainRequestType = budget[1]
263 |
264 | if checkBudget(budget) then
265 | stealBudget(budget)
266 | elseif #budgetRequestQueues[mainRequestType] >= Constants.THROTTLE_QUEUE_SIZE then
267 | return false -- no room in throttle queue
268 | else
269 | callback() -- would i.e. trigger a warning in output
270 |
271 | table.insert(budgetRequestQueues[mainRequestType], 1, {
272 | After = 0; -- no write lock
273 | Thread = coroutine.running();
274 | Budget = budget;
275 | })
276 | coroutine.yield()
277 | end
278 |
279 | return true
280 | end
281 |
282 | function MockDataStoreManager.ExportToJSON()
283 | local export = {}
284 |
285 | if next(Data.GlobalDataStore) ~= nil then -- GlobalDataStore not empty
286 | export.GlobalDataStore = Data.GlobalDataStore
287 | end
288 | export.DataStore = Utils.prepareDataStoresForExport(Data.DataStore) -- can be nil
289 | export.OrderedDataStore = Utils.prepareDataStoresForExport(Data.OrderedDataStore) -- can be nil
290 |
291 | return HttpService:JSONEncode(export)
292 | end
293 |
294 | -- Import into an entire datastore type:
295 | local function importDataStoresFromTable(origin, destination, warnFunc, methodName, prefix, isOrdered)
296 | for name, scopes in pairs(origin) do
297 | if type(name) ~= "string" then
298 | warnFunc(("%s: ignored %s > %q (name is not a string, but a %s)")
299 | :format(methodName, prefix, tostring(name), typeof(name)))
300 | elseif type(scopes) ~= "table" then
301 | warnFunc(("%s: ignored %s > %q (scope list is not a table, but a %s)")
302 | :format(methodName, prefix, name, typeof(scopes)))
303 | elseif #name == 0 then
304 | warnFunc(("%s: ignored %s > %q (name is an empty string)")
305 | :format(methodName, prefix, name))
306 | elseif #name > Constants.MAX_LENGTH_NAME then
307 | warnFunc(("%s: ignored %s > %q (name exceeds %d character limit)")
308 | :format(methodName, prefix, name, Constants.MAX_LENGTH_NAME))
309 | else
310 | for scope, data in pairs(scopes) do
311 | if type(scope) ~= "string" then
312 | warnFunc(("%s: ignored %s > %q > %q (scope is not a string, but a %s)")
313 | :format(methodName, prefix, name, tostring(scope), typeof(scope)))
314 | elseif type(data) ~= "table" then
315 | warnFunc(("%s: ignored %s > %q > %q (data list is not a table, but a %s)")
316 | :format(methodName, prefix, name, scope, typeof(data)))
317 | elseif #scope == 0 then
318 | warnFunc(("%s: ignored %s > %q > %q (scope is an empty string)")
319 | :format(methodName, prefix, name, scope))
320 | elseif #scope > Constants.MAX_LENGTH_SCOPE then
321 | warnFunc(("%s: ignored %s > %q > %q (scope exceeds %d character limit)")
322 | :format(methodName, prefix, name, scope, Constants.MAX_LENGTH_SCOPE))
323 | else
324 | if not destination[name] then
325 | destination[name] = {}
326 | end
327 | if not destination[name][scope] then
328 | destination[name][scope] = {}
329 | end
330 | Utils.importPairsFromTable(
331 | data,
332 | destination[name][scope],
333 | Interfaces[destination[name][scope]],
334 | warnFunc,
335 | methodName,
336 | ("%s > %q > %q"):format(prefix, name, scope),
337 | isOrdered
338 | )
339 | end
340 | end
341 | end
342 | end
343 | end
344 |
345 | function MockDataStoreManager.ImportFromJSON(content, verbose)
346 | assert(type(content) == "table")
347 | assert(verbose == nil or type(verbose) == "boolean")
348 |
349 | local warnFunc = warn -- assume verbose as default
350 | if verbose == false then -- intentional formatting
351 | warnFunc = function() end
352 | end
353 |
354 | if type(content.GlobalDataStore) == "table" then
355 | Utils.importPairsFromTable(
356 | content.GlobalDataStore,
357 | Data.GlobalDataStore,
358 | Interfaces[Data.GlobalDataStore],
359 | warnFunc,
360 | "ImportFromJSON",
361 | "GlobalDataStore",
362 | false
363 | )
364 | end
365 | if type(content.DataStore) == "table" then
366 | importDataStoresFromTable(
367 | content.DataStore,
368 | Data.DataStore,
369 | warnFunc,
370 | "ImportFromJSON",
371 | "DataStore",
372 | false
373 | )
374 | end
375 | if type(content.OrderedDataStore) == "table" then
376 | importDataStoresFromTable(
377 | content.OrderedDataStore,
378 | Data.OrderedDataStore,
379 | warnFunc,
380 | "ImportFromJSON",
381 | "OrderedDataStore",
382 | true
383 | )
384 | end
385 | end
386 |
387 | local function clearTable(t)
388 | for i,_ in pairs(t) do
389 | t[i] = nil
390 | end
391 | end
392 |
393 | function MockDataStoreManager.ResetData()
394 | for _, interface in pairs(Interfaces) do
395 | for key, _ in pairs(interface.__data) do
396 | interface.__data[key] = nil
397 | interface.__event:Fire(key, nil)
398 | end
399 | interface.__getCache = {}
400 | interface.__writeCache = {}
401 | interface.__writeLock = {}
402 | if interface.__sorted then
403 | interface.__sorted = {};
404 | interface.__ref = {};
405 | interface.__changed = false;
406 | end
407 | end
408 |
409 | clearTable(Data.GlobalDataStore)
410 |
411 | for _, scopes in pairs(Data.DataStore) do
412 | for _, data in pairs(scopes) do
413 | clearTable(data)
414 | end
415 | end
416 |
417 | for _, scopes in pairs(Data.OrderedDataStore) do
418 | for _, data in pairs(scopes) do
419 | clearTable(data)
420 | end
421 | end
422 | end
423 |
424 | return MockDataStoreManager
425 |
--------------------------------------------------------------------------------
/lib/MockDataStoreService/MockDataStorePages.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | MockDataStorePages.lua
3 | This module implements the API and functionality of Roblox's DataStorePages class.
4 |
5 | This module is licensed under APLv2, refer to the LICENSE file or:
6 | https://github.com/buildthomas/MockDataStoreService/blob/master/LICENSE
7 | ]]
8 |
9 | local MockDataStorePages = {}
10 | MockDataStorePages.__index = MockDataStorePages
11 |
12 | local MockDataStoreManager = require(script.Parent.MockDataStoreManager)
13 | local Utils = require(script.Parent.MockDataStoreUtils)
14 |
15 | function MockDataStorePages:GetCurrentPage()
16 | local retValue = {}
17 |
18 | local minimumIndex = math.max(1, (self.__currentPage - 1) * self.__pageSize + 1)
19 | local maximumIndex = math.min(self.__currentPage * self.__pageSize, #self.__results)
20 | for i = minimumIndex, maximumIndex do
21 | table.insert(retValue, {key = self.__results[i].key, value = self.__results[i].value})
22 | end
23 |
24 | return retValue
25 | end
26 |
27 | function MockDataStorePages:AdvanceToNextPageAsync()
28 | if self.IsFinished then
29 | error("AdvanceToNextPageAsync rejected with error (no pages to advance to)", 2)
30 | end
31 |
32 | Utils.simulateErrorCheck("AdvanceToNextPageAsync")
33 |
34 | local success = MockDataStoreManager.YieldForBudget(
35 | function()
36 | warn("AdvanceToNextPageAsync request was throttled due to lack of budget. Try sending fewer requests.")
37 | end,
38 | {Enum.DataStoreRequestType.GetAsync}
39 | )
40 |
41 | if not success then
42 | error("AdvanceToNextPageAsync rejected with error (request was throttled, but throttled queue was full)", 2)
43 | end
44 |
45 | Utils.simulateYield()
46 |
47 | if #self.__results > self.__currentPage * self.__pageSize then
48 | self.__currentPage = self.__currentPage + 1
49 | end
50 | self.IsFinished = #self.__results <= self.__currentPage * self.__pageSize
51 |
52 | Utils.logMethod(self.__datastore, "AdvanceToNextPageAsync")
53 |
54 | end
55 |
56 | return MockDataStorePages
57 |
--------------------------------------------------------------------------------
/lib/MockDataStoreService/MockDataStoreUtils.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | MockDataStoreUtils.lua
3 | Contains helper and utility functions used by other classes.
4 |
5 | This module is licensed under APLv2, refer to the LICENSE file or:
6 | https://github.com/buildthomas/MockDataStoreService/blob/master/LICENSE
7 | ]]
8 |
9 | local MockDataStoreUtils = {}
10 |
11 | local Constants = require(script.Parent.MockDataStoreConstants)
12 | local HttpService = game:GetService("HttpService") -- for json encode/decode
13 | local RunService = game:GetService("RunService")
14 |
15 | local rand = Random.new()
16 |
17 | local function shorten(s, num)
18 | if #s > num then
19 | return s:sub(1,num-2) .. ".."
20 | end
21 | return s
22 | end
23 |
24 | --[[
25 | [DataStore] [Name/Scope] [GetAsync] KEY
26 | [DataStore] [Name/Scope] [UpdateAsync] KEY => VALUE
27 | [DataStore] [Name/Scope] [SetAsync] KEY => VALUE
28 | [DataStore] [Name/Scope] [IncrementAsync] KEY by INCR => VALUE
29 | [DataStore] [Name/Scope] [RemoveAsync] KEY =/> VALUE
30 | [DataStore] [Name/Scope] [OnUpdate] KEY
31 | [DataStore] [Name/Scope] [GetSortedAsync]
32 |
33 | [OrderedDataStore] [Name/Scope] [GetAsync] KEY
34 | [OrderedDataStore] [Name/Scope] [UpdateAsync] KEY => VALUE
35 | [OrderedDataStore] [Name/Scope] [SetAsync] KEY => VALUE
36 | [OrderedDataStore] [Name/Scope] [IncrementAsync] KEY + INCR => VALUE
37 | [OrderedDataStore] [Name/Scope] [RemoveAsync] KEY =/> VALUE
38 | [OrderedDataStore] [Name/Scope] [OnUpdate] KEY
39 | [OrderedDataStore] [Name/Scope] [GetSortedAsync]
40 |
41 | [OrderedDataStore] [Name/Scope] [AdvanceToNextPageAsync]
42 | ]]
43 |
44 | local function logMethod(self, method, key, value, increment)
45 | if not Constants.LOGGING_ENABLED or type(Constants.LOGGING_FUNCTION) ~= "function" then
46 | return
47 | end
48 |
49 | local name = self.__name
50 | local scope = self.__scope
51 |
52 | local prefix
53 | if not name then
54 | prefix = ("[GlobalDataStore] [%s]"):format(method)
55 | elseif not scope then
56 | prefix = ("[%s] [%s] [%s]"):format(self.__type, shorten(name, 20), method)
57 | else
58 | prefix = ("[%s] [%s/%s] [%s]"):format(self.__type, shorten(name, 15), shorten(scope, 15), method)
59 | end
60 |
61 | local message
62 | if value and increment then
63 | message = key .. " + " .. tostring(increment) .. " => " .. tostring(value)
64 | elseif increment then
65 | message = key .. " + " .. tostring(increment)
66 | elseif value then
67 | if method == "RemoveAsync" then
68 | message = key .. " =/> " .. tostring(value)
69 | else
70 | message = key .. " => " .. tostring(value)
71 | end
72 | else
73 | message = "key"
74 | end
75 |
76 | Constants.LOGGING_FUNCTION(prefix .. " " .. message)
77 |
78 | end
79 |
80 | local function deepcopy(t)
81 | if type(t) == "table" then
82 | local n = {}
83 | for i,v in pairs(t) do
84 | n[i] = deepcopy(v)
85 | end
86 | return n
87 | else
88 | return t
89 | end
90 | end
91 |
92 | local function scanValidity(tbl, passed, path) -- Credit to Corecii (edited)
93 | if type(tbl) ~= "table" then
94 | return scanValidity({input = tbl}, {}, {})
95 | end
96 | passed, path = passed or {}, path or {"root"}
97 | passed[tbl] = true
98 | local tblType
99 | do
100 | local key = next(tbl)
101 | if type(key) == "number" then
102 | tblType = "Array"
103 | else
104 | tblType = "Dictionary"
105 | end
106 | end
107 | local last = 0
108 | for key, value in next, tbl do
109 | path[#path + 1] = tostring(key)
110 | if type(key) == "number" then
111 | if tblType == "Dictionary" then
112 | return false, path, "cannot store mixed tables"
113 | elseif key % 1 ~= 0 then
114 | return false, path, "cannot store tables with non-integer indices"
115 | elseif key == math.huge or key == -math.huge then
116 | return false, path, "cannot store tables with (-)infinity indices"
117 | end
118 | elseif type(key) ~= "string" then
119 | return false, path, "dictionaries cannot have keys of type " .. typeof(key)
120 | elseif tblType == "Array" then
121 | return false, path, "cannot store mixed tables"
122 | elseif not utf8.len(key) then
123 | return false, path, "dictionary has key that is invalid UTF-8"
124 | end
125 | if tblType == "Array" then
126 | if last ~= key - 1 then
127 | return false, path, "array has non-sequential indices"
128 | end
129 | last = key
130 | end
131 | if type(value) == "userdata" or type(value) == "function" or type(value) == "thread" then
132 | return false, path, "cannot store value '" .. tostring(value) .. "' of type " .. typeof(value)
133 | elseif type(value) == "string" and not utf8.len(value) then
134 | return false, path, "cannot store strings that are invalid UTF-8"
135 | end
136 | if type(value) == "table" then
137 | if passed[value] then
138 | return false, path, "cannot store cyclic tables"
139 | end
140 | local isValid, keyPath, reason = scanValidity(value, passed, path)
141 | if not isValid then
142 | return isValid, keyPath, reason
143 | end
144 | end
145 | path[#path] = nil
146 | end
147 | passed[tbl] = nil
148 | return true
149 | end
150 |
151 | local function getStringPath(path)
152 | return table.concat(path, '.')
153 | end
154 |
155 | -- Import into a single datastore:
156 | local function importPairsFromTable(origin, destination, interface, warnFunc, methodName, prefix, isOrdered)
157 | for key, value in pairs(origin) do
158 | if type(key) ~= "string" then
159 | warnFunc(("%s: ignored %s > '%s' (key is not a string, but a %s)")
160 | :format(methodName, prefix, tostring(key), typeof(key)))
161 | elseif not utf8.len(key) then
162 | warnFunc(("%s: ignored %s > '%s' (key is not valid UTF-8)")
163 | :format(methodName, prefix, tostring(key)))
164 | elseif #key > Constants.MAX_LENGTH_KEY then
165 | warnFunc(("%s: ignored %s > '%s' (key exceeds %d character limit)")
166 | :format(methodName, prefix, key, Constants.MAX_LENGTH_KEY))
167 | elseif type(value) == "string" and #value > Constants.MAX_LENGTH_DATA then
168 | warnFunc(("%s: ignored %s > '%s' (length of value exceeds %d character limit)")
169 | :format(methodName, prefix, key, Constants.MAX_LENGTH_DATA))
170 | elseif type(value) == "table" and #HttpService:JSONEncode(value) > Constants.MAX_LENGTH_DATA then
171 | warnFunc(("%s: ignored %s > '%s' (length of encoded value exceeds %d character limit)")
172 | :format(methodName, prefix, key, Constants.MAX_LENGTH_DATA))
173 | elseif type(value) == "function" or type(value) == "userdata" or type(value) == "thread" then
174 | warnFunc(("%s: ignored %s > '%s' (cannot store value '%s' of type %s)")
175 | :format(methodName, prefix, key, tostring(value), type(value)))
176 | elseif isOrdered and type(value) ~= "number" then
177 | warnFunc(("%s: ignored %s > '%s' (cannot store value '%s' of type %s in OrderedDataStore)")
178 | :format(methodName, prefix, key, tostring(value), type(value)))
179 | elseif isOrdered and value % 1 ~= 0 then
180 | warnFunc(("%s: ignored %s > '%s' (cannot store non-integer value '%s' in OrderedDataStore)")
181 | :format(methodName, prefix, key, tostring(value)))
182 | elseif type(value) == "string" and not utf8.len(value) then
183 | warnFunc(("%s: ignored %s > '%s' (string value is not valid UTF-8)")
184 | :format(methodName, prefix, key, tostring(value), type(value)))
185 | else
186 | local isValid = true
187 | local keyPath, reason
188 | if type(value) == "table" then
189 | isValid, keyPath, reason = scanValidity(value)
190 | end
191 | if isOrdered then
192 | value = math.floor(value + .5)
193 | end
194 | if isValid then
195 | local old = destination[key]
196 | destination[key] = value
197 | if interface and old ~= value then -- hacky block to fire OnUpdate signals
198 | if isOrdered and interface then -- hacky block to populate internal structures for OrderedDataStores
199 | if interface.__ref[key] then
200 | interface.__ref[key].Value = value
201 | interface.__changed = true
202 | else
203 | interface.__ref[key] = {Key = key, Value = interface.__data[key]}
204 | table.insert(interface.__sorted, interface.__ref[key])
205 | interface.__changed = true
206 | end
207 | end
208 | interface.__event:Fire(key, value)
209 | end
210 | else
211 | warnFunc(("%s: ignored %s > '%s' (table has invalid entry at <%s>: %s)")
212 | :format(methodName, prefix, key, getStringPath(keyPath), reason))
213 | end
214 | end
215 | end
216 | end
217 |
218 | -- Trim empty datastores and scopes from an entire datastore type:
219 | local function prepareDataStoresForExport(origin)
220 | local dataPrepared = {}
221 |
222 | for name, scopes in pairs(origin) do
223 | local exportScopes = {}
224 | for scope, data in pairs(scopes) do
225 | local exportData = {}
226 | for key, value in pairs(data) do
227 | exportData[key] = value
228 | end
229 | if next(exportData) ~= nil then -- Only export datastore when non-empty
230 | exportScopes[scope] = exportData
231 | end
232 | end
233 | if next(exportScopes) ~= nil then -- Only export scope list when non-empty
234 | dataPrepared[name] = exportScopes
235 | end
236 | end
237 |
238 | if next(dataPrepared) ~= nil then -- Only return datastore type when non-empty
239 | return dataPrepared
240 | end
241 | end
242 |
243 | local function preprocessKey(key)
244 | if type(key) == "number" then
245 | if key ~= key then
246 | return "NAN"
247 | elseif key >= math.huge then
248 | return "INF"
249 | elseif key <= -math.huge then
250 | return "-INF"
251 | end
252 | return tostring(key)
253 | end
254 | return key
255 | end
256 |
257 | local function simulateYield()
258 | if Constants.YIELD_TIME_MAX > 0 then
259 | task.wait(rand:NextNumber(Constants.YIELD_TIME_MIN, Constants.YIELD_TIME_MAX))
260 | end
261 | end
262 |
263 | local function simulateErrorCheck(method)
264 | if Constants.SIMULATE_ERROR_RATE > 0 and rand:NextNumber() <= Constants.SIMULATE_ERROR_RATE then
265 | simulateYield()
266 | error(method .. " rejected with error (simulated error)", 3)
267 | end
268 | end
269 |
270 | -- Setting these here so the functions above can self-reference just by name:
271 | MockDataStoreUtils.logMethod = logMethod
272 | MockDataStoreUtils.deepcopy = deepcopy
273 | MockDataStoreUtils.scanValidity = scanValidity
274 | MockDataStoreUtils.getStringPath = getStringPath
275 | MockDataStoreUtils.importPairsFromTable = importPairsFromTable
276 | MockDataStoreUtils.prepareDataStoresForExport = prepareDataStoresForExport
277 | MockDataStoreUtils.preprocessKey = preprocessKey
278 | MockDataStoreUtils.simulateYield = simulateYield
279 | MockDataStoreUtils.simulateErrorCheck = simulateErrorCheck
280 |
281 | return MockDataStoreUtils
282 |
--------------------------------------------------------------------------------
/lib/MockDataStoreService/MockGlobalDataStore.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | MockGlobalDataStore.lua
3 | This module implements the API and functionality of Roblox's GlobalDataStore class.
4 |
5 | This module is licensed under APLv2, refer to the LICENSE file or:
6 | https://github.com/buildthomas/MockDataStoreService/blob/master/LICENSE
7 | ]]
8 |
9 | local MockGlobalDataStore = {}
10 | MockGlobalDataStore.__index = MockGlobalDataStore
11 |
12 | local MockDataStoreManager = require(script.Parent.MockDataStoreManager)
13 | local Utils = require(script.Parent.MockDataStoreUtils)
14 | local Constants = require(script.Parent.MockDataStoreConstants)
15 | local HttpService = game:GetService("HttpService") -- for json encode/decode
16 |
17 | local rand = Random.new()
18 |
19 | function MockGlobalDataStore:OnUpdate(key, callback)
20 | key = Utils.preprocessKey(key)
21 | if type(key) ~= "string" then
22 | error(("bad argument #1 to 'OnUpdate' (string expected, got %s)"):format(typeof(key)), 2)
23 | elseif type(callback) ~= "function" then
24 | error(("bad argument #2 to 'OnUpdate' (function expected, got %s)"):format(typeof(callback)), 2)
25 | elseif #key == 0 then
26 | error("bad argument #1 to 'OnUpdate' (key name can't be empty)", 2)
27 | elseif #key > Constants.MAX_LENGTH_KEY then
28 | error(("bad argument #1 to 'OnUpdate' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
29 | end
30 |
31 | Utils.simulateErrorCheck("OnUpdate")
32 |
33 | local success = MockDataStoreManager.YieldForBudget(
34 | function()
35 | warn(("OnUpdate request was throttled due to lack of budget. Try sending fewer requests. Key = %s"):format(key))
36 | end,
37 | {Enum.DataStoreRequestType.OnUpdate}
38 | )
39 |
40 | if not success then
41 | error("OnUpdate rejected with error (request was throttled, but throttled queue was full)", 2)
42 | end
43 |
44 | Utils.logMethod(self, "OnUpdate", key)
45 |
46 | return self.__event.Event:Connect(function(k, v)
47 | if k == key then
48 | if Constants.YIELD_TIME_UPDATE_MAX > 0 then
49 | task.wait(rand:NextNumber(Constants.YIELD_TIME_UPDATE_MIN, Constants.YIELD_TIME_UPDATE_MAX))
50 | end
51 | callback(v) -- v was implicitly deep-copied
52 | end
53 | end)
54 | end
55 |
56 | function MockGlobalDataStore:GetAsync(key)
57 | key = Utils.preprocessKey(key)
58 | if type(key) ~= "string" then
59 | error(("bad argument #1 to 'GetAsync' (string expected, got %s)"):format(typeof(key)), 2)
60 | elseif #key == 0 then
61 | error("bad argument #1 to 'GetAsync' (key name can't be empty)", 2)
62 | elseif #key > Constants.MAX_LENGTH_KEY then
63 | error(("bad argument #1 to 'GetAsync' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
64 | end
65 |
66 | if self.__getCache[key] and tick() - self.__getCache[key] < Constants.GET_COOLDOWN then
67 | return Utils.deepcopy(self.__data[key])
68 | end
69 |
70 | Utils.simulateErrorCheck("GetAsync")
71 |
72 | local success = MockDataStoreManager.YieldForBudget(
73 | function()
74 | warn(("GetAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s"):format(key))
75 | end,
76 | {Enum.DataStoreRequestType.GetAsync}
77 | )
78 |
79 | if not success then
80 | error("GetAsync rejected with error (request was throttled, but throttled queue was full)", 2)
81 | end
82 |
83 | self.__getCache[key] = tick()
84 |
85 | local retValue = Utils.deepcopy(self.__data[key])
86 |
87 | Utils.simulateYield()
88 |
89 | Utils.logMethod(self, "GetAsync", key)
90 |
91 | return retValue
92 | end
93 |
94 | function MockGlobalDataStore:IncrementAsync(key, delta)
95 | key = Utils.preprocessKey(key)
96 | if type(key) ~= "string" then
97 | error(("bad argument #1 to 'IncrementAsync' (string expected, got %s)"):format(typeof(key)), 2)
98 | elseif delta ~= nil and type(delta) ~= "number" then
99 | error(("bad argument #2 to 'IncrementAsync' (number expected, got %s)"):format(typeof(delta)), 2)
100 | elseif #key == 0 then
101 | error("bad argument #1 to 'IncrementAsync' (key name can't be empty)", 2)
102 | elseif #key > Constants.MAX_LENGTH_KEY then
103 | error(("bad argument #1 to 'IncrementAsync' (key name exceeds %d character limit)")
104 | :format(Constants.MAX_LENGTH_KEY), 2)
105 | end
106 |
107 | Utils.simulateErrorCheck("IncrementAsync")
108 |
109 | local success
110 |
111 | if self.__writeLock[key] or tick() - (self.__writeCache[key] or 0) < Constants.WRITE_COOLDOWN then
112 | success = MockDataStoreManager.YieldForWriteLockAndBudget(
113 | function()
114 | warn(("IncrementAsync request was throttled, a key can only be written to once every %d seconds. Key = %s")
115 | :format(Constants.WRITE_COOLDOWN, key))
116 | end,
117 | key,
118 | self.__writeLock,
119 | self.__writeCache,
120 | {Enum.DataStoreRequestType.SetIncrementAsync}
121 | )
122 | else
123 | self.__writeLock[key] = true
124 | success = MockDataStoreManager.YieldForBudget(
125 | function()
126 | warn(("IncrementAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s")
127 | :format(key))
128 | end,
129 | {Enum.DataStoreRequestType.SetIncrementAsync}
130 | )
131 | self.__writeLock[key] = nil
132 | end
133 |
134 | if not success then
135 | error("IncrementAsync rejected with error (request was throttled, but throttled queue was full)", 2)
136 | end
137 |
138 | local old = self.__data[key]
139 |
140 | if old ~= nil and (type(old) ~= "number" or old % 1 ~= 0) then
141 | Utils.simulateYield()
142 | error("IncrementAsync rejected with error (cannot increment non-integer value)", 2)
143 | end
144 |
145 | self.__writeLock[key] = true
146 |
147 | delta = delta and math.floor(delta + .5) or 1
148 |
149 | self.__data[key] = (old or 0) + delta
150 |
151 | if old == nil or delta ~= 0 then
152 | self.__event:Fire(key, self.__data[key])
153 | end
154 |
155 | local retValue = self.__data[key]
156 |
157 | Utils.simulateYield()
158 |
159 | self.__writeLock[key] = nil
160 | self.__writeCache[key] = tick()
161 |
162 | self.__getCache[key] = tick()
163 |
164 | Utils.logMethod(self, "IncrementAsync", key, retValue, delta)
165 |
166 | return retValue
167 | end
168 |
169 | function MockGlobalDataStore:RemoveAsync(key)
170 | key = Utils.preprocessKey(key)
171 | if type(key) ~= "string" then
172 | error(("bad argument #1 to 'RemoveAsync' (string expected, got %s)"):format(typeof(key)), 2)
173 | elseif #key == 0 then
174 | error("bad argument #1 to 'RemoveAsync' (key name can't be empty)", 2)
175 | elseif #key > Constants.MAX_LENGTH_KEY then
176 | error(("bad argument #1 to 'RemoveAsync' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
177 | end
178 |
179 | Utils.simulateErrorCheck("RemoveAsync")
180 |
181 | local success
182 |
183 | if self.__writeLock[key] or tick() - (self.__writeCache[key] or 0) < Constants.WRITE_COOLDOWN then
184 | success = MockDataStoreManager.YieldForWriteLockAndBudget(
185 | function()
186 | warn(("RemoveAsync request was throttled, a key can only be written to once every %d seconds. Key = %s")
187 | :format(Constants.WRITE_COOLDOWN, key))
188 | end,
189 | key,
190 | self.__writeLock,
191 | self.__writeCache,
192 | {Enum.DataStoreRequestType.SetIncrementAsync}
193 | )
194 | else
195 | self.__writeLock[key] = true
196 | success = MockDataStoreManager.YieldForBudget(
197 | function()
198 | warn(("RemoveAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s")
199 | :format(key))
200 | end,
201 | {Enum.DataStoreRequestType.SetIncrementAsync}
202 | )
203 | self.__writeLock[key] = nil
204 | end
205 |
206 | if not success then
207 | error("RemoveAsync rejected with error (request was throttled, but throttled queue was full)", 2)
208 | end
209 |
210 | self.__writeLock[key] = true
211 |
212 | local value = Utils.deepcopy(self.__data[key])
213 | self.__data[key] = nil
214 |
215 | if value ~= nil then
216 | self.__event:Fire(key, nil)
217 | end
218 |
219 | Utils.simulateYield()
220 |
221 | self.__writeLock[key] = nil
222 | self.__writeCache[key] = tick()
223 |
224 | Utils.logMethod(self, "RemoveAsync", key, value)
225 |
226 | return value
227 | end
228 |
229 | function MockGlobalDataStore:SetAsync(key, value)
230 | key = Utils.preprocessKey(key)
231 | if type(key) ~= "string" then
232 | error(("bad argument #1 to 'SetAsync' (string expected, got %s)"):format(typeof(key)), 2)
233 | elseif #key == 0 then
234 | error("bad argument #1 to 'SetAsync' (key name can't be empty)", 2)
235 | elseif #key > Constants.MAX_LENGTH_KEY then
236 | error(("bad argument #1 to 'SetAsync' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
237 | elseif value == nil or type(value) == "function" or type(value) == "userdata" or type(value) == "thread" then
238 | error(("bad argument #2 to 'SetAsync' (cannot store value '%s' of type %s)")
239 | :format(tostring(value), typeof(value)), 2)
240 | end
241 |
242 | if type(value) == "table" then
243 | local isValid, keyPath, reason = Utils.scanValidity(value)
244 | if not isValid then
245 | error(("bad argument #2 to 'SetAsync' (table has invalid entry at <%s>: %s)")
246 | :format(Utils.getStringPath(keyPath), reason), 2)
247 | end
248 | local pass, content = pcall(function() return HttpService:JSONEncode(value) end)
249 | if not pass then
250 | error("bad argument #2 to 'SetAsync' (table could not be encoded to json)", 2)
251 | elseif #content > Constants.MAX_LENGTH_DATA then
252 | error(("bad argument #2 to 'SetAsync' (encoded data length exceeds %d character limit)")
253 | :format(Constants.MAX_LENGTH_DATA), 2)
254 | end
255 | elseif type(value) == "string" then
256 | if #value > Constants.MAX_LENGTH_DATA then
257 | error(("bad argument #2 to 'SetAsync' (data length exceeds %d character limit)")
258 | :format(Constants.MAX_LENGTH_DATA), 2)
259 | elseif not utf8.len(value) then
260 | error("bad argument #2 to 'SetAsync' (string value is not valid UTF-8)", 2)
261 | end
262 | end
263 |
264 | Utils.simulateErrorCheck("SetAsync")
265 |
266 | local success
267 |
268 | if self.__writeLock[key] or tick() - (self.__writeCache[key] or 0) < Constants.WRITE_COOLDOWN then
269 | success = MockDataStoreManager.YieldForWriteLockAndBudget(
270 | function()
271 | warn(("SetAsync request was throttled, a key can only be written to once every %d seconds. Key = %s")
272 | :format(Constants.WRITE_COOLDOWN, key))
273 | end,
274 | key,
275 | self.__writeLock,
276 | self.__writeCache,
277 | {Enum.DataStoreRequestType.SetIncrementAsync}
278 | )
279 | else
280 | self.__writeLock[key] = true
281 | success = MockDataStoreManager.YieldForBudget(
282 | function()
283 | warn(("SetAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s")
284 | :format(key))
285 | end,
286 | {Enum.DataStoreRequestType.SetIncrementAsync}
287 | )
288 | self.__writeLock[key] = nil
289 | end
290 |
291 | if not success then
292 | error("SetAsync rejected with error (request was throttled, but throttled queue was full)", 2)
293 | end
294 |
295 | self.__writeLock[key] = true
296 |
297 | if type(value) == "table" or value ~= self.__data[key] then
298 | self.__data[key] = Utils.deepcopy(value)
299 | self.__event:Fire(key, self.__data[key])
300 | end
301 |
302 | Utils.simulateYield()
303 |
304 | self.__writeLock[key] = nil
305 | self.__writeCache[key] = tick()
306 |
307 | Utils.logMethod(self, "SetAsync", key, self.__data[key])
308 |
309 | end
310 |
311 | function MockGlobalDataStore:UpdateAsync(key, transformFunction)
312 | key = Utils.preprocessKey(key)
313 | if type(key) ~= "string" then
314 | error(("bad argument #1 to 'UpdateAsync' (string expected, got %s)"):format(typeof(key)), 2)
315 | elseif type(transformFunction) ~= "function" then
316 | error(("bad argument #2 to 'UpdateAsync' (function expected, got %s)"):format(typeof(transformFunction)), 2)
317 | elseif #key == 0 then
318 | error("bad argument #1 to 'UpdateAsync' (key name can't be empty)", 2)
319 | elseif #key > Constants.MAX_LENGTH_KEY then
320 | error(("bad argument #1 to 'UpdateAsync' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
321 | end
322 |
323 | Utils.simulateErrorCheck("UpdateAsync")
324 |
325 | local success
326 |
327 | if self.__writeLock[key] or tick() - (self.__writeCache[key] or 0) < Constants.WRITE_COOLDOWN then
328 | success = MockDataStoreManager.YieldForWriteLockAndBudget(
329 | function()
330 | warn(("UpdateAsync request was throttled, a key can only be written to once every %d seconds. Key = %s")
331 | :format(Constants.WRITE_COOLDOWN, key))
332 | end,
333 | key,
334 | self.__writeLock,
335 | self.__writeCache,
336 | {Enum.DataStoreRequestType.SetIncrementAsync}
337 | )
338 | else
339 | self.__writeLock[key] = true
340 | local budget
341 | if self.__getCache[key] and tick() - self.__getCache[key] < Constants.GET_COOLDOWN then
342 | budget = {Enum.DataStoreRequestType.SetIncrementAsync}
343 | else
344 | budget = {Enum.DataStoreRequestType.GetAsync, Enum.DataStoreRequestType.SetIncrementAsync}
345 | end
346 | success = MockDataStoreManager.YieldForBudget(
347 | function()
348 | warn(("UpdateAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s")
349 | :format(key))
350 | end,
351 | budget
352 | )
353 | self.__writeLock[key] = nil
354 | end
355 |
356 | if not success then
357 | error("UpdateAsync rejected with error (request was throttled, but throttled queue was full)", 2)
358 | end
359 |
360 | local value = transformFunction(Utils.deepcopy(self.__data[key]))
361 |
362 | if value == nil then -- cancel update after remote call
363 | Utils.simulateYield()
364 | return nil -- this is what datastores do even though it should be old value
365 | end
366 |
367 | if type(value) == "function" or type(value) == "userdata" or type(value) == "thread" then
368 | error(("UpdateAsync rejected with error (resulting value '%s' is of type %s that cannot be stored)")
369 | :format(tostring(value), typeof(value)), 2)
370 | end
371 |
372 | if type(value) == "table" then
373 | local isValid, keyPath, reason = Utils.scanValidity(value)
374 | if not isValid then
375 | error(("UpdateAsync rejected with error (resulting table has invalid entry at <%s>: %s)")
376 | :format(Utils.getStringPath(keyPath), reason), 2)
377 | end
378 | local pass, content = pcall(function() return HttpService:JSONEncode(value) end)
379 | if not pass then
380 | error("UpdateAsync rejected with error (resulting table could not be encoded to json)", 2)
381 | elseif #content > Constants.MAX_LENGTH_DATA then
382 | error(("UpdateAsync rejected with error (resulting encoded data length exceeds %d character limit)")
383 | :format(Constants.MAX_LENGTH_DATA), 2)
384 | end
385 | elseif type(value) == "string" then
386 | if #value > Constants.MAX_LENGTH_DATA then
387 | error(("UpdateAsync rejected with error (resulting data length exceeds %d character limit)")
388 | :format(Constants.MAX_LENGTH_DATA), 2)
389 | elseif not utf8.len(value) then
390 | error("UpdateAsync rejected with error (string value is not valid UTF-8)", 2)
391 | end
392 | end
393 |
394 | self.__writeLock[key] = true
395 |
396 | if type(value) == "table" or value ~= self.__data[key] then
397 | self.__data[key] = Utils.deepcopy(value)
398 | self.__event:Fire(key, self.__data[key])
399 | end
400 |
401 | local retValue = Utils.deepcopy(value)
402 |
403 | self.__writeLock[key] = nil
404 | self.__writeCache[key] = tick()
405 |
406 | self.__getCache[key] = tick()
407 |
408 | Utils.logMethod(self, "UpdateAsync", key, retValue)
409 |
410 | return retValue
411 | end
412 |
413 | function MockGlobalDataStore:ExportToJSON()
414 | return HttpService:JSONEncode(self.__data)
415 | end
416 |
417 | function MockGlobalDataStore:ImportFromJSON(json, verbose)
418 | local content
419 | if type(json) == "string" then
420 | local parsed, value = pcall(function() return HttpService:JSONDecode(json) end)
421 | if not parsed then
422 | error("bad argument #1 to 'ImportFromJSON' (string is not valid json)", 2)
423 | end
424 | content = value
425 | elseif type(json) == "table" then
426 | content = Utils.deepcopy(json)
427 | else
428 | error(("bad argument #1 to 'ImportFromJSON' (string or table expected, got %s)"):format(typeof(json)), 2)
429 | end
430 |
431 | if verbose ~= nil and type(verbose) ~= "boolean" then
432 | error(("bad argument #2 to 'ImportFromJSON' (boolean expected, got %s)"):format(typeof(verbose)), 2)
433 | end
434 |
435 | Utils.importPairsFromTable(
436 | content,
437 | self.__data,
438 | MockDataStoreManager.GetDataInterface(self.__data),
439 | (verbose == false and function() end or warn),
440 | "ImportFromJSON",
441 | ((type(self.__name) == "string" and type(self.__scope) == "string")
442 | and ("DataStore > %s > %s"):format(self.__name, self.__scope)
443 | or "GlobalDataStore"),
444 | false
445 | )
446 | end
447 |
448 | return MockGlobalDataStore
449 |
--------------------------------------------------------------------------------
/lib/MockDataStoreService/MockOrderedDataStore.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | MockOrderedDataStore.lua
3 | This module implements the API and functionality of Roblox's OrderedDataStore class.
4 |
5 | This module is licensed under APLv2, refer to the LICENSE file or:
6 | https://github.com/buildthomas/MockDataStoreService/blob/master/LICENSE
7 | ]]
8 |
9 | local MockOrderedDataStore = {}
10 | MockOrderedDataStore.__index = MockOrderedDataStore
11 |
12 | local MockDataStoreManager = require(script.Parent.MockDataStoreManager)
13 | local MockDataStorePages = require(script.Parent.MockDataStorePages)
14 | local Utils = require(script.Parent.MockDataStoreUtils)
15 | local Constants = require(script.Parent.MockDataStoreConstants)
16 | local HttpService = game:GetService("HttpService") -- for json encode/decode
17 |
18 | local rand = Random.new()
19 |
20 | function MockOrderedDataStore:OnUpdate(key, callback)
21 | key = Utils.preprocessKey(key)
22 | if type(key) ~= "string" then
23 | error(("bad argument #1 to 'OnUpdate' (string expected, got %s)"):format(typeof(key)), 2)
24 | elseif type(callback) ~= "function" then
25 | error(("bad argument #2 to 'OnUpdate' (function expected, got %s)"):format(typeof(callback)), 2)
26 | elseif #key == 0 then
27 | error("bad argument #1 to 'OnUpdate' (key name can't be empty)", 2)
28 | elseif #key > Constants.MAX_LENGTH_KEY then
29 | error(("bad argument #1 to 'OnUpdate' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
30 | end
31 |
32 | Utils.simulateErrorCheck("OnUpdate")
33 |
34 | local success = MockDataStoreManager.YieldForBudget(
35 | function()
36 | warn(("OnUpdate request was throttled due to lack of budget. Try sending fewer requests. Key = %s"):format(key))
37 | end,
38 | {Enum.DataStoreRequestType.OnUpdate}
39 | )
40 |
41 | if not success then
42 | error("OnUpdate rejected with error (request was throttled, but throttled queue was full)", 2)
43 | end
44 |
45 | Utils.logMethod(self, "OnUpdate", key)
46 |
47 | return self.__event.Event:Connect(function(k, v)
48 | if k == key then
49 | if Constants.YIELD_TIME_UPDATE_MAX > 0 then
50 | task.wait(rand:NextNumber(Constants.YIELD_TIME_UPDATE_MIN, Constants.YIELD_TIME_UPDATE_MAX))
51 | end
52 | callback(v) -- v was implicitly deep-copied
53 | end
54 | end)
55 | end
56 |
57 | function MockOrderedDataStore:GetAsync(key)
58 | key = Utils.preprocessKey(key)
59 | if type(key) ~= "string" then
60 | error(("bad argument #1 to 'GetAsync' (string expected, got %s)"):format(typeof(key)), 2)
61 | elseif #key == 0 then
62 | error("bad argument #1 to 'GetAsync' (key name can't be empty)", 2)
63 | elseif #key > Constants.MAX_LENGTH_KEY then
64 | error(("bad argument #1 to 'GetAsync' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
65 | end
66 |
67 | if self.__getCache[key] and tick() - self.__getCache[key] < Constants.GET_COOLDOWN then
68 | return self.__data[key]
69 | end
70 |
71 | Utils.simulateErrorCheck("GetAsync")
72 |
73 | local success = MockDataStoreManager.YieldForBudget(
74 | function()
75 | warn(("GetAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s"):format(key))
76 | end,
77 | {Enum.DataStoreRequestType.GetAsync}
78 | )
79 |
80 | if not success then
81 | error("GetAsync rejected with error (request was throttled, but throttled queue was full)", 2)
82 | end
83 |
84 | local retValue = self.__data[key]
85 |
86 | Utils.simulateYield()
87 |
88 | self.__getCache[key] = tick()
89 |
90 | Utils.logMethod(self, "GetAsync", key)
91 |
92 | return retValue
93 | end
94 |
95 | function MockOrderedDataStore:IncrementAsync(key, delta)
96 | key = Utils.preprocessKey(key)
97 | if type(key) ~= "string" then
98 | error(("bad argument #1 to 'IncrementAsync' (string expected, got %s)"):format(typeof(key)), 2)
99 | elseif delta ~= nil and type(delta) ~= "number" then
100 | error(("bad argument #2 to 'IncrementAsync' (number expected, got %s)"):format(typeof(delta)), 2)
101 | elseif #key == 0 then
102 | error("bad argument #1 to 'IncrementAsync' (key name can't be empty)", 2)
103 | elseif #key > Constants.MAX_LENGTH_KEY then
104 | error(("bad argument #1 to 'IncrementAsync' (key name exceeds %d character limit)")
105 | :format(Constants.MAX_LENGTH_KEY), 2)
106 | end
107 |
108 | Utils.simulateErrorCheck("IncrementAsync")
109 |
110 | local success
111 |
112 | if self.__writeLock[key] or tick() - (self.__writeCache[key] or 0) < Constants.WRITE_COOLDOWN then
113 | success = MockDataStoreManager.YieldForWriteLockAndBudget(
114 | function()
115 | warn(("IncrementAsync request was throttled, a key can only be written to once every %d seconds. Key = %s")
116 | :format(Constants.WRITE_COOLDOWN, key))
117 | end,
118 | key,
119 | self.__writeLock,
120 | self.__writeCache,
121 | {Enum.DataStoreRequestType.SetIncrementSortedAsync}
122 | )
123 | else
124 | self.__writeLock[key] = true
125 | success = MockDataStoreManager.YieldForBudget(
126 | function()
127 | warn(("IncrementAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s")
128 | :format(key))
129 | end,
130 | {Enum.DataStoreRequestType.SetIncrementSortedAsync}
131 | )
132 | self.__writeLock[key] = nil
133 | end
134 |
135 | if not success then
136 | error("IncrementAsync rejected with error (request was throttled, but throttled queue was full)", 2)
137 | end
138 |
139 | local old = self.__data[key]
140 |
141 | if old ~= nil and (type(old) ~= "number" or old % 1 ~= 0) then
142 | Utils.simulateYield()
143 | error("IncrementAsync rejected with error (cannot increment non-integer value)", 2)
144 | end
145 |
146 | self.__writeLock[key] = true
147 |
148 | delta = delta and math.floor(delta + .5) or 1
149 |
150 | if old == nil then
151 | self.__data[key] = delta
152 | self.__ref[key] = {Key = key, Value = self.__data[key]}
153 | table.insert(self.__sorted, self.__ref[key])
154 | self.__changed = true
155 | self.__event:Fire(key, self.__data[key])
156 | elseif delta ~= 0 then
157 | self.__data[key] = self.__data[key] + delta
158 | self.__ref[key].Value = self.__data[key]
159 | self.__changed = true
160 | self.__event:Fire(key, self.__data[key])
161 | end
162 |
163 | local retValue = self.__data[key]
164 |
165 | Utils.simulateYield()
166 |
167 | self.__writeLock[key] = nil
168 | self.__writeCache[key] = tick()
169 |
170 | self.__getCache[key] = tick()
171 |
172 | Utils.logMethod(self, "IncrementAsync", key, retValue, delta)
173 |
174 | return retValue
175 | end
176 |
177 | function MockOrderedDataStore:RemoveAsync(key)
178 | key = Utils.preprocessKey(key)
179 | if type(key) ~= "string" then
180 | error(("bad argument #1 to 'RemoveAsync' (string expected, got %s)"):format(type(key)), 2)
181 | elseif #key == 0 then
182 | error("bad argument #1 to 'RemoveAsync' (key name can't be empty)", 2)
183 | elseif #key > Constants.MAX_LENGTH_KEY then
184 | error(("bad argument #1 to 'RemoveAsync' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
185 | end
186 |
187 | Utils.simulateErrorCheck("RemoveAsync")
188 |
189 | local success
190 |
191 | if self.__writeLock[key] or tick() - (self.__writeCache[key] or 0) < Constants.WRITE_COOLDOWN then
192 | success = MockDataStoreManager.YieldForWriteLockAndBudget(
193 | function()
194 | warn(("RemoveAsync request was throttled, a key can only be written to once every %d seconds. Key = %s")
195 | :format(Constants.WRITE_COOLDOWN, key))
196 | end,
197 | key,
198 | self.__writeLock,
199 | self.__writeCache,
200 | {Enum.DataStoreRequestType.SetIncrementSortedAsync}
201 | )
202 | else
203 | self.__writeLock[key] = true
204 | success = MockDataStoreManager.YieldForBudget(
205 | function()
206 | warn(("RemoveAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s")
207 | :format(key))
208 | end,
209 | {Enum.DataStoreRequestType.SetIncrementSortedAsync}
210 | )
211 | self.__writeLock[key] = nil
212 | end
213 |
214 | if not success then
215 | error("RemoveAsync rejected with error (request was throttled, but throttled queue was full)", 2)
216 | end
217 |
218 | self.__writeLock[key] = true
219 |
220 | local value = self.__data[key]
221 |
222 | if value ~= nil then
223 | self.__data[key] = nil
224 | self.__ref[key] = nil
225 | for i,v in pairs(self.__sorted) do
226 | if v.Key == key then
227 | table.remove(self.__sorted, i)
228 | break
229 | end
230 | end
231 | self.__event:Fire(key, nil)
232 | end
233 |
234 | Utils.simulateYield()
235 |
236 | self.__writeLock[key] = nil
237 | self.__writeCache[key] = tick()
238 |
239 | Utils.logMethod(self, "RemoveAsync", key, value)
240 |
241 | return value
242 | end
243 |
244 | function MockOrderedDataStore:SetAsync(key, value)
245 | key = Utils.preprocessKey(key)
246 | if type(key) ~= "string" then
247 | error(("bad argument #1 to 'SetAsync' (string expected, got %s)"):format(typeof(key)), 2)
248 | elseif #key == 0 then
249 | error("bad argument #1 to 'SetAsync' (key name can't be empty)", 2)
250 | elseif #key > Constants.MAX_LENGTH_KEY then
251 | error(("bad argument #1 to 'SetAsync' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
252 | elseif type(value) ~= "number" then
253 | error(("bad argument #2 to 'SetAsync' (number expected, got %s)"):format(typeof(value)), 2)
254 | elseif value % 1 ~= 0 then
255 | error("bad argument #2 to 'SetAsync' (cannot store non-integer values in OrderedDataStore)", 2)
256 | end
257 |
258 | Utils.simulateErrorCheck("SetAsync")
259 |
260 | local success
261 |
262 | if self.__writeLock[key] or tick() - (self.__writeCache[key] or 0) < Constants.WRITE_COOLDOWN then
263 | success = MockDataStoreManager.YieldForWriteLockAndBudget(
264 | function()
265 | warn(("SetAsync request was throttled, a key can only be written to once every %d seconds. Key = %s")
266 | :format(Constants.WRITE_COOLDOWN, key))
267 | end,
268 | key,
269 | self.__writeLock,
270 | self.__writeCache,
271 | {Enum.DataStoreRequestType.SetIncrementSortedAsync}
272 | )
273 | else
274 | self.__writeLock[key] = true
275 | success = MockDataStoreManager.YieldForBudget(
276 | function()
277 | warn(("SetAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s")
278 | :format(key))
279 | end,
280 | {Enum.DataStoreRequestType.SetIncrementSortedAsync}
281 | )
282 | self.__writeLock[key] = nil
283 | end
284 |
285 | if not success then
286 | error("SetAsync rejected with error (request was throttled, but throttled queue was full)", 2)
287 | end
288 |
289 | self.__writeLock[key] = true
290 |
291 | local old = self.__data[key]
292 |
293 | if old == nil then
294 | self.__data[key] = value
295 | self.__ref[key] = {Key = key, Value = value}
296 | table.insert(self.__sorted, self.__ref[key])
297 | self.__changed = true
298 | self.__event:Fire(key, self.__data[key])
299 | elseif old ~= value then
300 | self.__data[key] = value
301 | self.__ref[key].Value = value
302 | self.__changed = true
303 | self.__event:Fire(key, self.__data[key])
304 | end
305 |
306 | Utils.simulateYield()
307 |
308 | self.__writeLock[key] = nil
309 | self.__writeCache[key] = tick()
310 |
311 | Utils.logMethod(self, "SetAsync", key, self.__data[key])
312 |
313 | return value
314 | end
315 |
316 | function MockOrderedDataStore:UpdateAsync(key, transformFunction)
317 | key = Utils.preprocessKey(key)
318 | if type(key) ~= "string" then
319 | error(("bad argument #1 to 'UpdateAsync' (string expected, got %s)"):format(typeof(key)), 2)
320 | elseif type(transformFunction) ~= "function" then
321 | error(("bad argument #2 to 'UpdateAsync' (function expected, got %s)"):format(typeof(transformFunction)), 2)
322 | elseif #key == 0 then
323 | error("bad argument #1 to 'UpdateAsync' (key name can't be empty)", 2)
324 | elseif #key > Constants.MAX_LENGTH_KEY then
325 | error(("bad argument #1 to 'UpdateAsync' (key name exceeds %d character limit)"):format(Constants.MAX_LENGTH_KEY), 2)
326 | end
327 |
328 | Utils.simulateErrorCheck("UpdateAsync")
329 |
330 | local success
331 |
332 | if self.__writeLock[key] or tick() - (self.__writeCache[key] or 0) < Constants.WRITE_COOLDOWN then
333 | success = MockDataStoreManager.YieldForWriteLockAndBudget(
334 | function()
335 | warn(("UpdateAsync request was throttled, a key can only be written to once every %d seconds. Key = %s")
336 | :format(Constants.WRITE_COOLDOWN, key))
337 | end,
338 | key,
339 | self.__writeLock,
340 | self.__writeCache,
341 | {Enum.DataStoreRequestType.SetIncrementSortedAsync}
342 | )
343 | else
344 | self.__writeLock[key] = true
345 | local budget
346 | if self.__getCache[key] and tick() - self.__getCache[key] < Constants.GET_COOLDOWN then
347 | budget = {Enum.DataStoreRequestType.SetIncrementSortedAsync}
348 | else
349 | budget = {Enum.DataStoreRequestType.GetAsync, Enum.DataStoreRequestType.SetIncrementSortedAsync}
350 | end
351 | success = MockDataStoreManager.YieldForBudget(
352 | function()
353 | warn(("UpdateAsync request was throttled due to lack of budget. Try sending fewer requests. Key = %s")
354 | :format(key))
355 | end,
356 | budget
357 | )
358 | self.__writeLock[key] = nil
359 | end
360 |
361 | if not success then
362 | error("UpdateAsync rejected with error (request was throttled, but throttled queue was full)", 2)
363 | end
364 |
365 | local value = transformFunction(self.__data[key])
366 |
367 | if value == nil then -- cancel update after remote call
368 | Utils.simulateYield()
369 | return nil -- this is what datastores do even though it should be old value
370 | end
371 |
372 | if type(value) ~= "number" or value % 1 ~= 0 then
373 | error("UpdateAsync rejected with error (resulting non-integer value can't be stored in OrderedDataStore)", 2)
374 | end
375 |
376 | self.__writeLock[key] = true
377 |
378 | local old = self.__data[key]
379 |
380 | if old == nil then
381 | self.__data[key] = value
382 | self.__ref[key] = {Key = key, Value = value}
383 | table.insert(self.__sorted, self.__ref[key])
384 | self.__changed = true
385 | self.__event:Fire(key, self.__data[key])
386 | elseif old ~= value then
387 | self.__data[key] = value
388 | self.__ref[key].Value = value
389 | self.__changed = true
390 | self.__event:Fire(key, self.__data[key])
391 | end
392 |
393 | Utils.simulateYield()
394 |
395 | self.__writeLock[key] = nil
396 | self.__writeCache[key] = tick()
397 |
398 | self.__getCache[key] = tick()
399 |
400 | Utils.logMethod(self, "UpdateAsync", key, value)
401 |
402 | return value
403 | end
404 |
405 | function MockOrderedDataStore:GetSortedAsync(ascending, pageSize, minValue, maxValue)
406 | if type(ascending) ~= "boolean" then
407 | error(("bad argument #1 to 'GetSortedAsync' (boolean expected, got %s)"):format(typeof(ascending)), 2)
408 | elseif type(pageSize) ~= "number" then
409 | error(("bad argument #2 to 'GetSortedAsync' (number expected, got %s)"):format(typeof(pageSize)), 2)
410 | end
411 |
412 | pageSize = math.floor(pageSize + .5)
413 | if pageSize <= 0 or pageSize > Constants.MAX_PAGE_SIZE then
414 | error(("bad argument #2 to 'GetSortedAsync' (page size must be an integer above 0 and below or equal to %d)")
415 | :format(Constants.MAX_PAGE_SIZE), 2)
416 | end
417 |
418 | if minValue ~= nil then
419 | if type(minValue) ~= "number" then
420 | error(("bad argument #3 to 'GetSortedAsync' (number expected, got %s)"):format(typeof(minValue)), 2)
421 | elseif minValue % 1 ~= 0 then
422 | error("bad argument #3 to 'GetSortedAsync' (minimum threshold must be an integer)", 2)
423 | end
424 | else
425 | minValue = -math.huge
426 | end
427 |
428 | if maxValue ~= nil then
429 | if type(maxValue) ~= "number" then
430 | error(("bad argument #4 to 'GetSortedAsync' (number expected, got %s)"):format(typeof(maxValue)), 2)
431 | elseif maxValue % 1 ~= 0 then
432 | error("bad argument #4 to 'GetSortedAsync' (maximum threshold must be an integer)", 2)
433 | end
434 | else
435 | maxValue = math.huge
436 | end
437 |
438 | Utils.simulateErrorCheck("GetSortedAsync")
439 |
440 | local success = MockDataStoreManager.YieldForBudget(
441 | function()
442 | warn("GetSortedAsync request was throttled due to lack of budget. Try sending fewer requests.")
443 | end,
444 | {Enum.DataStoreRequestType.GetSortedAsync}
445 | )
446 |
447 | if not success then
448 | error("GetSortedAsync rejected with error (request was throttled, but throttled queue was full)", 2)
449 | end
450 |
451 | if minValue > maxValue then
452 | Utils.simulateYield()
453 | error("GetSortedAsync rejected with error (minimum threshold is higher than maximum threshold)", 2)
454 | end
455 |
456 | if self.__changed then
457 | table.sort(self.__sorted, function(a,b) return a.Value < b.Value end)
458 | self.__changed = false
459 | end
460 |
461 | local results = {}
462 |
463 | if ascending then
464 | local i = 1
465 | while self.__sorted[i] and self.__sorted[i].Value < minValue do
466 | i = i + 1
467 | end
468 | while self.__sorted[i] and self.__sorted[i].Value <= maxValue do
469 | table.insert(results, {key = self.__sorted[i].Key, value = self.__sorted[i].Value})
470 | i = i + 1
471 | end
472 | else
473 | local i = #self.__sorted
474 | while i > 0 and self.__sorted[i].Value > maxValue do
475 | i = i - 1
476 | end
477 | while i > 0 and self.__sorted[i].Value >= minValue do
478 | table.insert(results, {key = self.__sorted[i].Key, value = self.__sorted[i].Value})
479 | i = i - 1
480 | end
481 | end
482 |
483 | Utils.simulateYield()
484 |
485 | Utils.logMethod(self, "GetSortedAsync")
486 |
487 | return setmetatable({
488 | __datastore = self;
489 | __currentPage = 1;
490 | __pageSize = pageSize;
491 | __results = results;
492 | IsFinished = (#results <= pageSize);
493 | }, MockDataStorePages)
494 | end
495 |
496 | function MockOrderedDataStore:ExportToJSON()
497 | return HttpService:JSONEncode(self.__data)
498 | end
499 |
500 | function MockOrderedDataStore:ImportFromJSON(json, verbose)
501 | local content
502 | if type(json) == "string" then
503 | local parsed, value = pcall(function() return HttpService:JSONDecode(json) end)
504 | if not parsed then
505 | error("bad argument #1 to 'ImportFromJSON' (string is not valid json)", 2)
506 | end
507 | content = value
508 | elseif type(json) == "table" then
509 | content = json -- No need to deepcopy, OrderedDataStore only contains numbers which are passed by value
510 | else
511 | error(("bad argument #1 to 'ImportFromJSON' (string or table expected, got %s)"):format(typeof(json)), 2)
512 | end
513 |
514 | if verbose ~= nil and type(verbose) ~= "boolean" then
515 | error(("bad argument #2 to 'ImportFromJSON' (boolean expected, got %s)"):format(typeof(verbose)), 2)
516 | end
517 |
518 | Utils.importPairsFromTable(
519 | content,
520 | self.__data,
521 | MockDataStoreManager.GetDataInterface(self.__data),
522 | (verbose == false and function() end or warn),
523 | "ImportFromJSON",
524 | ("OrderedDataStore > %s > %s"):format(self.__name, self.__scope),
525 | true
526 | )
527 | end
528 |
529 | return MockOrderedDataStore
530 |
--------------------------------------------------------------------------------
/lib/MockDataStoreService/init.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | MockDataStoreService.lua
3 | This module implements the API and functionality of Roblox's DataStoreService class.
4 |
5 | This module is licensed under APLv2, refer to the LICENSE file or:
6 | https://github.com/buildthomas/MockDataStoreService/blob/master/LICENSE
7 | ]]
8 |
9 | local MockDataStoreService = {}
10 |
11 | local MockDataStoreManager = require(script.MockDataStoreManager)
12 | local MockGlobalDataStore = require(script.MockGlobalDataStore)
13 | local MockOrderedDataStore = require(script.MockOrderedDataStore)
14 | local Utils = require(script.MockDataStoreUtils)
15 | local Constants = require(script.MockDataStoreConstants)
16 | local HttpService = game:GetService("HttpService") -- for json encode/decode
17 |
18 | local function makeGetWrapper(methodName, getObject, isGlobal) -- Helper function to reduce amount of redundant code
19 | return function(_, name, scope)
20 | if not game:GetService("RunService"):IsServer() then
21 | error("DataStore can't be accessed from client", 2)
22 | end
23 |
24 | if isGlobal then
25 | return getObject()
26 | else
27 | if type(name) ~= "string" then
28 | error(("bad argument #1 to '%s' (string expected, got %s)")
29 | :format(methodName, typeof(name)), 2)
30 | elseif scope ~= nil and type(scope) ~= "string" then
31 | error(("bad argument #2 to '%s' (string expected, got %s)")
32 | :format(methodName, typeof(scope)), 2)
33 | elseif #name == 0 then
34 | error(("bad argument #1 to '%s' (name can't be empty string)")
35 | :format(methodName), 2)
36 | elseif #name > Constants.MAX_LENGTH_NAME then
37 | error(("bad argument #1 to '%s' (name exceeds %d character limit)")
38 | :format(methodName, Constants.MAX_LENGTH_NAME), 2)
39 | elseif scope and #scope == 0 then
40 | error(("bad argument #2 to '%s' (scope can't be empty string)")
41 | :format(methodName), 2)
42 | elseif scope and #scope > Constants.MAX_LENGTH_SCOPE then
43 | error(("bad argument #2 to '%s' (scope exceeds %d character limit)")
44 | :format(methodName, Constants.MAX_LENGTH_SCOPE), 2)
45 | end
46 | return getObject(name, scope or "global")
47 | end
48 |
49 | end
50 | end
51 |
52 | MockDataStoreService.GetGlobalDataStore = makeGetWrapper(
53 | "GetGlobalDataStore",
54 | function()
55 | local data = MockDataStoreManager.GetGlobalData()
56 |
57 | local interface = MockDataStoreManager.GetDataInterface(data)
58 | if interface then
59 | return interface
60 | end
61 |
62 | local value = {
63 | __type = "GlobalDataStore";
64 | __data = data; -- Mapping from to
65 | __event = Instance.new("BindableEvent"); -- For OnUpdate
66 | __writeCache = {};
67 | __writeLock = {};
68 | __getCache = {};
69 | }
70 | interface = setmetatable(value, MockGlobalDataStore)
71 | MockDataStoreManager.SetDataInterface(data, interface)
72 |
73 | return interface
74 | end,
75 | true -- This is the global datastore, no name/scope needed
76 | )
77 |
78 | MockDataStoreService.GetDataStore = makeGetWrapper(
79 | "GetDataStore",
80 | function(name, scope)
81 | local data = MockDataStoreManager.GetData(name, scope)
82 |
83 | local interface = MockDataStoreManager.GetDataInterface(data)
84 | if interface then
85 | return interface
86 | end
87 |
88 | local value = {
89 | __type = "GlobalDataStore";
90 | __name = name;
91 | __scope = scope;
92 | __data = data; -- Mapping from to
93 | __event = Instance.new("BindableEvent"); -- For OnUpdate
94 | __writeCache = {};
95 | __writeLock = {};
96 | __getCache = {};
97 | }
98 | interface = setmetatable(value, MockGlobalDataStore)
99 | MockDataStoreManager.SetDataInterface(data, interface)
100 |
101 | return interface
102 | end
103 | )
104 |
105 | MockDataStoreService.GetOrderedDataStore = makeGetWrapper(
106 | "GetOrderedDataStore",
107 | function(name, scope)
108 | local data = MockDataStoreManager.GetOrderedData(name, scope)
109 |
110 | local interface = MockDataStoreManager.GetDataInterface(data)
111 | if interface then
112 | return interface
113 | end
114 |
115 | local value = {
116 | __type = "OrderedDataStore";
117 | __name = name;
118 | __scope = scope;
119 | __data = data; -- Mapping from to
120 | __sorted = {}; -- List of {Key = , Value = } pairs
121 | __ref = {}; -- Mapping from to corresponding {Key = , Value = } entry in __sorted
122 | __changed = false; -- Whether __sorted is guaranteed sorted at the moment
123 | __event = Instance.new("BindableEvent"); -- For OnUpdate
124 | __writeCache = {};
125 | __writeLock = {};
126 | __getCache = {};
127 | }
128 | interface = setmetatable(value, MockOrderedDataStore)
129 | MockDataStoreManager.SetDataInterface(data, interface)
130 |
131 | return interface
132 | end
133 | )
134 |
135 | local DataStoreRequestTypes = {}
136 |
137 | for _, Enumerator in ipairs(Enum.DataStoreRequestType:GetEnumItems()) do
138 | DataStoreRequestTypes[Enumerator] = Enumerator
139 | DataStoreRequestTypes[Enumerator.Name] = Enumerator
140 | DataStoreRequestTypes[Enumerator.Value] = Enumerator
141 | end
142 |
143 | function MockDataStoreService:GetRequestBudgetForRequestType(requestType) -- luacheck: ignore self
144 | if not DataStoreRequestTypes[requestType] then
145 | error(("bad argument #1 to 'GetRequestBudgetForRequestType' (unable to cast '%s' of type %s to DataStoreRequestType)")
146 | :format(tostring(requestType), typeof(requestType)), 2)
147 | end
148 |
149 | return MockDataStoreManager.GetBudget(DataStoreRequestTypes[requestType])
150 | end
151 |
152 | function MockDataStoreService:ImportFromJSON(json, verbose) -- luacheck: ignore self
153 | local content
154 | if type(json) == "string" then
155 | local parsed, value = pcall(function() return HttpService:JSONDecode(json) end)
156 | if not parsed then
157 | error("bad argument #1 to 'ImportFromJSON' (string is not valid json)", 2)
158 | end
159 | content = value
160 | elseif type(json) == "table" then
161 | content = Utils.deepcopy(json)
162 | else
163 | error(("bad argument #1 to 'ImportFromJSON' (string or table expected, got %s)"):format(typeof(json)), 2)
164 | end
165 | if verbose ~= nil and type(verbose) ~= "boolean" then
166 | error(("bad argument #2 to 'ImportFromJSON' (boolean expected, got %s)"):format(typeof(verbose)), 2)
167 | end
168 |
169 | return MockDataStoreManager.ImportFromJSON(content, verbose)
170 | end
171 |
172 | function MockDataStoreService:ExportToJSON() -- luacheck: ignore self
173 | return MockDataStoreManager.ExportToJSON()
174 | end
175 |
176 | return MockDataStoreService
177 |
--------------------------------------------------------------------------------
/lib/init.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | DataStoreService.lua
3 | This module decides whether to use actual datastores or mock datastores depending on the environment.
4 |
5 | This module is licensed under APLv2, refer to the LICENSE file or:
6 | https://github.com/buildthomas/MockDataStoreService/blob/master/LICENSE
7 | ]]
8 |
9 | local MockDataStoreServiceModule = script.MockDataStoreService
10 |
11 | local shouldUseMock = false
12 | if game.GameId == 0 then -- Local place file
13 | shouldUseMock = true
14 | elseif game:GetService("RunService"):IsStudio() then -- Published file in Studio
15 | local status, message = pcall(function()
16 | -- This will error if current instance has no Studio API access:
17 | game:GetService("DataStoreService"):GetDataStore("__TEST"):SetAsync("__TEST", "__TEST_" .. os.time())
18 | end)
19 | if not status and message:find("403", 1, true) then -- HACK
20 | -- Can connect to datastores, but no API access
21 | shouldUseMock = true
22 | end
23 | end
24 |
25 | -- Return the mock or actual service depending on environment:
26 | if shouldUseMock then
27 | warn("INFO: Using MockDataStoreService instead of DataStoreService")
28 | return require(MockDataStoreServiceModule)
29 | else
30 | return game:GetService("DataStoreService")
31 | end
32 |
--------------------------------------------------------------------------------
/place.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MockDataStoreService Test Place",
3 | "tree": {
4 | "$className": "DataModel",
5 |
6 | "ServerStorage": {
7 | "$className": "ServerStorage",
8 |
9 | "DataStoreService": {
10 | "$path": "lib"
11 | },
12 |
13 | "TestDataStoreService": {
14 | "$path": "spec"
15 | },
16 |
17 | "TestEZ": {
18 | "$path": "vendor/testez/lib"
19 | }
20 | },
21 |
22 | "ServerScriptService": {
23 | "$className": "ServerScriptService",
24 |
25 | "MockDataStoreServiceTests": {
26 | "$path": "bin/run-tests.server.lua"
27 | }
28 | },
29 |
30 | "HttpService": {
31 | "$className": "HttpService",
32 | "$properties": {
33 | "HttpEnabled": true
34 | }
35 | },
36 |
37 | "Players": {
38 | "$className": "Players",
39 | "$properties": {
40 | "CharacterAutoLoads": false
41 | }
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/rotriever.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "buildthomas/MockDataStoreService"
3 | author = "buildthomas"
4 | license = "Apache-2.0"
5 | content_root = "lib"
6 | version = "1.0.3"
7 |
--------------------------------------------------------------------------------
/spec/MockDataStoreService/MockDataStoreConstants.spec.lua:
--------------------------------------------------------------------------------
1 | return function()
2 | local Constants = require(script.Parent.Test).Constants
3 |
4 | local budgetTypes = {
5 | "BUDGET_GETASYNC";
6 | "BUDGET_GETSORTEDASYNC";
7 | "BUDGET_ONUPDATE";
8 | "BUDGET_SETINCREMENTASYNC";
9 | "BUDGET_SETINCREMENTSORTEDASYNC";
10 | }
11 |
12 | describe("Constants list", function()
13 |
14 | it("should be a table", function()
15 | expect(Constants).to.be.a("table")
16 | end)
17 |
18 | it("should contain all plain values", function()
19 | expect(Constants.MAX_LENGTH_KEY).to.be.a("number")
20 | expect(Constants.MAX_LENGTH_NAME).to.be.a("number")
21 | expect(Constants.MAX_LENGTH_SCOPE).to.be.a("number")
22 | expect(Constants.MAX_LENGTH_DATA).to.be.a("number")
23 | expect(Constants.MAX_PAGE_SIZE).to.be.a("number")
24 | expect(Constants.YIELD_TIME_MIN).to.be.a("number")
25 | expect(Constants.YIELD_TIME_MAX).to.be.a("number")
26 | expect(Constants.YIELD_TIME_UPDATE_MIN).to.be.a("number")
27 | expect(Constants.YIELD_TIME_UPDATE_MAX).to.be.a("number")
28 | expect(Constants.WRITE_COOLDOWN).to.be.a("number")
29 | expect(Constants.GET_COOLDOWN).to.be.a("number")
30 | expect(Constants.THROTTLE_QUEUE_SIZE).to.be.a("number")
31 | expect(Constants.BUDGETING_ENABLED).to.be.a("boolean")
32 | expect(Constants.BUDGET_BASE).to.be.a("number")
33 | expect(Constants.BUDGET_ONCLOSE_BASE).to.be.a("number")
34 | expect(Constants.BUDGET_UPDATE_INTERVAL).to.be.a("number")
35 | end)
36 |
37 | it("should contain all structured values", function()
38 | for i = 1, #budgetTypes do
39 | local budgetType = budgetTypes[i]
40 | expect(Constants[budgetType]).to.be.a("table")
41 | expect(Constants[budgetType].START).to.be.a("number")
42 | expect(Constants[budgetType].RATE).to.be.a("number")
43 | expect(Constants[budgetType].RATE_PLR).to.be.a("number")
44 | expect(Constants[budgetType].MAX_FACTOR).to.be.a("number")
45 | end
46 | end)
47 |
48 | it("should have positive integer limits for characters and page size", function()
49 | expect(Constants.MAX_LENGTH_KEY % 1).to.equal(0)
50 | expect(Constants.MAX_LENGTH_KEY > 0).to.equal(true)
51 |
52 | expect(Constants.MAX_LENGTH_NAME % 1).to.equal(0)
53 | expect(Constants.MAX_LENGTH_NAME > 0).to.equal(true)
54 |
55 | expect(Constants.MAX_LENGTH_SCOPE % 1).to.equal(0)
56 | expect(Constants.MAX_LENGTH_SCOPE > 0).to.equal(true)
57 |
58 | expect(Constants.MAX_LENGTH_DATA % 1).to.equal(0)
59 | expect(Constants.MAX_LENGTH_DATA > 0).to.equal(true)
60 |
61 | expect(Constants.MAX_PAGE_SIZE % 1).to.equal(0)
62 | expect(Constants.MAX_PAGE_SIZE > 0).to.equal(true)
63 | end)
64 |
65 | it("should have positive integer limits for budgeting", function()
66 | expect(Constants.THROTTLE_QUEUE_SIZE % 1).to.equal(0)
67 | expect(Constants.THROTTLE_QUEUE_SIZE > 0).to.equal(true)
68 |
69 | for i = 1, #budgetTypes do
70 | local budgetType = budgetTypes[i]
71 | expect(Constants[budgetType].START % 1).to.equal(0)
72 | expect(Constants[budgetType].START > 0).to.equal(true)
73 |
74 | expect(Constants[budgetType].RATE % 1).to.equal(0)
75 | expect(Constants[budgetType].RATE > 0).to.equal(true)
76 |
77 | expect(Constants[budgetType].RATE_PLR % 1).to.equal(0)
78 | expect(Constants[budgetType].RATE_PLR > 0).to.equal(true)
79 |
80 | expect(Constants[budgetType].MAX_FACTOR % 1).to.equal(0)
81 | expect(Constants[budgetType].MAX_FACTOR > 0).to.equal(true)
82 | end
83 |
84 | expect(Constants.BUDGET_BASE % 1).to.equal(0)
85 | expect(Constants.BUDGET_BASE > 0).to.equal(true)
86 |
87 | expect(Constants.BUDGET_ONCLOSE_BASE % 1).to.equal(0)
88 | expect(Constants.BUDGET_ONCLOSE_BASE > 0).to.equal(true)
89 | end)
90 |
91 | it("should have starting budgets that are within the maximum limit", function()
92 | for i = 1, #budgetTypes do
93 | local budgetType = budgetTypes[i]
94 | expect(Constants[budgetType].START <= Constants[budgetType].MAX_FACTOR * Constants[budgetType].RATE)
95 | .to.equal(true)
96 | end
97 | end)
98 |
99 | it("should have non-negative time duration values", function()
100 | expect(Constants.YIELD_TIME_MIN >= 0).to.equal(true)
101 | expect(Constants.YIELD_TIME_MAX >= 0).to.equal(true)
102 |
103 | expect(Constants.YIELD_TIME_UPDATE_MIN >= 0).to.equal(true)
104 | expect(Constants.YIELD_TIME_UPDATE_MAX >= 0).to.equal(true)
105 |
106 | expect(Constants.WRITE_COOLDOWN >= 0).to.equal(true)
107 | expect(Constants.GET_COOLDOWN >= 0).to.equal(true)
108 |
109 | expect(Constants.BUDGET_UPDATE_INTERVAL >= 0).to.equal(true)
110 | end)
111 |
112 | it("should have consistent minima and maxima for yielding time values", function()
113 | expect(Constants.YIELD_TIME_MIN <= Constants.YIELD_TIME_MAX).to.equal(true)
114 | expect(Constants.YIELD_TIME_UPDATE_MIN <= Constants.YIELD_TIME_UPDATE_MAX).to.equal(true)
115 | end)
116 |
117 | end)
118 |
119 | end
120 |
--------------------------------------------------------------------------------
/spec/MockDataStoreService/MockDataStorePages.spec.lua:
--------------------------------------------------------------------------------
1 | return function()
2 | local Test = require(script.Parent.Test)
3 |
4 | describe("MockDataStorePages", function()
5 |
6 | it("should expose all API members", function()
7 | Test.reset()
8 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
9 |
10 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(true, 100)
11 |
12 | expect(MockDataStorePages.AdvanceToNextPageAsync).to.be.a("function")
13 | expect(MockDataStorePages.GetCurrentPage).to.be.a("function")
14 | expect(MockDataStorePages.IsFinished).to.be.a("boolean")
15 |
16 | end)
17 |
18 | end)
19 |
20 | describe("MockDataStorePages::AdvanceToNextPageAsync", function()
21 |
22 | it("should get all results", function()
23 | Test.reset()
24 | Test.setStaticBudgets(100)
25 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
26 |
27 | local totalResults = 1021
28 |
29 | local data = {}
30 | for i = 1, totalResults do
31 | data["TestKey"..i] = i
32 | end
33 |
34 | MockOrderedDataStore:ImportFromJSON(data)
35 |
36 | local numResults = 0
37 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(true, 50)
38 | expect(MockDataStorePages.IsFinished).to.equal(false)
39 | repeat
40 | numResults = numResults + #MockDataStorePages:GetCurrentPage()
41 | until MockDataStorePages.IsFinished or MockDataStorePages:AdvanceToNextPageAsync()
42 |
43 | expect(numResults).to.equal(totalResults)
44 |
45 | end)
46 |
47 | it("should report correctly ordered results for ascending mode", function()
48 | Test.reset()
49 | Test.setStaticBudgets(100)
50 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
51 |
52 | local totalResults = 1000
53 |
54 | local data = {}
55 | for i = 1, totalResults do
56 | data["TestKey"..i] = i
57 | end
58 |
59 | MockOrderedDataStore:ImportFromJSON(data)
60 |
61 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(true, 100)
62 | local previous = -math.huge
63 | repeat
64 | for _, pair in ipairs(MockDataStorePages:GetCurrentPage()) do
65 | expect(previous <= pair.value).to.equal(true)
66 | previous = pair.value
67 | end
68 | until MockDataStorePages.IsFinished or MockDataStorePages:AdvanceToNextPageAsync()
69 |
70 | end)
71 |
72 | it("should report correctly ordered results for descending mode", function()
73 | Test.reset()
74 | Test.setStaticBudgets(100)
75 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
76 |
77 | local totalResults = 1000
78 |
79 | local data = {}
80 | for i = 1, totalResults do
81 | data["TestKey"..i] = i
82 | end
83 |
84 | MockOrderedDataStore:ImportFromJSON(data)
85 |
86 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(false, 100)
87 | local previous = math.huge
88 | repeat
89 | for _, pair in ipairs(MockDataStorePages:GetCurrentPage()) do
90 | expect(previous >= pair.value).to.equal(true)
91 | previous = pair.value
92 | end
93 | until MockDataStorePages.IsFinished or MockDataStorePages:AdvanceToNextPageAsync()
94 |
95 | end)
96 |
97 | it("should not exceed page size for each page of results", function()
98 | Test.reset()
99 | Test.setStaticBudgets(100)
100 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
101 |
102 | local totalResults = 918
103 |
104 | local data = {}
105 | for i = 1, totalResults do
106 | data["TestKey"..i] = i
107 | end
108 |
109 | MockOrderedDataStore:ImportFromJSON(data)
110 |
111 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(true, 43)
112 | repeat
113 | if not MockDataStorePages.IsFinished then
114 | expect(#MockDataStorePages:GetCurrentPage()).to.equal(43)
115 | else
116 | expect(#MockDataStorePages:GetCurrentPage() <= 43).to.equal(true)
117 | end
118 | until MockDataStorePages.IsFinished or MockDataStorePages:AdvanceToNextPageAsync()
119 |
120 | end)
121 |
122 | it("should report values if and only if they are in range", function()
123 | Test.reset()
124 | Test.setStaticBudgets(1e3)
125 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
126 |
127 | local totalResults = 1000
128 |
129 | local data = {}
130 | for i = 1, totalResults do
131 | data["TestKey"..i] = i
132 | end
133 |
134 | MockOrderedDataStore:ImportFromJSON(data)
135 |
136 | local function test(isAscending, pageSize, minValue, maxValue)
137 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(
138 | isAscending,
139 | pageSize,
140 | minValue,
141 | maxValue
142 | )
143 | minValue = minValue or -math.huge
144 | maxValue = maxValue or math.huge
145 | repeat
146 | for _, pair in ipairs(MockDataStorePages:GetCurrentPage()) do
147 | expect(pair.value >= minValue).to.equal(true)
148 | expect(pair.value <= maxValue).to.equal(true)
149 | end
150 | until MockDataStorePages.IsFinished or MockDataStorePages:AdvanceToNextPageAsync()
151 | end
152 |
153 | test(true, 100, nil, -5)
154 | test(true, 100, nil, 234)
155 | test(true, 100, nil, 1592)
156 |
157 | test(false, 100, nil, -5)
158 | test(false, 100, nil, 234)
159 | test(false, 100, nil, 1592)
160 |
161 | test(true, 100, 1023, nil)
162 | test(true, 100, 689, nil)
163 | test(true, 100, -102, nil)
164 |
165 | test(false, 100, 1023, nil)
166 | test(false, 100, 689, nil)
167 | test(false, 100, -102, nil)
168 |
169 | test(true, 100, -123, -49)
170 | test(true, 100, -148, 184)
171 | test(true, 100, -94, 1194)
172 | test(true, 100, 395, 748)
173 | test(true, 100, 859, 1048)
174 | test(true, 100, 1038, 1492)
175 |
176 | test(false, 100, -123, -49)
177 | test(false, 100, -148, 184)
178 | test(false, 100, -94, 1194)
179 | test(false, 100, 395, 748)
180 | test(false, 100, 859, 1048)
181 | test(false, 100, 1038, 1492)
182 |
183 | end)
184 |
185 | it("should throw when no more pages left", function()
186 | Test.reset()
187 | Test.setStaticBudgets(100)
188 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
189 |
190 | local data = {}
191 | for i = 1, 76 do
192 | data["TestKey"..i] = i
193 | end
194 |
195 | MockOrderedDataStore:ImportFromJSON(data)
196 |
197 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(true, 100) -- exceeds 76
198 |
199 | expect(MockDataStorePages.IsFinished).to.equal(true)
200 | expect(function()
201 | MockDataStorePages:AdvanceToNextPageAsync()
202 | end).to.throw()
203 |
204 | end)
205 |
206 | it("should consume budgets correctly", function()
207 | Test.reset()
208 | Test.setStaticBudgets(100)
209 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
210 |
211 | local data = {}
212 | for i = 1, 1000 do
213 | data["TestKey"..i] = i
214 | end
215 |
216 | MockOrderedDataStore:ImportFromJSON(data)
217 |
218 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(true, 10)
219 |
220 | Test.setStaticBudgets(1e3)
221 |
222 | Test.captureBudget()
223 |
224 | for _ = 1, 42 do
225 | MockDataStorePages:AdvanceToNextPageAsync()
226 | expect(Test.checkpointBudget{
227 | [Enum.DataStoreRequestType.GetSortedAsync] = -1;
228 | }).to.be.ok()
229 | end
230 |
231 | end)
232 |
233 | it("should throttle correctly when out of budget", function()
234 | -- TODO
235 | end)
236 |
237 | end)
238 |
239 | describe("MockDataStorePages::GetCurrentPage", function()
240 |
241 | it("should return a table", function()
242 | Test.reset()
243 | Test.setStaticBudgets(100)
244 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
245 |
246 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(true, 100)
247 |
248 | expect(MockDataStorePages:GetCurrentPage()).to.be.a("table")
249 |
250 | end)
251 |
252 | it("should not allow mutation of values indirectly", function()
253 | Test.reset()
254 | Test.setStaticBudgets(100)
255 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
256 |
257 | local data = {}
258 | for i = 1, 100 do
259 | data["TestKey"..i] = i
260 | end
261 |
262 | MockOrderedDataStore:ImportFromJSON(data)
263 |
264 | local MockDataStorePages = MockOrderedDataStore:GetSortedAsync(true, 100)
265 |
266 | local result = MockDataStorePages:GetCurrentPage()
267 |
268 | result[1].value = 10001
269 | result[2].value = 10002
270 | result[3].value = 10003
271 | result[4].key = "This is not the actual key..."
272 | result[5].key = true
273 | for i = 97,100 do
274 | result[i] = nil
275 | end
276 |
277 | result = MockDataStorePages:GetCurrentPage()
278 |
279 | expect(#result).to.equal(100)
280 | for i = 1, 100 do
281 | expect(result[i]).to.be.ok()
282 | expect(result[i].key).to.equal("TestKey"..i)
283 | expect(result[i].value).to.equal(i)
284 | end
285 |
286 | end)
287 |
288 | end)
289 |
290 | end
291 |
--------------------------------------------------------------------------------
/spec/MockDataStoreService/MockDataStoreUtils.spec.lua:
--------------------------------------------------------------------------------
1 | return function()
2 | local Utils = require(script.Parent.Test).Utils
3 |
4 | describe("Utils", function()
5 |
6 | it("should be a table", function()
7 | expect(Utils).to.be.a("table")
8 | end)
9 |
10 | end)
11 |
12 | describe("Utils.deepcopy", function()
13 |
14 | it("should copy flat arrays correctly", function()
15 | local array = {1, 2, 3, "Testing...", true, false}
16 | local copy = Utils.deepcopy(array)
17 |
18 | expect(copy).to.be.a("table")
19 | for i, v in pairs(array) do
20 | expect(copy[i]).to.equal(v)
21 | end
22 | for i, v in pairs(copy) do
23 | expect(array[i]).to.equal(v)
24 | end
25 | expect(#copy).to.equal(#array)
26 | end)
27 |
28 | it("should copy flat dictionaries correctly", function()
29 | local dictionary = {a = 1, b = 2, c = 3, [true] = false}
30 | local copy = Utils.deepcopy(dictionary)
31 |
32 | expect(copy).to.be.a("table")
33 | for i, v in pairs(dictionary) do
34 | expect(copy[i]).to.equal(v)
35 | end
36 | for i, v in pairs(copy) do
37 | expect(dictionary[i]).to.equal(v)
38 | end
39 | expect(#copy).to.equal(#dictionary)
40 | end)
41 |
42 | it("should copy flat mixed tables correctly", function()
43 | local mixed = {
44 | a = "Test";
45 | 42;
46 | 1337;
47 | b = "Hello world!";
48 | c = 123;
49 | "Testing!";
50 | }
51 | local copy = Utils.deepcopy(mixed)
52 |
53 | expect(mixed).to.be.a("table")
54 | for i, v in pairs(copy) do
55 | expect(mixed[i]).to.equal(v)
56 | end
57 | for i, v in pairs(mixed) do
58 | expect(copy[i]).to.equal(v)
59 | end
60 | expect(#mixed).to.equal(#copy)
61 | end)
62 |
63 | it("should copy nested arrays/dictionaries/mixed tables correctly", function()
64 | local nested = {
65 | a = {42};
66 | b = {
67 | c = 3.14;
68 | d = {"Testing", 1, 2, 3};
69 | "w";
70 | "t";
71 | "f";
72 | };
73 | e = {};
74 | }
75 | local copy = Utils.deepcopy(nested)
76 |
77 | expect(copy).to.be.a("table")
78 | expect(#copy).to.equal(#nested)
79 |
80 | expect(copy.a).to.be.a("table")
81 | expect(#copy.a).to.equal(#nested.a)
82 | expect(copy.a[1]).to.equal(nested.a[1])
83 |
84 | expect(copy.b).to.be.a("table")
85 | expect(#copy.b).to.equal(#nested.b)
86 | expect(copy.b.c).to.equal(nested.b.c)
87 | expect(copy.b.d).to.a("table")
88 | for i = 1, 4 do
89 | expect(copy.b.d[i]).to.equal(nested.b.d[i])
90 | end
91 | expect(#copy.b.d).to.equal(#nested.b.d)
92 | for i = 1, 3 do
93 | expect(copy.b[i]).to.equal(nested.b[i])
94 | end
95 |
96 | expect(copy.e).to.be.a("table")
97 | expect(#copy.e).to.equal(#nested.e)
98 | end)
99 |
100 | end)
101 |
102 | describe("Utils.scanValidity", function()
103 |
104 | it("should report nothing for proper entries", function()
105 | local proper1 = {
106 | a = 1;
107 | b = {1, 2, 3};
108 | c = 3;
109 | }
110 | local proper2 = {
111 | a = "Test";
112 | b = {true, false, true};
113 | c = "Hello world!";
114 | }
115 |
116 | expect(Utils.scanValidity(proper1)).to.equal(true)
117 | expect(Utils.scanValidity(proper2)).to.equal(true)
118 | end)
119 |
120 | it("should report invalidly typed values", function()
121 | local testWithFunction = {
122 | a = function() end;
123 | b = 2;
124 | c = 3;
125 | }
126 | local testWithInstances = {
127 | a = 1;
128 | b = {"a", Instance.new("Frame"), "c"};
129 | c = Instance.new("Frame");
130 | }
131 | local testWithCoroutines = {
132 | a = 1;
133 | b = coroutine.create(function() end);
134 | c = {coroutine.create(function() end)};
135 | }
136 |
137 | local isValid, keyPath, reason = Utils.scanValidity(testWithFunction)
138 | expect(isValid).to.equal(false)
139 | expect(keyPath).to.be.ok()
140 | expect(reason).to.be.ok()
141 |
142 | isValid, keyPath, reason = Utils.scanValidity(testWithInstances)
143 | expect(isValid).to.equal(false)
144 | expect(keyPath).to.be.ok()
145 | expect(reason).to.be.ok()
146 |
147 | isValid, keyPath, reason = Utils.scanValidity(testWithCoroutines)
148 | expect(isValid).to.equal(false)
149 | expect(keyPath).to.be.ok()
150 | expect(reason).to.be.ok()
151 | end)
152 |
153 | it("should report mixed tables", function()
154 | local mixed = {
155 | a = 1;
156 | 2;
157 | 3;
158 | }
159 | local mixedNested = {
160 | a = { "1", b = "2", "3" };
161 | b = 2;
162 | }
163 |
164 | local isValid, keyPath, reason = Utils.scanValidity(mixed)
165 | expect(isValid).to.equal(false)
166 | expect(keyPath).to.be.ok()
167 | expect(reason).to.be.ok()
168 |
169 | isValid, keyPath, reason = Utils.scanValidity(mixedNested)
170 | expect(isValid).to.equal(false)
171 | expect(keyPath).to.be.ok()
172 | expect(reason).to.be.ok()
173 | end)
174 |
175 | it("should report array tables with holes", function()
176 | local arrayWithHoles = {
177 | [1] = "a";
178 | [2] = "b";
179 | [4] = "c";
180 | [-1] = "d";
181 | }
182 |
183 | local isValid, keyPath, reason = Utils.scanValidity(arrayWithHoles)
184 | expect(isValid).to.equal(false)
185 | expect(keyPath).to.be.ok()
186 | expect(reason).to.be.ok()
187 | end)
188 |
189 | it("should report float indices", function()
190 | local dictionaryFloatKeys = {
191 | [-1.4] = "a";
192 | [math.pi] = "b";
193 | [1/9] = "c";
194 | }
195 |
196 | local isValid, keyPath, reason = Utils.scanValidity(dictionaryFloatKeys)
197 | expect(isValid).to.equal(false)
198 | expect(keyPath).to.be.ok()
199 | expect(reason).to.be.ok()
200 | end)
201 |
202 | it("should report invalidly typed indices", function()
203 | local dictionaryInvalidKeys = {
204 | [true] = "a";
205 | [function() end] = "b";
206 | [Instance.new("Frame")] = "c";
207 | }
208 |
209 | local isValid, keyPath, reason = Utils.scanValidity(dictionaryInvalidKeys)
210 | expect(isValid).to.equal(false)
211 | expect(keyPath).to.be.ok()
212 | expect(reason).to.be.ok()
213 | end)
214 |
215 | it("should report cyclic tables", function()
216 | local cyclic1 = {
217 | level = {
218 | baz = 3;
219 | };
220 | foo = 1;
221 | bar = 2;
222 | }
223 | cyclic1.level.test = cyclic1
224 | local cyclic2 = {
225 | recursion = {};
226 | }
227 | cyclic2.recursion.recursion = cyclic2.recursion
228 |
229 | local isValid, keyPath, reason = Utils.scanValidity(cyclic1)
230 | expect(isValid).to.equal(false)
231 | expect(keyPath).to.be.ok()
232 | expect(reason).to.be.ok()
233 |
234 | isValid, keyPath, reason = Utils.scanValidity(cyclic2)
235 | expect(isValid).to.equal(false)
236 | expect(keyPath).to.be.ok()
237 | expect(reason).to.be.ok()
238 | end)
239 |
240 | it("should report infinite/-infinite indices", function()
241 | local dictionaryOutOfRangeKeys = {
242 | [-math.huge] = "Hello";
243 | [math.huge] = "world!";
244 | }
245 |
246 | local isValid, keyPath, reason = Utils.scanValidity(dictionaryOutOfRangeKeys)
247 | expect(isValid).to.equal(false)
248 | expect(keyPath).to.be.ok()
249 | expect(reason).to.be.ok()
250 | end)
251 |
252 | end)
253 |
254 | describe("Utils.getStringPath", function()
255 |
256 | it("should format paths in the expected way", function()
257 | local pathTable = {"foo", "bar", "baz"}
258 |
259 | expect(Utils.getStringPath(pathTable)).to.equal("foo.bar.baz")
260 | end)
261 |
262 | end)
263 |
264 | -- Utils.importPairsFromTable is not tested here, but through
265 | -- DataStoreService/GlobalDataStore/OrderedDataStore:ImportFromJSON(...)
266 |
267 | describe("Utils.prepareDataStoresForExport", function()
268 |
269 | it("should strip off empty scopes", function()
270 | local stores = {
271 | TestName = {
272 | TestScope = {
273 | Key1 = 1;
274 | Key2 = 2;
275 | Key3 = 3
276 | };
277 | TestScope2 = {};
278 | };
279 | TestName2 = {
280 | TestScope = {};
281 | TestScope2 = {};
282 | TestScope3 = {};
283 | };
284 | TestName3 = {};
285 | }
286 |
287 | stores = Utils.prepareDataStoresForExport(stores)
288 | expect(stores.TestName).to.be.ok()
289 | expect(stores.TestName.TestScope).to.be.ok()
290 | expect(stores.TestName.TestScope2).to.never.be.ok()
291 | expect(stores.TestName2).to.never.be.ok()
292 | expect(stores.TestName3).to.never.be.ok()
293 | end)
294 |
295 | it("should return nothing if entirely empty", function()
296 | local storesEmpty = {}
297 | local storesEmptyName = {
298 | TestName = {};
299 | }
300 | local storesEmptyScopes = {
301 | TestName = {
302 | TestScope1 = {};
303 | TestScope2 = {};
304 | };
305 | }
306 |
307 | expect(Utils.prepareDataStoresForExport(storesEmpty)).to.never.be.ok()
308 | expect(Utils.prepareDataStoresForExport(storesEmptyName)).to.never.be.ok()
309 | expect(Utils.prepareDataStoresForExport(storesEmptyScopes)).to.never.be.ok()
310 | end)
311 |
312 | end)
313 |
314 | end
315 |
--------------------------------------------------------------------------------
/spec/MockDataStoreService/MockGlobalDataStore.spec.lua:
--------------------------------------------------------------------------------
1 | return function()
2 | local Test = require(script.Parent.Test)
3 | local HttpService = game:GetService("HttpService")
4 |
5 | describe("MockGlobalDataStore", function()
6 |
7 | it("should expose all API members", function()
8 | Test.reset()
9 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
10 |
11 | expect(MockGlobalDataStore.GetAsync).to.be.a("function")
12 | expect(MockGlobalDataStore.IncrementAsync).to.be.a("function")
13 | expect(MockGlobalDataStore.RemoveAsync).to.be.a("function")
14 | expect(MockGlobalDataStore.SetAsync).to.be.a("function")
15 | expect(MockGlobalDataStore.UpdateAsync).to.be.a("function")
16 | expect(MockGlobalDataStore.OnUpdate).to.be.a("function")
17 |
18 | end)
19 |
20 | end)
21 |
22 | describe("MockGlobalDataStore::GetAsync", function()
23 |
24 | it("should return nil for non-existing keys", function()
25 | Test.reset()
26 | Test.setStaticBudgets(100)
27 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
28 |
29 | expect(MockGlobalDataStore:GetAsync("TestKey")).to.never.be.ok()
30 |
31 | end)
32 |
33 | it("should return the value for existing keys", function()
34 | Test.reset()
35 | Test.setStaticBudgets(100)
36 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
37 |
38 | local values = {
39 | TestKey1 = 123;
40 | TestKey2 = "abc";
41 | TestKey3 = true;
42 | TestKey4 = false;
43 | TestKey5 = 5.6;
44 | TestKey6 = {a = 1, b = 2, c = {1,2,3}, d = 4};
45 | }
46 |
47 | MockGlobalDataStore:ImportFromJSON(values)
48 |
49 | for key, value in pairs(values) do
50 | expect(Test.subsetOf(MockGlobalDataStore:GetAsync(key), value)).to.equal(true)
51 | end
52 |
53 | end)
54 |
55 | it("should not allow mutation of stored values indirectly", function()
56 | Test.reset()
57 | Test.setStaticBudgets(100)
58 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
59 |
60 | MockGlobalDataStore:ImportFromJSON({
61 | TestKey = {a = 1, b = 2, c = {1,2,3}, d = 4};
62 | })
63 |
64 | local result = MockGlobalDataStore:GetAsync("TestKey")
65 |
66 | result.a = 500;
67 | result.c[2] = 1337;
68 |
69 | result = MockGlobalDataStore:GetAsync("TestKey")
70 |
71 | expect(result.a).to.equal(1)
72 | expect(result.c[2]).to.equal(2);
73 |
74 | end)
75 |
76 | it("should consume budgets correctly", function()
77 | Test.reset()
78 | Test.setStaticBudgets(100)
79 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
80 |
81 | Test.captureBudget()
82 |
83 | for i = 1, 10 do
84 | MockGlobalDataStore:GetAsync("TestKey"..i)
85 | expect(Test.checkpointBudget{
86 | [Enum.DataStoreRequestType.GetAsync] = -1
87 | }).to.be.ok()
88 | end
89 |
90 | end)
91 |
92 | it("should throttle requests correctly when out of budget", function()
93 | --TODO
94 | end)
95 |
96 | it("should throw for invalid input", function()
97 | Test.reset()
98 | Test.setStaticBudgets(100)
99 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
100 |
101 | expect(function()
102 | MockGlobalDataStore:GetAsync()
103 | end).to.throw()
104 |
105 | expect(function()
106 | MockGlobalDataStore:GetAsync(123)
107 | end).to.throw()
108 |
109 | expect(function()
110 | MockGlobalDataStore:GetAsync("")
111 | end).to.throw()
112 |
113 | expect(function()
114 | MockGlobalDataStore:GetAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1))
115 | end).to.throw()
116 |
117 | end)
118 |
119 | it("should set the get-cache", function()
120 | Test.reset()
121 | Test.setStaticBudgets(100)
122 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
123 |
124 | MockGlobalDataStore:GetAsync("TestKey")
125 |
126 | Test.captureBudget()
127 |
128 | MockGlobalDataStore:GetAsync("TestKey")
129 |
130 | expect(Test.checkpointBudget{}).to.be.ok()
131 |
132 | end)
133 |
134 | end)
135 |
136 | describe("MockGlobalDataStore::IncrementAsync", function()
137 |
138 | it("should increment keys", function()
139 | Test.reset()
140 | Test.setStaticBudgets(100)
141 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
142 |
143 | MockGlobalDataStore:ImportFromJSON({TestKey = 1})
144 |
145 | MockGlobalDataStore:IncrementAsync("TestKey", 1)
146 |
147 | local export = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
148 |
149 | expect(export.TestKey).to.equal(2)
150 |
151 | end)
152 |
153 | it("should increment non-existing keys", function()
154 | Test.reset()
155 | Test.setStaticBudgets(100)
156 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
157 |
158 | MockGlobalDataStore:IncrementAsync("TestKey", 1)
159 |
160 | local export = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
161 |
162 | expect(export.TestKey).to.equal(1)
163 |
164 | end)
165 |
166 | it("should increment by the correct value", function()
167 | Test.reset()
168 | Test.setStaticBudgets(100)
169 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
170 |
171 | MockGlobalDataStore:ImportFromJSON({TestKey1 = 1, TestKey2 = 2, TestKey3 = 3, TestKey4 = 4, TestKey5 = 5})
172 |
173 | MockGlobalDataStore:IncrementAsync("TestKey1", 19)
174 | MockGlobalDataStore:IncrementAsync("TestKey2", -43)
175 | MockGlobalDataStore:IncrementAsync("TestKey3", 0)
176 | MockGlobalDataStore:IncrementAsync("TestKey4", 1.5)
177 | MockGlobalDataStore:IncrementAsync("TestKey5")
178 |
179 | local export = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
180 |
181 | expect(export.TestKey1).to.equal(20)
182 | expect(export.TestKey2).to.equal(-41)
183 | expect(export.TestKey3).to.equal(3)
184 | expect(export.TestKey4).to.equal(6)
185 | expect(export.TestKey5).to.equal(6)
186 |
187 | end)
188 |
189 | it("should return the incremented value", function()
190 | Test.reset()
191 | Test.setStaticBudgets(100)
192 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
193 |
194 | MockGlobalDataStore:ImportFromJSON({TestKey6 = 1938, TestKey7 = 42})
195 |
196 | expect(MockGlobalDataStore:IncrementAsync("TestKey1")).to.be.a("number")
197 | expect(MockGlobalDataStore:IncrementAsync("TestKey2", 100)).to.be.a("number")
198 | expect(MockGlobalDataStore:IncrementAsync("TestKey3", -100)).to.be.a("number")
199 | expect(MockGlobalDataStore:IncrementAsync("TestKey4", 0)).to.be.a("number")
200 | expect(MockGlobalDataStore:IncrementAsync("TestKey5", 1.5)).to.be.a("number")
201 | expect(MockGlobalDataStore:IncrementAsync("TestKey6")).to.be.a("number")
202 | expect(MockGlobalDataStore:IncrementAsync("TestKey7", 1083)).to.be.a("number")
203 |
204 | end)
205 |
206 | it("should throw when incrementing non-number key", function()
207 | Test.reset()
208 | Test.setStaticBudgets(100)
209 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
210 |
211 | MockGlobalDataStore:ImportFromJSON({TestKey1 = {}, TestKey2 = "Hello world!", TestKey3 = true})
212 |
213 | expect(function()
214 | MockGlobalDataStore:IncrementAsync("TestKey1")
215 | end).to.throw()
216 |
217 | expect(function()
218 | MockGlobalDataStore:IncrementAsync("TestKey2")
219 | end).to.throw()
220 |
221 | expect(function()
222 | MockGlobalDataStore:IncrementAsync("TestKey3")
223 | end).to.throw()
224 |
225 | end)
226 |
227 | it("should consume budgets correctly", function()
228 | Test.reset()
229 | Test.setStaticBudgets(100)
230 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
231 |
232 | Test.captureBudget()
233 |
234 | MockGlobalDataStore:IncrementAsync("TestKey1")
235 | expect(Test.checkpointBudget{
236 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
237 | }).to.be.ok()
238 |
239 | MockGlobalDataStore:IncrementAsync("TestKey2", 5)
240 | expect(Test.checkpointBudget{
241 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
242 | }).to.be.ok()
243 |
244 | MockGlobalDataStore:IncrementAsync("TestKey3", 0)
245 | expect(Test.checkpointBudget{
246 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
247 | }).to.be.ok()
248 |
249 | MockGlobalDataStore:IncrementAsync("TestKey4", -5)
250 | expect(Test.checkpointBudget{
251 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
252 | }).to.be.ok()
253 |
254 | end)
255 |
256 | it("should throttle requests correctly when out of budget", function()
257 | --TODO
258 | end)
259 |
260 | it("should throttle requests to respect write cooldown", function()
261 | --TODO
262 | end)
263 |
264 | it("should throw for invalid input", function()
265 | Test.reset()
266 | Test.setStaticBudgets(100)
267 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
268 |
269 | expect(function()
270 | MockGlobalDataStore:IncrementAsync()
271 | end).to.throw()
272 |
273 | expect(function()
274 | MockGlobalDataStore:IncrementAsync(123)
275 | end).to.throw()
276 |
277 | expect(function()
278 | MockGlobalDataStore:IncrementAsync("")
279 | end).to.throw()
280 |
281 | expect(function()
282 | MockGlobalDataStore:IncrementAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1))
283 | end).to.throw()
284 |
285 | expect(function()
286 | MockGlobalDataStore:IncrementAsync("Test", "Not A Number")
287 | end).to.throw()
288 |
289 | expect(function()
290 | MockGlobalDataStore:IncrementAsync(123, 1)
291 | end).to.throw()
292 |
293 | end)
294 |
295 | it("should set the get-cache", function()
296 | Test.reset()
297 | Test.setStaticBudgets(100)
298 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
299 |
300 | MockGlobalDataStore:IncrementAsync("TestKey")
301 |
302 | Test.captureBudget()
303 |
304 | MockGlobalDataStore:GetAsync("TestKey")
305 |
306 | expect(Test.checkpointBudget{}).to.be.ok()
307 |
308 | end)
309 |
310 | end)
311 |
312 | describe("MockGlobalDataStore::RemoveAsync", function()
313 |
314 | it("should be able to remove existing keys", function()
315 | Test.reset()
316 | Test.setStaticBudgets(100)
317 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
318 |
319 | MockGlobalDataStore:ImportFromJSON({ExistingKey = "Hello world!"})
320 |
321 | MockGlobalDataStore:RemoveAsync("ExistingKey")
322 |
323 | local export = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
324 | expect(export.ExistingKey).to.never.be.ok()
325 |
326 | end)
327 |
328 | it("should be able to remove non-existing keys", function()
329 | Test.reset()
330 | Test.setStaticBudgets(100)
331 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
332 |
333 | MockGlobalDataStore:RemoveAsync("NonExistingKey")
334 |
335 | local export = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
336 | expect(export.NonExistingKey).to.never.be.ok()
337 |
338 | end)
339 |
340 | it("should return the old value", function()
341 | Test.reset()
342 | Test.setStaticBudgets(100)
343 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
344 |
345 | local values = {
346 | TestKey1 = "Hello world!";
347 | TestKey2 = 123;
348 | TestKey3 = false;
349 | TestKey4 = {1,2,3};
350 | TestKey5 = {A = {B = {}, C = 3}, D = 4, E = 5};
351 | }
352 |
353 | MockGlobalDataStore:ImportFromJSON(values)
354 |
355 | for key, value in pairs(values) do
356 | expect(Test.subsetOf(MockGlobalDataStore:RemoveAsync(key), value)).to.equal(true)
357 | end
358 |
359 | end)
360 |
361 | it("should consume budgets correctly", function()
362 | Test.reset()
363 | Test.setStaticBudgets(100)
364 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
365 |
366 | MockGlobalDataStore:ImportFromJSON({ExistingKey = "Hello world!"})
367 |
368 | Test.captureBudget()
369 |
370 | MockGlobalDataStore:RemoveAsync("NonExistingKey")
371 |
372 | expect(Test.checkpointBudget{
373 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1;
374 | }).to.be.ok()
375 |
376 | MockGlobalDataStore:RemoveAsync("ExistingKey")
377 |
378 | expect(Test.checkpointBudget{
379 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1;
380 | }).to.be.ok()
381 |
382 | end)
383 |
384 | it("should throttle requests correctly when out of budget", function()
385 | --TODO
386 | end)
387 |
388 | it("should throttle requests to respect write cooldown", function()
389 | --TODO
390 | end)
391 |
392 | it("should throw for invalid input", function()
393 | Test.reset()
394 | Test.setStaticBudgets(100)
395 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
396 |
397 | expect(function()
398 | MockGlobalDataStore:RemoveAsync()
399 | end).to.throw()
400 |
401 | expect(function()
402 | MockGlobalDataStore:RemoveAsync(123)
403 | end).to.throw()
404 |
405 | expect(function()
406 | MockGlobalDataStore:RemoveAsync("")
407 | end).to.throw()
408 |
409 | expect(function()
410 | MockGlobalDataStore:RemoveAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1))
411 | end).to.throw()
412 |
413 | end)
414 |
415 | it("should not set the get-cache", function()
416 | Test.reset()
417 | Test.setStaticBudgets(100)
418 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
419 |
420 | MockGlobalDataStore:RemoveAsync("TestKey")
421 |
422 | Test.captureBudget()
423 |
424 | MockGlobalDataStore:GetAsync("TestKey")
425 |
426 | expect(Test.checkpointBudget{
427 | [Enum.DataStoreRequestType.GetAsync] = -1;
428 | }).to.be.ok()
429 |
430 | end)
431 |
432 | end)
433 |
434 | describe("MockGlobalDataStore::SetAsync", function()
435 |
436 | it("should set keys if value is valid", function()
437 | Test.reset()
438 | Test.setStaticBudgets(100)
439 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
440 |
441 | MockGlobalDataStore:ImportFromJSON({TestKey3 = "ThisShouldBeOverwritten", TestKey4 = "ThisToo"})
442 |
443 | MockGlobalDataStore:SetAsync("TestKey1", 123)
444 | MockGlobalDataStore:SetAsync("TestKey2", "abc")
445 | MockGlobalDataStore:SetAsync("TestKey3", {a = {1,2,3}, b = {c = 1, d = 2}, e = 3})
446 | MockGlobalDataStore:SetAsync("TestKey4", false)
447 |
448 | local exported = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
449 | expect(exported.TestKey1).to.equal(123)
450 | expect(exported.TestKey2).to.equal("abc")
451 | expect(exported.TestKey3).to.be.a("table")
452 | expect(exported.TestKey4).to.equal(false)
453 |
454 | end)
455 |
456 | it("should not return anything", function()
457 | Test.reset()
458 | Test.setStaticBudgets(100)
459 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
460 |
461 | MockGlobalDataStore:ImportFromJSON({TestKey2 = "test"})
462 |
463 | expect(MockGlobalDataStore:SetAsync("TestKey1", 123)).to.never.be.ok()
464 | expect(MockGlobalDataStore:SetAsync("TestKey2", false)).to.never.be.ok()
465 | expect(MockGlobalDataStore:SetAsync("TestKey3", "abc")).to.never.be.ok()
466 | expect(MockGlobalDataStore:SetAsync("TestKey4", {})).to.never.be.ok()
467 |
468 | end)
469 |
470 | it("should not allow mutation of stored values indirectly", function()
471 | Test.reset()
472 | Test.setStaticBudgets(100)
473 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
474 |
475 | local value = {a = {1,2,3}, b = {c = 1, d = 2}, e = 3}
476 |
477 | MockGlobalDataStore:SetAsync(value)
478 |
479 | value.a[1] = 1337
480 | value.e = "This should not be changed in the datastore"
481 | value.b.d = 42
482 |
483 | local exported = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
484 |
485 | expect(exported.a[1]).to.equal(1)
486 | expect(exported.e).to.equal(3)
487 | expect(exported.b.d).to.equal(2)
488 |
489 | end)
490 |
491 | it("should consume budgets correctly", function()
492 | Test.reset()
493 | Test.setStaticBudgets(100)
494 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
495 |
496 | Test.captureBudget()
497 |
498 | MockGlobalDataStore:SetAsync("TestKey1", 123)
499 | expect(Test.checkpointBudget{
500 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
501 | }).to.be.ok()
502 |
503 | MockGlobalDataStore:SetAsync("TestKey2", "abc")
504 | expect(Test.checkpointBudget{
505 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
506 | }).to.be.ok()
507 |
508 | MockGlobalDataStore:SetAsync("TestKey3", {})
509 | expect(Test.checkpointBudget{
510 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
511 | }).to.be.ok()
512 |
513 | MockGlobalDataStore:SetAsync("TestKey4", true)
514 | expect(Test.checkpointBudget{
515 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
516 | }).to.be.ok()
517 |
518 | end)
519 |
520 | it("should throttle requests correctly when out of budget", function()
521 | --TODO
522 | end)
523 |
524 | it("should throttle requests to respect write cooldown", function()
525 | --TODO
526 | end)
527 |
528 | it("should throw for invalid key", function()
529 | Test.reset()
530 | Test.setStaticBudgets(100)
531 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
532 |
533 | expect(function()
534 | MockGlobalDataStore:SetAsync()
535 | end).to.throw()
536 |
537 | expect(function()
538 | MockGlobalDataStore:SetAsync(nil, "value")
539 | end).to.throw()
540 |
541 | expect(function()
542 | MockGlobalDataStore:SetAsync(123, "value")
543 | end).to.throw()
544 |
545 | expect(function()
546 | MockGlobalDataStore:SetAsync("", "value")
547 | end).to.throw()
548 |
549 | expect(function()
550 | MockGlobalDataStore:SetAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1), "value")
551 | end).to.throw()
552 |
553 | end)
554 |
555 | it("should throw at attempts to store invalid data", function()
556 | Test.reset()
557 | Test.setStaticBudgets(100)
558 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
559 |
560 | local function testValue(v)
561 | expect(function()
562 | MockGlobalDataStore:SetAsync("TestKey", v)
563 | end).to.throw()
564 | end
565 |
566 | testValue(nil)
567 | testValue(function() end)
568 | testValue(coroutine.create(function() end))
569 | testValue(Instance.new("Frame"))
570 | testValue(Enum.DataStoreRequestType.GetAsync)
571 | testValue({a = 1, 2, 3})
572 | testValue({[0] = 1, 2, 3})
573 | testValue({[1] = 1, [2] = 2, [4] = 3})
574 | testValue({a = {function() end, 1, 2}, b = Instance.new("Frame")})
575 | testValue({a = {1,2,3}, b = {1,2,{{coroutine.create(function() end)},4,5}}, c = 3})
576 | testValue(("a"):rep(Test.Constants.MAX_LENGTH_DATA + 1))
577 |
578 | end)
579 |
580 | it("should not set the get-cache", function()
581 | Test.reset()
582 | Test.setStaticBudgets(100)
583 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
584 |
585 | MockGlobalDataStore:SetAsync("TestKey", 1)
586 |
587 | Test.captureBudget()
588 |
589 | MockGlobalDataStore:GetAsync("TestKey")
590 |
591 | expect(Test.checkpointBudget{
592 | [Enum.DataStoreRequestType.GetAsync] = -1;
593 | })
594 |
595 | end)
596 |
597 | end)
598 |
599 | describe("MockGlobalDataStore::UpdateAsync", function()
600 |
601 | it("should update keys correctly", function()
602 | Test.reset()
603 | Test.setStaticBudgets(100)
604 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
605 |
606 | MockGlobalDataStore:ImportFromJSON({TestKey3 = "ThisShouldBeOverwritten", TestKey4 = "ThisToo"})
607 |
608 | MockGlobalDataStore:UpdateAsync("TestKey1", function() return 123 end)
609 | MockGlobalDataStore:UpdateAsync("TestKey2", function() return "abc" end)
610 | MockGlobalDataStore:UpdateAsync("TestKey3", function() return {a = {1,2,3}, b = {c = 1, d = 2}, e = 3} end)
611 | MockGlobalDataStore:UpdateAsync("TestKey4", function() return false end)
612 |
613 | local exported = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
614 | expect(exported.TestKey1).to.equal(123)
615 | expect(exported.TestKey2).to.equal("abc")
616 | expect(exported.TestKey3).to.be.a("table")
617 | expect(exported.TestKey4).to.equal(false)
618 |
619 | end)
620 |
621 | it("should return the updated value", function()
622 | Test.reset()
623 | Test.setStaticBudgets(100)
624 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
625 |
626 | expect(MockGlobalDataStore:UpdateAsync("TestKey1", function() return 123 end)).to.equal(123)
627 | expect(MockGlobalDataStore:UpdateAsync("TestKey2", function() return false end)).to.equal(false)
628 | expect(MockGlobalDataStore:UpdateAsync("TestKey3", function() return "abc" end)).to.equal("abc")
629 | expect(MockGlobalDataStore:UpdateAsync("TestKey4", function() return {} end)).to.be.a("table")
630 |
631 | end)
632 |
633 | it("should pass the old value to the callback", function()
634 | Test.reset()
635 | Test.setStaticBudgets(100)
636 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
637 |
638 | local oldValues = {
639 | TestKey1 = "OldValue";
640 | TestKey2 = {};
641 | TestKey3 = false;
642 | TestKey4 = 123;
643 | }
644 |
645 | MockGlobalDataStore:ImportFromJSON(oldValues)
646 |
647 | for key, value in pairs(oldValues) do
648 | expect(MockGlobalDataStore:UpdateAsync(key, function(oldValue)
649 | if oldValue == value then
650 | return oldValue
651 | end
652 | error()
653 | end)).never.to.throw()
654 | end
655 |
656 | end)
657 |
658 | it("should not allow mutation of stored values indirectly", function()
659 | Test.reset()
660 | Test.setStaticBudgets(100)
661 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
662 |
663 | local value = {a = {1,2,3}, b = 2, c = {d = 1, e = 2}}
664 |
665 | MockGlobalDataStore:UpdateAsync("TestKey1", function() return value end)
666 |
667 | value.b = 300
668 | value.a[3] = 1337
669 | value.c.e = 200
670 |
671 | local exported = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
672 | expect(exported.TestKey1).to.be.ok()
673 | expect(exported.TestKey1.b).to.equal(2)
674 | expect(exported.TestKey1.a[3]).to.equal(3)
675 | expect(exported.TestKey1.c.e).to.equal(2)
676 |
677 | MockGlobalDataStore:ImportFromJSON({TestKey2 = value})
678 |
679 | expect(function()
680 | MockGlobalDataStore:UpdateAsync("TestKey2", function(old)
681 | old.a = 123
682 | old.b = 500
683 | old.c = 456
684 | error()
685 | end)
686 | end).to.throw()
687 |
688 | exported = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
689 | expect(exported.TestKey2).to.be.ok()
690 | expect(exported.TestKey2.b).to.equal(2)
691 | expect(exported.TestKey2.a[3]).to.equal(3)
692 | expect(exported.TestKey2.c.e).to.equal(2)
693 |
694 | end)
695 |
696 | it("should consume budgets correctly", function()
697 | Test.reset()
698 | Test.setStaticBudgets(100)
699 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
700 |
701 | Test.captureBudget()
702 |
703 | for i = 1, 10 do
704 | MockGlobalDataStore:UpdateAsync("TestKey"..i, function() return 1 end)
705 | expect(Test.checkpointBudget{
706 | [Enum.DataStoreRequestType.GetAsync] = -1;
707 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1;
708 | }).to.be.ok()
709 | end
710 |
711 | end)
712 |
713 | it("should throttle requests correctly when out of budget", function()
714 | --TODO
715 | end)
716 |
717 | it("should throttle requests to respect write cooldown", function()
718 | --TODO
719 | end)
720 |
721 | it("should throw for invalid key", function()
722 | Test.reset()
723 | Test.setStaticBudgets(100)
724 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
725 |
726 | local func = function() return 1 end
727 |
728 | expect(function()
729 | MockGlobalDataStore:UpdateAsync()
730 | end).to.throw()
731 |
732 | expect(function()
733 | MockGlobalDataStore:UpdateAsync(nil, func)
734 | end).to.throw()
735 |
736 | expect(function()
737 | MockGlobalDataStore:UpdateAsync(123, func)
738 | end).to.throw()
739 |
740 | expect(function()
741 | MockGlobalDataStore:UpdateAsync("", func)
742 | end).to.throw()
743 |
744 | expect(function()
745 | MockGlobalDataStore:UpdateAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1), func)
746 | end).to.throw()
747 |
748 | end)
749 |
750 | it("should throw at attempts to store invalid data", function()
751 | Test.reset()
752 | Test.setStaticBudgets(100)
753 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
754 |
755 | local function testValue(v)
756 | expect(function()
757 | MockGlobalDataStore:UpdateAsync("TestKey", function() return v end)
758 | end).to.throw()
759 | end
760 |
761 | testValue(nil)
762 | testValue(function() end)
763 | testValue(coroutine.create(function() end))
764 | testValue(Instance.new("Frame"))
765 | testValue(Enum.DataStoreRequestType.GetAsync)
766 | testValue({a = 1, 2, 3})
767 | testValue({[0] = 1, 2, 3})
768 | testValue({[1] = 1, [2] = 2, [4] = 3})
769 | testValue({a = {function() end, 1, 2}, b = Instance.new("Frame")})
770 | testValue({a = {1,2,3}, b = {1,2,{{coroutine.create(function() end)},4,5}}, c = 3})
771 | testValue(("a"):rep(Test.Constants.MAX_LENGTH_DATA + 1))
772 |
773 | end)
774 |
775 | it("should set the get-cache", function()
776 | Test.reset()
777 | Test.setStaticBudgets(100)
778 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
779 |
780 | MockGlobalDataStore:UpdateAsync("TestKey", function() return 1 end)
781 | MockGlobalDataStore:GetAsync("TestKey")
782 |
783 | expect(Test.Manager.GetBudget(Enum.DataStoreRequestType.GetAsync)).to.equal(1)
784 |
785 | end)
786 |
787 | end)
788 |
789 | describe("MockGlobalDataStore::OnUpdate", function()
790 |
791 | it("should return a RBXScriptConnection", function()
792 | Test.reset()
793 | Test.setStaticBudgets(100)
794 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
795 |
796 | local conn = MockGlobalDataStore:OnUpdate("TestKey")
797 |
798 | conn:Disconnect() -- don't leak after test
799 |
800 | expect(conn).to.be.a("RBXScriptConnection")
801 |
802 | end)
803 |
804 | it("should only receives updates for its connected key", function()
805 | --TODO
806 | end)
807 |
808 | it("should work with SetAsync", function()
809 | --TODO
810 | end)
811 |
812 | it("should work with UpdateAsync", function()
813 | --TODO
814 | end)
815 |
816 | it("should work with RemoveAsync", function()
817 | --TODO
818 | end)
819 |
820 | it("should work with IncrementAsync", function()
821 | --TODO
822 | end)
823 |
824 | it("should not fire callback after disconnecting", function()
825 | --TODO
826 | end)
827 |
828 | it("should not allow mutation of stored values indirectly", function()
829 | --TODO
830 | end)
831 |
832 | it("should consume budgets correctly", function()
833 | Test.reset()
834 | Test.setStaticBudgets(100)
835 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
836 |
837 | Test.captureBudget()
838 |
839 | for i = 1, 10 do
840 | local conn = MockGlobalDataStore:OnUpdate("TestKey"..i, function() end)
841 | conn:Disconnect()
842 | expect(Test.checkpointBudget{
843 | [Enum.DataStoreRequestType.OnUpdate] = -1;
844 | }).to.be.ok()
845 | end
846 |
847 | end)
848 |
849 | it("should throttle requests correctly when out of budget", function()
850 | --TODO
851 | end)
852 |
853 | it("should throw for invalid input", function()
854 | Test.reset()
855 | Test.setStaticBudgets(100)
856 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
857 |
858 | expect(function()
859 | MockGlobalDataStore:OnUpdate()
860 | end).to.throw()
861 |
862 | expect(function()
863 | MockGlobalDataStore:OnUpdate(123)
864 | end).to.throw()
865 |
866 | expect(function()
867 | MockGlobalDataStore:OnUpdate("")
868 | end).to.throw()
869 |
870 | expect(function()
871 | MockGlobalDataStore:OnUpdate(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1))
872 | end).to.throw()
873 |
874 | expect(function()
875 | MockGlobalDataStore:OnUpdate("Test", 123)
876 | end).to.throw()
877 |
878 | expect(function()
879 | MockGlobalDataStore:OnUpdate(123, function() end)
880 | end).to.throw()
881 |
882 | end)
883 |
884 | it("should not set the get-cache", function()
885 | Test.reset()
886 | Test.setStaticBudgets(100)
887 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
888 |
889 | local connection = MockGlobalDataStore:OnUpdate("TestKey", function() end)
890 |
891 | Test.captureBudget()
892 |
893 | local result = expect(function()
894 | MockGlobalDataStore:GetAsync("TestKey")
895 | end)
896 |
897 | if connection then
898 | connection:Disconnect()
899 | end
900 |
901 | expect(result.never.to.throw())
902 | expect(Test.checkpointBudget{
903 | [Enum.DataStoreRequestType.GetAsync] = -1;
904 | }).to.be.ok()
905 |
906 | end)
907 |
908 | end)
909 |
910 | local scope1 = {
911 | TestKey1 = true;
912 | TestKey2 = "Hello world!";
913 | TestKey3 = {First = 1, Second = 2, Third = 3};
914 | TestKey4 = false;
915 | }
916 |
917 | local scope2 = {}
918 |
919 | local scope3 = {
920 | TestKey1 = "Test string";
921 | TestKey2 = {
922 | First = {First = "Hello"};
923 | Second = {First = true, Second = false};
924 | Third = 3;
925 | Fourth = {"One", 1, "Two", 2, "Three", {3, 4, 5, 6}, 7};
926 | };
927 | TestKey3 = 12345;
928 | }
929 |
930 | local scope4 = {
931 | TestKey1 = 1;
932 | TestKey2 = 2;
933 | TestKey3 = 3;
934 | }
935 |
936 | local scope5 = {
937 | TestImportKey1 = -5.1;
938 | TestImportKey2 = "Test string";
939 | TestImportKey3 = {};
940 | }
941 |
942 | describe("MockGlobalDataStore::ImportFromJSON/ExportToJSON", function()
943 |
944 | it("should import keys correctly", function()
945 | Test.reset()
946 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
947 |
948 | expect(function()
949 |
950 | MockGlobalDataStore:ImportFromJSON(scope1, false)
951 | MockGlobalDataStore:ImportFromJSON(scope2, false)
952 | MockGlobalDataStore:ImportFromJSON(scope3, false)
953 | MockGlobalDataStore:ImportFromJSON(scope4, false)
954 | MockGlobalDataStore:ImportFromJSON(scope5, false)
955 |
956 | MockGlobalDataStore:ImportFromJSON(HttpService:JSONEncode(scope1), false)
957 | MockGlobalDataStore:ImportFromJSON(HttpService:JSONEncode(scope2), false)
958 | MockGlobalDataStore:ImportFromJSON(HttpService:JSONEncode(scope3), false)
959 | MockGlobalDataStore:ImportFromJSON(HttpService:JSONEncode(scope4), false)
960 | MockGlobalDataStore:ImportFromJSON(HttpService:JSONEncode(scope5), false)
961 |
962 | end).never.to.throw()
963 |
964 | end)
965 |
966 | it("should contain all imported values afterwards", function()
967 | Test.reset()
968 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
969 |
970 | MockGlobalDataStore:ImportFromJSON(scope3, false)
971 | MockGlobalDataStore:ImportFromJSON(scope5, false)
972 |
973 | local exported = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
974 | expect(Test.subsetOf(scope3, exported)).to.equal(true)
975 | expect(Test.subsetOf(scope5, exported)).to.equal(true)
976 |
977 | end)
978 |
979 | it("should fire OnUpdate signals", function()
980 | --TODO
981 | end)
982 |
983 | it("should ignore invalid values and keys", function()
984 | Test.reset()
985 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
986 |
987 | local partiallyValid = {
988 | TestKey1 = 1;
989 | TestKey2 = true;
990 | TestKey3 = "Test";
991 | TestKey4 = {1,2,3,4};
992 | TestKey5 = {}; -- will loop
993 | TestKey6 = 6;
994 | [true] = 7;
995 | [123] = "Hello";
996 | TestKey8 = Instance.new("Frame");
997 | TestKey9 = math.huge;
998 | TestKey10 = -math.huge;
999 | TestKey11 = 11;
1000 | TestKey12 = function() end;
1001 | TestKey13 = {a = 1, 2, 3};
1002 | TestKey14 = {[1] = 1, [2] = 2, [4] = 3};
1003 | TestKey15 = {a = {1,2,3}, b = 4, c = {{1,2},3,4,5,{6,7}}, d = "Testing"};
1004 | TestKey16 = ("a"):rep(Test.Constants.MAX_LENGTH_DATA + 1);
1005 | TestKey17 = {a = {1,2,3}, b = 4, c = {{1,2},3,4,5,{6,7, coroutine.create(function() end)}}};
1006 | }
1007 | partiallyValid.TestKey5.loop = partiallyValid.TestKey5
1008 |
1009 | MockGlobalDataStore:ImportFromJSON(partiallyValid)
1010 |
1011 | local exported = HttpService:JSONDecode(MockGlobalDataStore:ExportToJSON())
1012 |
1013 | expect(exported.TestKey1).to.be.ok()
1014 | expect(exported.TestKey2).to.be.ok()
1015 | expect(exported.TestKey3).to.be.ok()
1016 | expect(exported.TestKey4).to.be.ok()
1017 | expect(exported.TestKey5).to.never.be.ok()
1018 | expect(exported.TestKey6).to.be.ok()
1019 | expect(exported[true]).to.never.be.ok()
1020 | expect(exported[123]).to.never.be.ok()
1021 | expect(exported.TestKey8).to.never.be.ok()
1022 | expect(exported.TestKey9).to.never.be.ok()
1023 | expect(exported.TestKey10).to.never.be.ok()
1024 | expect(exported.TestKey11).to.be.ok()
1025 | expect(exported.TestKey12).to.never.be.ok()
1026 | expect(exported.TestKey13).to.never.be.ok()
1027 | expect(exported.TestKey14).to.never.be.ok()
1028 | expect(exported.TestKey15).to.be.ok()
1029 | expect(exported.TestKey16).to.never.be.ok()
1030 | expect(exported.TestKey17).to.never.be.ok()
1031 |
1032 | end)
1033 |
1034 | it("should throw for invalid input", function()
1035 | Test.reset()
1036 | local MockGlobalDataStore = Test.Service:GetDataStore("Test")
1037 |
1038 | expect(function()
1039 | MockGlobalDataStore:ImportFromJSON("{this is invalid json}", false)
1040 | end).to.throw()
1041 |
1042 | expect(function()
1043 | MockGlobalDataStore:ImportFromJSON(123, false)
1044 | end).to.throw()
1045 |
1046 | expect(function()
1047 | MockGlobalDataStore:ImportFromJSON({}, 123)
1048 | end).to.throw()
1049 |
1050 | expect(function()
1051 | MockGlobalDataStore:ImportFromJSON("{}", 123)
1052 | end).to.throw()
1053 |
1054 | end)
1055 |
1056 | end)
1057 |
1058 | end
1059 |
--------------------------------------------------------------------------------
/spec/MockDataStoreService/MockOrderedDataStore.spec.lua:
--------------------------------------------------------------------------------
1 | return function()
2 | local Test = require(script.Parent.Test)
3 | local HttpService = game:GetService("HttpService")
4 |
5 | describe("MockOrderedDataStore", function()
6 |
7 | it("should expose all API members", function()
8 | Test.reset()
9 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test", "Test")
10 |
11 | expect(MockOrderedDataStore.GetAsync).to.be.a("function")
12 | expect(MockOrderedDataStore.IncrementAsync).to.be.a("function")
13 | expect(MockOrderedDataStore.RemoveAsync).to.be.a("function")
14 | expect(MockOrderedDataStore.SetAsync).to.be.a("function")
15 | expect(MockOrderedDataStore.UpdateAsync).to.be.a("function")
16 | expect(MockOrderedDataStore.OnUpdate).to.be.a("function")
17 | expect(MockOrderedDataStore.GetSortedAsync).to.be.a("function")
18 |
19 | end)
20 |
21 | end)
22 |
23 | describe("MockOrderedDataStore::GetAsync", function()
24 |
25 | it("should return nil for non-existing keys", function()
26 | Test.reset()
27 | Test.setStaticBudgets(100)
28 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
29 |
30 | expect(MockOrderedDataStore:GetAsync("TestKey")).to.never.be.ok()
31 |
32 | end)
33 |
34 | it("should return the value for existing keys", function()
35 | Test.reset()
36 | Test.setStaticBudgets(100)
37 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
38 |
39 | MockOrderedDataStore:ImportFromJSON({
40 | TestKey1 = -123;
41 | TestKey2 = 0;
42 | TestKey3 = 291;
43 | })
44 |
45 | expect(MockOrderedDataStore:GetAsync("TestKey1")).to.equal(-123)
46 | expect(MockOrderedDataStore:GetAsync("TestKey2")).to.equal(0)
47 | expect(MockOrderedDataStore:GetAsync("TestKey3")).to.equal(291)
48 |
49 | end)
50 |
51 | it("should consume budgets correctly", function()
52 | Test.reset()
53 | Test.setStaticBudgets(100)
54 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
55 |
56 | Test.captureBudget()
57 |
58 | for i = 1, 10 do
59 | MockOrderedDataStore:GetAsync("TestKey"..i)
60 | expect(Test.checkpointBudget{
61 | [Enum.DataStoreRequestType.GetAsync] = -1
62 | }).to.be.ok()
63 | end
64 |
65 | end)
66 |
67 | it("should throttle requests correctly when out of budget", function()
68 | --TODO
69 | end)
70 |
71 | it("should throw for invalid input", function()
72 | Test.reset()
73 | Test.setStaticBudgets(100)
74 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
75 |
76 | expect(function()
77 | MockOrderedDataStore:GetAsync()
78 | end).to.throw()
79 |
80 | expect(function()
81 | MockOrderedDataStore:GetAsync(123)
82 | end).to.throw()
83 |
84 | expect(function()
85 | MockOrderedDataStore:GetAsync("")
86 | end).to.throw()
87 |
88 | expect(function()
89 | MockOrderedDataStore:GetAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1))
90 | end).to.throw()
91 |
92 | end)
93 |
94 | it("should set the get-cache", function()
95 | Test.reset()
96 | Test.setStaticBudgets(100)
97 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
98 |
99 | MockOrderedDataStore:GetAsync("TestKey")
100 |
101 | Test.captureBudget()
102 |
103 | MockOrderedDataStore:GetAsync("TestKey")
104 |
105 | expect(Test.checkpointBudget{}).to.be.ok()
106 |
107 | end)
108 |
109 | end)
110 |
111 | describe("MockOrderedDataStore::IncrementAsync", function()
112 |
113 | it("should increment keys", function()
114 | Test.reset()
115 | Test.setStaticBudgets(100)
116 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
117 |
118 | MockOrderedDataStore:ImportFromJSON({TestKey = 1})
119 |
120 | MockOrderedDataStore:IncrementAsync("TestKey", 1)
121 |
122 | local export = HttpService:JSONDecode(MockOrderedDataStore:ExportToJSON())
123 |
124 | expect(export.TestKey).to.equal(2)
125 |
126 | end)
127 |
128 | it("should increment non-existing keys", function()
129 | Test.reset()
130 | Test.setStaticBudgets(100)
131 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
132 |
133 | MockOrderedDataStore:IncrementAsync("TestKey", 1)
134 |
135 | local export = HttpService:JSONDecode(MockOrderedDataStore:ExportToJSON())
136 |
137 | expect(export.TestKey).to.equal(1)
138 |
139 | end)
140 |
141 | it("should increment by the correct value", function()
142 | Test.reset()
143 | Test.setStaticBudgets(100)
144 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
145 |
146 | MockOrderedDataStore:ImportFromJSON({TestKey1 = 1, TestKey2 = 2, TestKey3 = 3, TestKey4 = 4, TestKey5 = 5})
147 |
148 | MockOrderedDataStore:IncrementAsync("TestKey1", 19)
149 | MockOrderedDataStore:IncrementAsync("TestKey2", -43)
150 | MockOrderedDataStore:IncrementAsync("TestKey3", 0)
151 | MockOrderedDataStore:IncrementAsync("TestKey4", 1.5)
152 | MockOrderedDataStore:IncrementAsync("TestKey5")
153 |
154 | local export = HttpService:JSONDecode(MockOrderedDataStore:ExportToJSON())
155 |
156 | expect(export.TestKey1).to.equal(20)
157 | expect(export.TestKey2).to.equal(-41)
158 | expect(export.TestKey3).to.equal(3)
159 | expect(export.TestKey4).to.equal(6)
160 | expect(export.TestKey5).to.equal(6)
161 |
162 | end)
163 |
164 | it("should return the incremented value", function()
165 | Test.reset()
166 | Test.setStaticBudgets(100)
167 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
168 |
169 | MockOrderedDataStore:ImportFromJSON({TestKey6 = 1938, TestKey7 = 42})
170 |
171 | expect(MockOrderedDataStore:IncrementAsync("TestKey1")).to.be.a("number")
172 | expect(MockOrderedDataStore:IncrementAsync("TestKey2", 100)).to.be.a("number")
173 | expect(MockOrderedDataStore:IncrementAsync("TestKey3", -100)).to.be.a("number")
174 | expect(MockOrderedDataStore:IncrementAsync("TestKey4", 0)).to.be.a("number")
175 | expect(MockOrderedDataStore:IncrementAsync("TestKey5", 1.5)).to.be.a("number")
176 | expect(MockOrderedDataStore:IncrementAsync("TestKey6")).to.be.a("number")
177 | expect(MockOrderedDataStore:IncrementAsync("TestKey7", 1083)).to.be.a("number")
178 |
179 | end)
180 |
181 | it("should consume budgets correctly", function()
182 | Test.reset()
183 | Test.setStaticBudgets(100)
184 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
185 |
186 | MockOrderedDataStore:ImportFromJSON({TestKey2 = 10})
187 |
188 | Test.captureBudget()
189 |
190 | MockOrderedDataStore:IncrementAsync("TestKey1")
191 | expect(Test.checkpointBudget{
192 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
193 | }).to.be.ok()
194 |
195 | MockOrderedDataStore:IncrementAsync("TestKey2", 5)
196 | expect(Test.checkpointBudget{
197 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
198 | }).to.be.ok()
199 |
200 | MockOrderedDataStore:IncrementAsync("TestKey3", 0)
201 | expect(Test.checkpointBudget{
202 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
203 | }).to.be.ok()
204 |
205 | MockOrderedDataStore:IncrementAsync("TestKey4", -5)
206 | expect(Test.checkpointBudget{
207 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
208 | }).to.be.ok()
209 |
210 | end)
211 |
212 | it("should throttle requests correctly when out of budget", function()
213 | --TODO
214 | end)
215 |
216 | it("should throttle requests to respect write cooldown", function()
217 | --TODO
218 | end)
219 |
220 | it("should throw for invalid input", function()
221 | Test.reset()
222 | Test.setStaticBudgets(100)
223 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
224 |
225 | expect(function()
226 | MockOrderedDataStore:IncrementAsync()
227 | end).to.throw()
228 |
229 | expect(function()
230 | MockOrderedDataStore:IncrementAsync(123)
231 | end).to.throw()
232 |
233 | expect(function()
234 | MockOrderedDataStore:IncrementAsync("")
235 | end).to.throw()
236 |
237 | expect(function()
238 | MockOrderedDataStore:IncrementAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1))
239 | end).to.throw()
240 |
241 | expect(function()
242 | MockOrderedDataStore:IncrementAsync("Test", "Not A Number")
243 | end).to.throw()
244 |
245 | expect(function()
246 | MockOrderedDataStore:IncrementAsync(123, 1)
247 | end).to.throw()
248 |
249 | end)
250 |
251 | it("should set the get-cache", function()
252 | Test.reset()
253 | Test.setStaticBudgets(100)
254 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
255 |
256 | MockOrderedDataStore:IncrementAsync("TestKey")
257 |
258 | Test.captureBudget()
259 |
260 | MockOrderedDataStore:GetAsync("TestKey")
261 |
262 | expect(Test.checkpointBudget{}).to.be.ok()
263 |
264 | end)
265 |
266 | end)
267 |
268 | describe("MockOrderedDataStore::RemoveAsync", function()
269 |
270 | it("should be able to remove existing keys", function()
271 | Test.reset()
272 | Test.setStaticBudgets(100)
273 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
274 |
275 | MockOrderedDataStore:ImportFromJSON({ExistingKey = 1})
276 |
277 | MockOrderedDataStore:RemoveAsync("ExistingKey")
278 |
279 | local export = HttpService:JSONDecode(MockOrderedDataStore:ExportToJSON())
280 | expect(export.ExistingKey).to.never.be.ok()
281 |
282 | end)
283 |
284 | it("should be able to remove non-existing keys", function()
285 | Test.reset()
286 | Test.setStaticBudgets(100)
287 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
288 |
289 | MockOrderedDataStore:RemoveAsync("NonExistingKey")
290 |
291 | local export = HttpService:JSONDecode(MockOrderedDataStore:ExportToJSON())
292 | expect(export.NonExistingKey).to.never.be.ok()
293 |
294 | end)
295 |
296 | it("should return the old value", function()
297 | Test.reset()
298 | Test.setStaticBudgets(100)
299 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
300 |
301 | local values = {
302 | TestKey1 = 123;
303 | }
304 |
305 | MockOrderedDataStore:ImportFromJSON(values)
306 |
307 | expect(MockOrderedDataStore:RemoveAsync("TestKey1")).to.equal(values.TestKey1)
308 |
309 | end)
310 |
311 | it("should consume budgets correctly", function()
312 | Test.reset()
313 | Test.setStaticBudgets(100)
314 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
315 |
316 | MockOrderedDataStore:ImportFromJSON({ExistingKey = 42})
317 |
318 | Test.captureBudget()
319 |
320 | MockOrderedDataStore:RemoveAsync("NonExistingKey")
321 |
322 | expect(Test.checkpointBudget{
323 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1;
324 | }).to.be.ok()
325 |
326 | MockOrderedDataStore:RemoveAsync("ExistingKey")
327 |
328 | expect(Test.checkpointBudget{
329 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1;
330 | }).to.be.ok()
331 |
332 | end)
333 |
334 | it("should throttle requests correctly when out of budget", function()
335 | --TODO
336 | end)
337 |
338 | it("should throttle requests to respect write cooldown", function()
339 | --TODO
340 | end)
341 |
342 | it("should throw for invalid input", function()
343 | Test.reset()
344 | Test.setStaticBudgets(100)
345 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
346 |
347 | expect(function()
348 | MockOrderedDataStore:RemoveAsync()
349 | end).to.throw()
350 |
351 | expect(function()
352 | MockOrderedDataStore:RemoveAsync(123)
353 | end).to.throw()
354 |
355 | expect(function()
356 | MockOrderedDataStore:RemoveAsync("")
357 | end).to.throw()
358 |
359 | expect(function()
360 | MockOrderedDataStore:RemoveAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1))
361 | end).to.throw()
362 |
363 | end)
364 |
365 | it("should not set the get-cache", function()
366 | Test.reset()
367 | Test.setStaticBudgets(100)
368 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
369 |
370 | MockOrderedDataStore:RemoveAsync("TestKey")
371 |
372 | Test.captureBudget()
373 |
374 | MockOrderedDataStore:GetAsync("TestKey")
375 |
376 | expect(Test.checkpointBudget{
377 | [Enum.DataStoreRequestType.GetAsync] = -1;
378 | }).to.equal(true)
379 |
380 | end)
381 |
382 | end)
383 |
384 | describe("MockOrderedDataStore::SetAsync", function()
385 |
386 | it("should set keys if value is valid", function()
387 | Test.reset()
388 | Test.setStaticBudgets(100)
389 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
390 |
391 | MockOrderedDataStore:ImportFromJSON({TestKey2 = 42})
392 |
393 | MockOrderedDataStore:SetAsync("TestKey1", 100)
394 | MockOrderedDataStore:SetAsync("TestKey2", 200)
395 | MockOrderedDataStore:SetAsync("TestKey3", -300)
396 | MockOrderedDataStore:SetAsync("TestKey4", 0)
397 |
398 | local exported = HttpService:JSONDecode(MockOrderedDataStore:ExportToJSON())
399 | expect(exported.TestKey1).to.equal(100)
400 | expect(exported.TestKey2).to.equal(200)
401 | expect(exported.TestKey3).to.equal(-300)
402 | expect(exported.TestKey4).to.equal(0)
403 |
404 | end)
405 |
406 | it("should not return anything", function()
407 | Test.reset()
408 | Test.setStaticBudgets(100)
409 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
410 |
411 | MockOrderedDataStore:ImportFromJSON({TestKey2 = 42})
412 |
413 | expect(MockOrderedDataStore:SetAsync("TestKey1", 100)).to.never.be.ok()
414 | expect(MockOrderedDataStore:SetAsync("TestKey2", -100)).to.never.be.ok()
415 | expect(MockOrderedDataStore:SetAsync("TestKey3", 0)).to.never.be.ok()
416 |
417 | end)
418 |
419 | it("should consume budgets correctly", function()
420 | Test.reset()
421 | Test.setStaticBudgets(100)
422 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
423 |
424 | Test.captureBudget()
425 |
426 | MockOrderedDataStore:SetAsync("TestKey1", 123)
427 | expect(Test.checkpointBudget{
428 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
429 | }).to.be.ok()
430 |
431 | MockOrderedDataStore:SetAsync("TestKey2", -123)
432 | expect(Test.checkpointBudget{
433 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
434 | }).to.be.ok()
435 |
436 | MockOrderedDataStore:SetAsync("TestKey3", 0)
437 | expect(Test.checkpointBudget{
438 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1
439 | }).to.be.ok()
440 |
441 | end)
442 |
443 | it("should throttle requests correctly when out of budget", function()
444 | --TODO
445 | end)
446 |
447 | it("should throttle requests to respect write cooldown", function()
448 | --TODO
449 | end)
450 |
451 | it("should throw for invalid input", function()
452 | Test.reset()
453 | Test.setStaticBudgets(100)
454 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
455 |
456 | expect(function()
457 | MockOrderedDataStore:SetAsync()
458 | end).to.throw()
459 |
460 | expect(function()
461 | MockOrderedDataStore:SetAsync(nil, 42)
462 | end).to.throw()
463 |
464 | expect(function()
465 | MockOrderedDataStore:SetAsync(123, 42)
466 | end).to.throw()
467 |
468 | expect(function()
469 | MockOrderedDataStore:SetAsync("", 42)
470 | end).to.throw()
471 |
472 | expect(function()
473 | MockOrderedDataStore:SetAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1), 42)
474 | end).to.throw()
475 |
476 | end)
477 |
478 | it("should throw at attempts to store invalid data", function()
479 | Test.reset()
480 | Test.setStaticBudgets(100)
481 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
482 |
483 | local function testValue(v)
484 | expect(function()
485 | MockOrderedDataStore:SetAsync("TestKey", v)
486 | end).to.throw()
487 | end
488 |
489 | testValue(nil)
490 | testValue("string")
491 | testValue({})
492 | testValue(true)
493 | testValue(function() end)
494 | testValue(Instance.new("Frame"))
495 | testValue(Enum.DataStoreRequestType.GetAsync)
496 | testValue(coroutine.create(function() end))
497 | testValue(10.23)
498 | testValue(math.huge)
499 | testValue(-math.huge)
500 | testValue(-294.4)
501 |
502 | end)
503 |
504 | it("should not set the get-cache", function()
505 | Test.reset()
506 | Test.setStaticBudgets(100)
507 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
508 |
509 | MockOrderedDataStore:SetAsync("TestKey", 1)
510 |
511 | Test.captureBudget()
512 |
513 | MockOrderedDataStore:GetAsync("TestKey")
514 |
515 | expect(Test.checkpointBudget{
516 | [Enum.DataStoreRequestType.GetAsync] = -1;
517 | }).to.equal(true)
518 |
519 | end)
520 |
521 | end)
522 |
523 | describe("MockOrderedDataStore::UpdateAsync", function()
524 |
525 | it("should update keys correctly", function()
526 | Test.reset()
527 | Test.setStaticBudgets(100)
528 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
529 |
530 | MockOrderedDataStore:ImportFromJSON({TestKey2 = 42})
531 |
532 | MockOrderedDataStore:UpdateAsync("TestKey1", function() return 123 end)
533 | MockOrderedDataStore:UpdateAsync("TestKey2", function() return 456 end)
534 |
535 | local exported = HttpService:JSONDecode(MockOrderedDataStore:ExportToJSON())
536 | expect(exported.TestKey1).to.equal(123)
537 | expect(exported.TestKey2).to.equal(456)
538 |
539 | end)
540 |
541 | it("should return the updated value", function()
542 | Test.reset()
543 | Test.setStaticBudgets(100)
544 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
545 |
546 | expect(MockOrderedDataStore:UpdateAsync("TestKey", function() return 13 end)).to.equal(13)
547 |
548 | end)
549 |
550 | it("should pass the old value to the callback", function()
551 | Test.reset()
552 | Test.setStaticBudgets(100)
553 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
554 |
555 | local oldValues = {
556 | TestKey1 = 0;
557 | TestKey2 = 100;
558 | TestKey3 = -100;
559 | }
560 |
561 | MockOrderedDataStore:ImportFromJSON(oldValues)
562 |
563 | for key, value in pairs(oldValues) do
564 | expect(MockOrderedDataStore:UpdateAsync(key, function(oldValue)
565 | if oldValue == value then
566 | return oldValue
567 | end
568 | error()
569 | end)).never.to.throw()
570 | end
571 |
572 | end)
573 |
574 | it("should consume budgets correctly", function()
575 | Test.reset()
576 | Test.setStaticBudgets(100)
577 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
578 |
579 | Test.captureBudget()
580 |
581 | for i = 1, 10 do
582 | MockOrderedDataStore:UpdateAsync("TestKey"..i, function() return 1 end)
583 | expect(Test.checkpointBudget{
584 | [Enum.DataStoreRequestType.GetAsync] = -1;
585 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1;
586 | }).to.be.ok()
587 | end
588 |
589 | end)
590 |
591 | it("should throttle requests correctly when out of budget", function()
592 | --TODO
593 | end)
594 |
595 | it("should throttle requests to respect write cooldown", function()
596 | --TODO
597 | end)
598 |
599 | it("should throw for invalid key", function()
600 | Test.reset()
601 | Test.setStaticBudgets(100)
602 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
603 |
604 | local func = function() return 1 end
605 |
606 | expect(function()
607 | MockOrderedDataStore:UpdateAsync()
608 | end).to.throw()
609 |
610 | expect(function()
611 | MockOrderedDataStore:UpdateAsync(nil, func)
612 | end).to.throw()
613 |
614 | expect(function()
615 | MockOrderedDataStore:UpdateAsync(123, func)
616 | end).to.throw()
617 |
618 | expect(function()
619 | MockOrderedDataStore:UpdateAsync("", func)
620 | end).to.throw()
621 |
622 | expect(function()
623 | MockOrderedDataStore:UpdateAsync(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1), func)
624 | end).to.throw()
625 |
626 | end)
627 |
628 | it("should throw at attempts to store invalid data", function()
629 | Test.reset()
630 | Test.setStaticBudgets(100)
631 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
632 |
633 | local function testValue(v)
634 | expect(function()
635 | MockOrderedDataStore:UpdateAsync("TestKey", function() return v end)
636 | end).to.throw()
637 | end
638 |
639 | testValue(nil)
640 | testValue("string")
641 | testValue({})
642 | testValue(true)
643 | testValue(function() end)
644 | testValue(Instance.new("Frame"))
645 | testValue(Enum.DataStoreRequestType.GetAsync)
646 | testValue(coroutine.create(function() end))
647 | testValue(10.23)
648 | testValue(math.huge)
649 | testValue(-math.huge)
650 | testValue(-294.4)
651 |
652 | end)
653 |
654 | it("should set the get-cache", function()
655 | Test.reset()
656 | Test.setStaticBudgets(100)
657 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
658 |
659 | Test.captureBudget()
660 |
661 | MockOrderedDataStore:UpdateAsync("TestKey", function() return 1 end)
662 | MockOrderedDataStore:GetAsync("TestKey")
663 |
664 | expect(Test.checkpointBudget{
665 | [Enum.DataStoreRequestType.GetAsync] = -1;
666 | [Enum.DataStoreRequestType.SetIncrementAsync] = -1;
667 | [Enum.DataStoreRequestType.UpdateAsync] = -1;
668 | }).to.equal(true)
669 |
670 | end)
671 |
672 | end)
673 |
674 | describe("MockOrderedDataStore::OnUpdate", function()
675 |
676 | it("should return a RBXScriptConnection", function()
677 | Test.reset()
678 | Test.setStaticBudgets(100)
679 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
680 |
681 | local conn = MockOrderedDataStore:OnUpdate("TestKey")
682 |
683 | conn:Disconnect() -- don't leak after test
684 |
685 | expect(conn).to.be.a("RBXScriptConnection")
686 |
687 | end)
688 |
689 | it("should only receives updates for its connected key", function()
690 | --TODO
691 | end)
692 |
693 | it("should work with SetAsync", function()
694 | --TODO
695 | end)
696 |
697 | it("should work with UpdateAsync", function()
698 | --TODO
699 | end)
700 |
701 | it("should work with RemoveAsync", function()
702 | --TODO
703 | end)
704 |
705 | it("should work with IncrementAsync", function()
706 | --TODO
707 | end)
708 |
709 | it("should consume budgets correctly", function()
710 | Test.reset()
711 | Test.setStaticBudgets(100)
712 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
713 |
714 | Test.captureBudget()
715 |
716 | for i = 1, 10 do
717 | local conn = MockOrderedDataStore:OnUpdate("TestKey"..i, function() end)
718 | conn:Disconnect()
719 | expect(Test.checkpointBudget{
720 | [Enum.DataStoreRequestType.OnUpdate] = -1;
721 | }).to.be.ok()
722 | end
723 |
724 | end)
725 |
726 | it("should throttle requests correctly when out of budget", function()
727 | --TODO
728 | end)
729 |
730 | it("should throw for invalid input", function()
731 | Test.reset()
732 | Test.setStaticBudgets(100)
733 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
734 |
735 | expect(function()
736 | MockOrderedDataStore:OnUpdate()
737 | end).to.throw()
738 |
739 | expect(function()
740 | MockOrderedDataStore:OnUpdate(123)
741 | end).to.throw()
742 |
743 | expect(function()
744 | MockOrderedDataStore:OnUpdate("")
745 | end).to.throw()
746 |
747 | expect(function()
748 | MockOrderedDataStore:OnUpdate(("a"):rep(Test.Constants.MAX_LENGTH_KEY + 1))
749 | end).to.throw()
750 |
751 | expect(function()
752 | MockOrderedDataStore:OnUpdate("Test", 123)
753 | end).to.throw()
754 |
755 | expect(function()
756 | MockOrderedDataStore:OnUpdate(123, function() end)
757 | end).to.throw()
758 |
759 | end)
760 |
761 | it("should not set the get-cache", function()
762 | Test.reset()
763 | Test.setStaticBudgets(100)
764 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
765 |
766 | local connection = MockOrderedDataStore:OnUpdate("TestKey", function() end)
767 |
768 | Test.captureBudget()
769 |
770 | local result = expect(function()
771 | MockOrderedDataStore:GetAsync("TestKey")
772 | end)
773 |
774 | if connection then
775 | connection:Disconnect()
776 | end
777 |
778 | expect(result.never.to.throw())
779 | expect(Test.checkpointBudget{
780 | [Enum.DataStoreRequestType.GetAsync] = -1;
781 | }).to.be.ok()
782 |
783 | end)
784 |
785 | end)
786 |
787 | describe("MockOrderedDataStore::GetSortedAsync", function()
788 |
789 | it("should complete successfully and return object for valid input", function()
790 | Test.reset()
791 | Test.setStaticBudgets(100)
792 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
793 |
794 | expect(MockOrderedDataStore:GetSortedAsync(true, 50)).to.be.ok()
795 | expect(MockOrderedDataStore:GetSortedAsync(true, 50, 100)).to.be.ok()
796 | expect(MockOrderedDataStore:GetSortedAsync(true, 50, nil, 100)).to.be.ok()
797 | expect(MockOrderedDataStore:GetSortedAsync(true, 50, 100, 200)).to.be.ok()
798 |
799 | expect(MockOrderedDataStore:GetSortedAsync(false, 50)).to.be.ok()
800 | expect(MockOrderedDataStore:GetSortedAsync(false, 50, 100)).to.be.ok()
801 | expect(MockOrderedDataStore:GetSortedAsync(false, 50, nil, 100)).to.be.ok()
802 | expect(MockOrderedDataStore:GetSortedAsync(false, 50, 100, 200)).to.be.ok()
803 |
804 | end)
805 |
806 | it("should consume budgets correctly", function()
807 | Test.reset()
808 | Test.setStaticBudgets(100)
809 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
810 |
811 | Test.captureBudget()
812 |
813 | MockOrderedDataStore:GetSortedAsync(true, 100) -- empty results
814 | expect(Test.checkpointBudget{
815 | [Enum.DataStoreRequestType.GetSortedAsync] = -1;
816 | }).to.be.ok()
817 |
818 | local values = {}
819 | for i = 1, 789 do
820 | values["TestKey"..i] = i
821 | end
822 |
823 | MockOrderedDataStore:ImportFromJSON(values)
824 |
825 | MockOrderedDataStore:GetSortedAsync(false, 110, 230, 720) -- not empty
826 | expect(Test.checkpointBudget{
827 | [Enum.DataStoreRequestType.GetSortedAsync] = -1;
828 | }).to.be.ok()
829 |
830 | end)
831 |
832 | it("should throttle requests correctly when out of budget", function()
833 | --TODO
834 | end)
835 |
836 | it("should throw for invalid input", function()
837 | Test.reset()
838 | Test.setStaticBudgets(100)
839 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test")
840 |
841 | expect(function()
842 | MockOrderedDataStore:GetSortedAsync()
843 | end).to.throw()
844 |
845 | expect(function()
846 | MockOrderedDataStore:GetSortedAsync("wrong type", 50)
847 | end).to.throw()
848 |
849 | expect(function()
850 | MockOrderedDataStore:GetSortedAsync(true)
851 | end).to.throw()
852 |
853 | expect(function()
854 | MockOrderedDataStore:GetSortedAsync(false, "wrong type")
855 | end).to.throw()
856 |
857 | expect(function()
858 | MockOrderedDataStore:GetSortedAsync(true, -5)
859 | end).to.throw()
860 |
861 | expect(function()
862 | MockOrderedDataStore:GetSortedAsync(false, 0)
863 | end).to.throw()
864 |
865 | expect(function()
866 | MockOrderedDataStore:GetSortedAsync(true, Test.Constants.MAX_PAGE_SIZE + 1)
867 | end).to.throw()
868 |
869 | expect(function()
870 | MockOrderedDataStore:GetSortedAsync(false, 56.7)
871 | end).to.throw()
872 |
873 | expect(function()
874 | MockOrderedDataStore:GetSortedAsync(false, 50, "wrong type")
875 | end).to.throw()
876 |
877 | expect(function()
878 | MockOrderedDataStore:GetSortedAsync(false, 50, 102.9)
879 | end).to.throw()
880 |
881 | expect(function()
882 | MockOrderedDataStore:GetSortedAsync(true, 50, 100, "wrong type")
883 | end).to.throw()
884 |
885 | expect(function()
886 | MockOrderedDataStore:GetSortedAsync(true, 50, 100, 472.39)
887 | end).to.throw()
888 |
889 | expect(function()
890 | MockOrderedDataStore:GetSortedAsync(true, 50, 100, 99)
891 | end).to.throw()
892 |
893 | end)
894 |
895 | -- See more testing with ordered data lookups in MockDataStorePages.spec.lua!
896 |
897 | end)
898 |
899 | describe("MockOrderedDataStore::ImportFromJSON/ExportToJSON", function()
900 |
901 | it("should import keys correctly", function()
902 | Test.reset()
903 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test", "Test")
904 |
905 | local scope = {
906 | TestKey1 = 1;
907 | TestKey2 = 2;
908 | TestKey3 = 3;
909 | }
910 |
911 | expect(function()
912 | MockOrderedDataStore:ImportFromJSON(scope, false)
913 | MockOrderedDataStore:ImportFromJSON(HttpService:JSONEncode(scope), false)
914 | end).never.to.throw()
915 |
916 | end)
917 |
918 | it("should contain all imported values afterwards", function()
919 | Test.reset()
920 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test", "Test")
921 |
922 | local data = {}
923 | for i = 1, 100 do
924 | data["TestKey"..i] = i
925 | end
926 |
927 | MockOrderedDataStore:ImportFromJSON(data, false)
928 |
929 | local exported = HttpService:JSONDecode(MockOrderedDataStore:ExportToJSON())
930 | for i = 1, 100 do
931 | expect(exported["TestKey"..i]).to.equal(i)
932 | end
933 |
934 | end)
935 |
936 | it("should fire OnUpdate signals", function()
937 | --TODO
938 | end)
939 |
940 | it("should ignore invalid values and keys", function()
941 | Test.reset()
942 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test", "Test")
943 |
944 | local data = {
945 | TestKey1 = 1;
946 | TestKey2 = true;
947 | TestKey3 = "Test";
948 | TestKey4 = {1,2,3,4};
949 | TestKey5 = 5;
950 | TestKey6 = 6;
951 | [true] = 7;
952 | TestKey8 = Instance.new("Frame");
953 | TestKey9 = math.huge;
954 | TestKey10 = -math.huge;
955 | TestKey11 = 11;
956 | }
957 |
958 | MockOrderedDataStore:ImportFromJSON(data, false)
959 |
960 | local store = HttpService:JSONDecode(MockOrderedDataStore:ExportToJSON())
961 | expect(store.TestKey1).to.equal(1)
962 | expect(store.TestKey2).to.never.be.ok()
963 | expect(store.TestKey3).to.never.be.ok()
964 | expect(store.TestKey4).to.never.be.ok()
965 | expect(store.TestKey5).to.equal(5)
966 | expect(store.TestKey6).to.equal(6)
967 | expect(store[true]).to.never.be.ok()
968 | expect(store.TestKey8).to.never.be.ok()
969 | expect(store.TestKey9).to.never.be.ok()
970 | expect(store.TestKey10).to.never.be.ok()
971 | expect(store.TestKey11).to.equal(11)
972 |
973 | end)
974 |
975 | it("should throw for invalid input", function()
976 | Test.reset()
977 | local MockOrderedDataStore = Test.Service:GetOrderedDataStore("Test", "Test")
978 |
979 | expect(function()
980 | MockOrderedDataStore:ImportFromJSON("{this is invalid json}", false)
981 | end).to.throw()
982 |
983 | expect(function()
984 | MockOrderedDataStore:ImportFromJSON(123, false)
985 | end).to.throw()
986 |
987 | expect(function()
988 | MockOrderedDataStore:ImportFromJSON({}, 123)
989 | end).to.throw()
990 |
991 | expect(function()
992 | MockOrderedDataStore:ImportFromJSON("{}", 123)
993 | end).to.throw()
994 |
995 | end)
996 |
997 | end)
998 |
999 | end
1000 |
--------------------------------------------------------------------------------
/spec/MockDataStoreService/Test.lua:
--------------------------------------------------------------------------------
1 | local Test = {}
2 |
3 | local MockDataStoreService_Module = script.Parent.Parent.Parent.DataStoreService.MockDataStoreService
4 |
5 | Test.Service = require(MockDataStoreService_Module)
6 | Test.Constants = require(MockDataStoreService_Module.MockDataStoreConstants)
7 | Test.Manager = require(MockDataStoreService_Module.MockDataStoreManager)
8 | Test.Utils = require(MockDataStoreService_Module.MockDataStoreUtils)
9 | Test.Pages = require(MockDataStoreService_Module.MockDataStorePages)
10 |
11 | Test.Constants.YIELD_TIME_MIN = 0
12 | Test.Constants.YIELD_TIME_MAX = 0
13 | Test.Constants.YIELD_TIME_UPDATE_MIN = 0
14 | Test.Constants.YIELD_TIME_UPDATE_MAX = 0
15 |
16 | local capturedBudgets = {}
17 |
18 | function Test.reset()
19 | Test.Manager.ResetData()
20 | Test.Manager.ResetBudget()
21 | Test.Manager.ThawBudgetUpdates()
22 | capturedBudgets = {}
23 | end
24 |
25 | function Test.subsetOf(t1, t2)
26 | if type(t1) ~= "table" or type(t2) ~= "table" then
27 | return t1 == t2
28 | end
29 | for key, value in pairs(t1) do
30 | if type(value) == "table" then
31 | if type(t2[key]) == "table" then
32 | if not Test.subsetOf(t1[key], t2[key]) then
33 | return false
34 | end
35 | else
36 | return false
37 | end
38 | elseif t1[key] ~= t2[key] then
39 | return false
40 | end
41 | end
42 | return true
43 | end
44 |
45 | function Test.setStaticBudgets(var)
46 | Test.Manager.FreezeBudgetUpdates()
47 | if type(var) == "number" then
48 | local budget = var
49 | for _,v in pairs(Enum.DataStoreRequestType:GetEnumItems()) do
50 | Test.Manager.SetBudget(v, budget)
51 | end
52 | elseif type(var) == "table" then
53 | local budgets = var
54 | for requestType, budget in pairs(budgets) do
55 | Test.Manager.SetBudget(requestType, budget)
56 | end
57 | end
58 | end
59 |
60 | function Test.captureBudget()
61 | for _,v in pairs(Enum.DataStoreRequestType:GetEnumItems()) do
62 | if v ~= Enum.DataStoreRequestType.UpdateAsync then
63 | capturedBudgets[v] = Test.Manager.GetBudget(v)
64 | end
65 | end
66 | end
67 |
68 | function Test.checkpointBudget(checkpoint)
69 | local match = true
70 | for requestType, difference in pairs(checkpoint) do
71 | if Test.Manager.GetBudget(requestType) - capturedBudgets[requestType] ~= difference then
72 | match = nil
73 | break
74 | end
75 | capturedBudgets[requestType] = nil
76 | end
77 | if match then
78 | for requestType, budget in pairs(capturedBudgets) do
79 | if Test.Manager.GetBudget(requestType) ~= budget then
80 | match = nil
81 | end
82 | end
83 | end
84 | Test.captureBudget()
85 | return match
86 | end
87 |
88 | return Test
89 |
--------------------------------------------------------------------------------
/spec/MockDataStoreService/init.spec.lua:
--------------------------------------------------------------------------------
1 | return function()
2 | local Test = require(script.Parent.Test)
3 | local HttpService = game:GetService("HttpService")
4 |
5 | local function reset()
6 | Test.Manager.ResetData()
7 | Test.Manager.ResetBudget()
8 | Test.Manager.ThawBudgetUpdates()
9 | end
10 |
11 | describe("MockDataStoreService", function()
12 |
13 | it("should expose all API members", function()
14 | expect(Test.Service.GetDataStore).to.be.a("function")
15 | expect(Test.Service.GetGlobalDataStore).to.be.a("function")
16 | expect(Test.Service.GetOrderedDataStore).to.be.a("function")
17 | expect(Test.Service.GetRequestBudgetForRequestType).to.be.a("function")
18 | expect(Test.Service.ImportFromJSON).to.be.a("function")
19 | expect(Test.Service.ExportFromJSON).to.be.a("function")
20 | end)
21 |
22 | end)
23 |
24 | describe("Test.Service::GetDataStore", function()
25 |
26 | it("should return an object for valid input", function()
27 | expect(Test.Service:GetDataStore("Test")).to.be.ok()
28 | expect(Test.Service:GetDataStore("Test2", "Test2")).to.be.ok()
29 | end)
30 |
31 | it("should throw for invalid input", function()
32 |
33 | expect(function()
34 | Test.Service:GetDataStore()
35 | end).to.throw()
36 |
37 | expect(function()
38 | Test.Service:GetDataStore(nil, "Test")
39 | end).to.throw()
40 |
41 | expect(function()
42 | Test.Service:GetDataStore("Test", 123)
43 | end).to.throw()
44 |
45 | expect(function()
46 | Test.Service:GetDataStore(("a"):rep(Test.Constants.MAX_LENGTH_NAME + 1), "Test")
47 | end).to.throw()
48 |
49 | expect(function()
50 | Test.Service:GetDataStore(123, "Test")
51 | end).to.throw()
52 |
53 | expect(function()
54 | Test.Service:GetDataStore("Test", ("a"):rep(Test.Constants.MAX_LENGTH_SCOPE + 1))
55 | end).to.throw()
56 |
57 | expect(function()
58 | Test.Service:GetDataStore("", "Test")
59 | end).to.throw()
60 |
61 | expect(function()
62 | Test.Service:GetDataStore("Test", "")
63 | end).to.throw()
64 |
65 | end)
66 |
67 | end)
68 |
69 | describe("Test.Service::GetGlobalDataStore", function()
70 |
71 | it("should return an object", function()
72 | expect(Test.Service:GetGlobalDataStore()).to.be.ok()
73 | end)
74 |
75 | end)
76 |
77 | describe("Test.Service::GetOrderedDataStore", function()
78 |
79 | it("should return an object for valid input", function()
80 | expect(Test.Service:GetOrderedDataStore("Test")).to.be.ok()
81 | expect(Test.Service:GetOrderedDataStore("Test2", "Test2")).to.be.ok()
82 | end)
83 |
84 | it("should throw for invalid input", function()
85 |
86 | expect(function()
87 | Test.Service:GetOrderedDataStore()
88 | end).to.throw()
89 |
90 | expect(function()
91 | Test.Service:GetOrderedDataStore(nil, "Test")
92 | end).to.throw()
93 |
94 | expect(function()
95 | Test.Service:GetOrderedDataStore("Test", 123)
96 | end).to.throw()
97 |
98 | expect(function()
99 | Test.Service:GetOrderedDataStore(("a"):rep(51), "Test")
100 | end).to.throw()
101 |
102 | expect(function()
103 | Test.Service:GetOrderedDataStore(123, "Test")
104 | end).to.throw()
105 |
106 | expect(function()
107 | Test.Service:GetOrderedDataStore("Test", ("a"):rep(51))
108 | end).to.throw()
109 |
110 | expect(function()
111 | Test.Service:GetOrderedDataStore("", "Test")
112 | end).to.throw()
113 |
114 | expect(function()
115 | Test.Service:GetOrderedDataStore("Test", "")
116 | end).to.throw()
117 |
118 | end)
119 |
120 | end)
121 |
122 | describe("Test.Service::GetRequestBudgetForRequestType", function()
123 |
124 | it("should return numerical budgets", function()
125 | for _,v in pairs(Enum.DataStoreRequestType:GetEnumItems()) do
126 | expect(Test.Service:GetRequestBudgetForRequestType(v)).to.be.a("number")
127 | end
128 | end)
129 |
130 | it("should accept enumerator values", function()
131 | for _,v in pairs(Enum.DataStoreRequestType:GetEnumItems()) do
132 | expect(Test.Service:GetRequestBudgetForRequestType(v.Value)).to.be.ok()
133 | end
134 | end)
135 |
136 | it("should accept enumerator names", function()
137 | for _,v in pairs(Enum.DataStoreRequestType:GetEnumItems()) do
138 | expect(Test.Service:GetRequestBudgetForRequestType(v.Name)).to.be.ok()
139 | end
140 | end)
141 |
142 | it("should throw for invalid input", function()
143 |
144 | expect(function()
145 | Test.Service:GetRequestBudgetForRequestType("NotARequestType")
146 | end).to.throw()
147 |
148 | expect(function()
149 | Test.Service:GetRequestBudgetForRequestType()
150 | end).to.throw()
151 |
152 | expect(function()
153 | Test.Service:GetRequestBudgetForRequestType(13373)
154 | end).to.throw()
155 |
156 | expect(function()
157 | Test.Service:GetRequestBudgetForRequestType(true)
158 | end).to.throw()
159 |
160 | end)
161 |
162 | end)
163 |
164 | local testDataStores = {
165 | DataStores = {
166 | ImportTestName = {
167 | ImportTestScope = {
168 | TestKey1 = true;
169 | TestKey2 = "Hello world!";
170 | TestKey3 = {First = 1, Second = 2, Third = 3};
171 | TestKey4 = false;
172 | };
173 | ImportTestScope2 = {};
174 | ImportTestScope3 = {
175 | TestKey1 = "Test string";
176 | TestKey2 = {
177 | First = {First = "Hello"};
178 | Second = {First = true, Second = false};
179 | Third = 3;
180 | Fourth = {"One", 1, "Two", 2, "Three", {3, 4, 5, 6}, 7};
181 | };
182 | TestKey3 = 12345;
183 | };
184 | };
185 | ImportTestName2 = {};
186 | };
187 | OrderedDataStores = {
188 | ImportTestName = {
189 | ImportTestScope = {
190 | TestKey1 = 1;
191 | TestKey2 = 2;
192 | TestKey3 = 3;
193 | };
194 | ImportTestScope2 = {
195 | TestKey1 = 100;
196 | TestKey2 = 12308;
197 | TestKey3 = 1288;
198 | TestKey4 = 1287;
199 | };
200 | ImportTestScope3 = {};
201 | };
202 | ImportTestName2 = {
203 | ImportTestScope = {};
204 | };
205 | ImportTestName3 = {};
206 | };
207 | GlobalDataStore = {
208 | TestImportKey1 = -5.1;
209 | TestImportKey2 = "Test string";
210 | TestImportKey3 = {};
211 | };
212 | }
213 |
214 | describe("Test.Service::ImportFromJSON", function()
215 |
216 | it("should import from correct json strings", function()
217 | reset()
218 |
219 | local json = HttpService:JSONEncode(testDataStores)
220 |
221 | expect(function()
222 | Test.Service:ImportFromJSON(json, false)
223 | end).never.to.throw()
224 |
225 | end)
226 |
227 | it("should import from correct table input", function()
228 | reset()
229 |
230 | expect(function()
231 | Test.Service:ImportFromJSON(testDataStores, false)
232 | end).never.to.throw()
233 |
234 | end)
235 |
236 | it("should contain newly imported values after importing", function()
237 | reset()
238 |
239 | Test.Service:ImportFromJSON(testDataStores, false)
240 |
241 | local globalData = Test.Manager.GetGlobalData()
242 | expect(globalData).to.be.ok()
243 | expect(Test.subsetOf(globalData, testDataStores.GlobalDataStore)).to.equal(true)
244 |
245 | for name, scopes in pairs(testDataStores.DataStores) do
246 | for scope, data in pairs(scopes) do
247 | local importedData = Test.Manager.GetData(name, scope)
248 | expect(importedData).to.be.ok()
249 | expect(Test.subsetOf(data, importedData)).to.equal(true)
250 | end
251 | end
252 |
253 | for name, scopes in pairs(testDataStores.OrderedDataStores) do
254 | for scope, data in pairs(scopes) do
255 | local importedData = Test.Manager.GetOrderedData(name, scope)
256 | expect(importedData).to.be.ok()
257 | expect(Test.subsetOf(data, importedData)).to.equal(true)
258 | end
259 | end
260 |
261 | end)
262 |
263 | it("should contain old values after importing new values", function()
264 | reset()
265 |
266 | local oldValues = {
267 | DataStores = {
268 | ImportTestName = {
269 | ImportTestScope = {
270 | TestKey5 = 123;
271 | TestKey6 = {A = "a", B = "b", C = "c"};
272 | };
273 | ImportTestScope2 = {
274 | TestKey1 = "Test";
275 | };
276 | ImportTestScope4 = {
277 | TestKey1 = "Hello world!";
278 | }
279 | };
280 | ImportTestName2 = {
281 | ImportTestScope = {
282 | TestKey1 = 456;
283 | TestKey2 = {1,2,3,4};
284 | };
285 | };
286 | };
287 | GlobalDataStore = {
288 | TestImportKey4 = "Foo";
289 | TestImportKey5 = "Bar";
290 | TestImportKey6 = "Baz";
291 | };
292 | }
293 |
294 | Test.Service:ImportFromJSON(oldValues, false)
295 |
296 | Test.Service:ImportFromJSON(testDataStores, false)
297 |
298 | local globalData = Test.Manager.GetGlobalData()
299 | expect(globalData).to.be.ok()
300 | expect(Test.subsetOf(globalData, oldValues.GlobalDataStore)).to.equal(true)
301 |
302 | for name, scopes in pairs(oldValues.DataStores) do
303 | for scope, data in pairs(scopes) do
304 | local importedData = Test.Manager.GetData(name, scope)
305 | expect(importedData).to.be.ok()
306 | expect(Test.subsetOf(data, importedData)).to.equal(true)
307 | end
308 | end
309 |
310 | end)
311 |
312 | it("should not contain invalid entries from input tables after importing", function()
313 | reset()
314 |
315 | local frame = Instance.new("Frame")
316 | local func = function() end
317 |
318 | local partiallyValid = {
319 | DataStores = {
320 | ImportTestName = {
321 | ImportTestScope = {
322 | TestKey1 = 123;
323 | TestKey2 = {A = "a", "b", C = "c"}; -- mixed table
324 | };
325 | ImportTestScope2 = {
326 | TestKey1 = func; -- invalid type
327 | TestKey2 = "Hello world!";
328 | TestKey3 = frame;
329 | };
330 | ImportTestScope3 = {
331 | TestKey1 = "Hello world!";
332 | [frame] = 123; -- invalid keys
333 | [true] = 456;
334 | [123] = 789;
335 | [func] = "abc";
336 | };
337 | };
338 | ImportTestName2 = {
339 | ImportTestScope = {
340 | TestKey1 = 456;
341 | TestKey2 = {1,2,3,4};
342 | TestKey3 = {[0] = 1, 2, 3}; -- does not start at 1
343 | };
344 | ImportTestScope2 = {
345 | TestKey1 = {[1] = 1, [2] = 2, [4] = 3}; -- holes
346 | TestKey2 = {[1.2] = true}; -- invalid key entry
347 | };
348 | };
349 | };
350 | GlobalDataStore = {
351 | TestKey1 = "Foo";
352 | TestKey2 = "Bar";
353 | TestKey3 = "Baz";
354 | TestKey4 = math.huge; -- invalid value
355 | };
356 | }
357 |
358 | Test.Service:ImportFromJSON(partiallyValid, false)
359 |
360 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope").TestKey1).to.be.ok()
361 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope").TestKey2).to.never.be.ok()
362 |
363 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope2").TestKey1).to.never.be.ok()
364 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope2").TestKey2).to.be.ok()
365 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope2").TestKey3).to.never.be.ok()
366 |
367 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope3").TestKey1).to.be.ok()
368 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope3")[frame]).to.never.be.ok()
369 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope3")[true]).to.never.be.ok()
370 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope3")[123]).to.never.be.ok()
371 | expect(Test.Manager.GetData("ImportTestName", "ImportTestScope3")[func]).to.never.be.ok()
372 |
373 | expect(Test.Manager.GetData("ImportTestName2", "ImportTestScope").TestKey1).to.be.ok()
374 | expect(Test.Manager.GetData("ImportTestName2", "ImportTestScope").TestKey2).to.be.ok()
375 | expect(Test.Manager.GetData("ImportTestName2", "ImportTestScope").TestKey3).to.never.be.ok()
376 |
377 | expect(Test.Manager.GetData("ImportTestName2", "ImportTestScope2").TestKey1).to.never.be.ok()
378 | expect(Test.Manager.GetData("ImportTestName2", "ImportTestScope2").TestKey2).to.never.be.ok()
379 |
380 | expect(Test.Manager.GetGlobalData().TestKey1).to.be.ok()
381 | expect(Test.Manager.GetGlobalData().TestKey1).to.be.ok()
382 | expect(Test.Manager.GetGlobalData().TestKey1).to.be.ok()
383 | expect(Test.Manager.GetGlobalData().TestKey1).to.never.be.ok()
384 |
385 | end)
386 |
387 | it("should throw for invalid input", function()
388 |
389 | expect(function()
390 | Test.Service:ImportFromJSON("{this is invalid json}", false)
391 | end).to.throw()
392 |
393 | expect(function()
394 | Test.Service:ImportFromJSON(123, false)
395 | end).to.throw()
396 |
397 | expect(function()
398 | Test.Service:ImportFromJSON({}, 123)
399 | end).to.throw()
400 |
401 | expect(function()
402 | Test.Service:ImportFromJSON("{}", 123)
403 | end).to.throw()
404 |
405 | end)
406 |
407 | end)
408 |
409 | describe("Test.Service::ExportToJSON", function()
410 |
411 | it("should return valid json", function()
412 | reset()
413 |
414 | Test.Service:ImportFromJSON(testDataStores, false)
415 |
416 | local json = Test.Service:ExportToJSON()
417 |
418 | expect(function()
419 | HttpService:JSONDecode(json)
420 | end).never.to.throw()
421 |
422 | end)
423 |
424 | it("should export all values", function()
425 | reset()
426 |
427 | Test.Service:ImportFromJSON(testDataStores, false)
428 |
429 | local exported = HttpService:JSONDecode(Test.Service:ExportToJSON())
430 |
431 | expect(Test.subsetOf(exported, testDataStores)).to.equal(true)
432 |
433 | end)
434 |
435 | it("should not contain empty datastore scopes", function()
436 | reset()
437 |
438 | Test.Service:ImportFromJSON(testDataStores, false)
439 |
440 | local exported = HttpService:JSONDecode(Test.Service:ExportToJSON())
441 |
442 | expect(exported.DataStores.ImportTestName.ImportTestScope2).to.never.be.ok()
443 | expect(exported.OrderedDataStores.ImportTestName.ImportTestScope3).to.never.be.ok()
444 | expect(exported.OrderedDataStores.ImportTestName2.ImportTestScope).to.never.be.ok()
445 |
446 | end)
447 |
448 | it("should not contain empty datastore names", function()
449 | reset()
450 |
451 | Test.Service:ImportFromJSON(testDataStores, false)
452 |
453 | local exported = HttpService:JSONDecode(Test.Service:ExportToJSON())
454 |
455 | expect(exported.DataStores.ImportTestName2).to.never.be.ok()
456 | expect(exported.OrderedDataStores.ImportTestName3).to.never.be.ok()
457 |
458 | end)
459 |
460 | it("should not contain empty datastore types", function()
461 | reset()
462 |
463 | local exported = HttpService:JSONDecode(Test.Service:ExportToJSON())
464 |
465 | expect(exported.DataStores).to.never.be.ok()
466 | expect(exported.OrderedDataStores).to.never.be.ok()
467 | expect(exported.GlobalDataStore).to.never.be.ok()
468 |
469 | end)
470 |
471 | end)
472 |
473 | end
474 |
--------------------------------------------------------------------------------
/spec/init.spec.lua:
--------------------------------------------------------------------------------
1 | return function()
2 |
3 | describe("DataStoreService", function()
4 |
5 | it("should return MockDataStoreService in a test environment", function()
6 | local DataStoreService = require(script.Parent.Parent.DataStoreService)
7 | local MockDataStoreService = require(script.Parent.Parent.DataStoreService.MockDataStoreService)
8 |
9 | expect(MockDataStoreService).to.be.ok()
10 | expect(MockDataStoreService).to.equal(DataStoreService)
11 | end)
12 |
13 | it("should return the built-in DataStoreService in a live environment", function()
14 | local DataStoreService = require(script.Parent.Parent.DataStoreService)
15 | --TODO
16 | expect(DataStoreService).to.be.ok()
17 | end)
18 |
19 | end)
20 |
21 | end
22 |
--------------------------------------------------------------------------------
/wally.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "buildthomas/mockdatastoreservice"
3 | description = "Emulation of Roblox's DataStoreService for seamless offline development & testing"
4 | version = "1.0.3"
5 | authors = ["buildthomas"]
6 | realm = "shared"
7 | license = "Apache-2.0"
8 | registry = "https://github.com/UpliftGames/wally-index"
9 |
--------------------------------------------------------------------------------