├── Rakefile ├── Gemfile ├── Makefile ├── package.json ├── Gemfile.lock ├── test ├── command │ ├── quit_test.rb │ ├── version_test.rb │ ├── delete_test.rb │ ├── touch_test.rb │ ├── incr_test.rb │ ├── decr_test.rb │ ├── add_test.rb │ ├── cas_test.rb │ ├── append_test.rb │ ├── prepend_test.rb │ ├── gets_test.rb │ ├── set_test.rb │ ├── replace_test.rb │ └── get_test.rb ├── license_test.rb ├── readme_test.rb └── test_helper.rb ├── LICENSE.🍣.md ├── .github └── workflows │ └── test.yml ├── LICENSE.md ├── README.md └── bashcached /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new do |t| 4 | t.pattern = "test/**/*_test.rb" 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "minitest" 6 | gem "rake" 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN ?= bashcached 2 | PREFIX ?= /usr/local 3 | 4 | install: 5 | cp bashcached $(PREFIX)/bin/$(BIN) 6 | 7 | uninstall: 8 | rm -f $(PREFIX)/bin/$(BIN) 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bashcached", 3 | "description": "memcached server built on [bash] + [socat]", 4 | "global": true, 5 | "scripts": [ 6 | "bashcached" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | minitest (5.25.1) 5 | rake (13.2.1) 6 | 7 | PLATFORMS 8 | arm64-darwin-23 9 | ruby 10 | 11 | DEPENDENCIES 12 | minitest 13 | rake 14 | 15 | BUNDLED WITH 16 | 2.6.0.dev 17 | -------------------------------------------------------------------------------- /test/command/quit_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/quit" do 4 | it "terminate the client connection" do 5 | with_bashcached_and_client do |client| 6 | client << "quit\r\n" 7 | _(client.gets).must_be_nil 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/license_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | describe "license" do 4 | it "'bashcached --license' output is same as LICENSE files" do 5 | skip "this spec is not related to memcached" if TEST_MEMCACHED 6 | 7 | license = "#{File.read("LICENSE.md")}\n#{File.read("LICENSE.🍣.md")}" 8 | output = `./bashcached --license` 9 | _(license).must_equal output 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/command/version_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/version" do 4 | it "returns a version string" do 5 | skip "bashcached doesn't know memcached version" if TEST_MEMCACHED 6 | 7 | expect_version = `./bashcached --version`.chomp 8 | with_bashcached_and_client do |client| 9 | client << "version\r\n" 10 | version = client.gets 11 | _(version).must_equal "VERSION #{expect_version}\r\n" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE.🍣.md: -------------------------------------------------------------------------------- 1 | # "THE SUSHI-WARE LICENSE" 2 | 3 | wrote this file. 4 | 5 | As long as you retain this notice you can do whatever you want 6 | with this stuff. If we meet some day, and you think this stuff 7 | is worth it, you can buy me a **sushi 🍣** in return. 8 | 9 | (This license is based on ["THE BEER-WARE LICENSE" (Revision 42)]. 10 | Thanks a lot, Poul-Henning Kamp ;) 11 | 12 | ["THE BEER-WARE LICENSE" (Revision 42)]: https://people.freebsd.org/~phk/ 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '3.3.5' 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - name: Install socat 27 | run: sudo apt-get install -y socat 28 | - name: Run test 29 | run: bundle exec rake test 30 | -------------------------------------------------------------------------------- /test/command/delete_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/delete" do 4 | it "does not delete the key if the key does not exist" do 5 | with_bashcached_and_client do |client| 6 | expect_delete client, not_found: true 7 | end 8 | end 9 | 10 | it "deletes the key if the key exists" do 11 | with_bashcached_and_client do |client| 12 | expect_set client, value: "test" 13 | expect_get client, value: "test" 14 | expect_delete client 15 | expect_not_get client 16 | end 17 | end 18 | 19 | it "can be sent with noreply" do 20 | with_bashcached_and_client do |client| 21 | expect_set client, value: "test" 22 | expect_get client, value: "test" 23 | expect_delete client, noreply: true 24 | expect_not_get client 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/command/touch_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/touch" do 4 | it "does not set exptime if the key does not exist" do 5 | with_bashcached_and_client do |client| 6 | expect_touch client, exptime: 0, not_found: true 7 | end 8 | end 9 | 10 | it "sets exptime if the key exists" do 11 | with_bashcached_and_client do |client| 12 | expect_set client, value: "test", exptime: 0 13 | expect_touch client, exptime: 2 14 | expect_get client, value: "test" 15 | sleep 2.5 16 | expect_not_get client 17 | end 18 | end 19 | 20 | it "can be sent with noreply" do 21 | with_bashcached_and_client do |client| 22 | expect_set client, value: "test", exptime: 0 23 | expect_touch client, exptime: 2, noreply: true 24 | expect_get client, value: "test" 25 | sleep 2.5 26 | expect_not_get client 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2016-2024 TSUYUSATO "MakeNowJust" Kitsune 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test/command/incr_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/incr" do 4 | it "does not increment a value if the key does not exist" do 5 | with_bashcached_and_client do |client| 6 | expect_incr client, expect: "NOT_FOUND" 7 | end 8 | end 9 | 10 | it "increments a value if the key exists" do 11 | with_bashcached_and_client do |client| 12 | expect_set client, value: "0" 13 | expect_incr client, expect: 1 14 | end 15 | end 16 | 17 | it "increments values if the key exists" do 18 | with_bashcached_and_client do |client| 19 | expect_set client, value: "0" 20 | expect_incr client, value: 42, expect: 42 21 | end 22 | end 23 | 24 | it "increments a value multiple times" do 25 | with_bashcached_and_client do |client| 26 | expect_set client, value: "0" 27 | 5.times do |i| 28 | expect_incr client, expect: 1 + i 29 | end 30 | end 31 | end 32 | 33 | it "does not overwrite flags" do 34 | with_bashcached_and_client do |client| 35 | expect_set client, value: "0", flags: 42 36 | expect_incr client, expect: 1 37 | expect_get client, value: "1", flags: 42 38 | end 39 | end 40 | 41 | it "does not overwrite flags" do 42 | with_bashcached_and_client do |client| 43 | expect_set client, value: "0", exptime: 2 44 | expect_incr client, expect: 1 45 | sleep 2.5 46 | expect_not_get client 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/command/decr_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/decr" do 4 | it "does not decrement a value if the key does not exist" do 5 | with_bashcached_and_client do |client| 6 | expect_decr client, expect: "NOT_FOUND" 7 | end 8 | end 9 | 10 | it "decrements a value if the key exists" do 11 | with_bashcached_and_client do |client| 12 | expect_set client, value: "42" 13 | expect_decr client, expect: 41 14 | end 15 | end 16 | 17 | it "decrements values if the key exists" do 18 | with_bashcached_and_client do |client| 19 | expect_set client, value: "42" 20 | expect_decr client, value: 42, expect: 0 21 | end 22 | end 23 | 24 | it "decrements a value multiple times" do 25 | with_bashcached_and_client do |client| 26 | expect_set client, value: "42" 27 | 5.times do |i| 28 | expect_decr client, expect: 41 - i 29 | end 30 | end 31 | end 32 | 33 | it "does not overwrite flags" do 34 | with_bashcached_and_client do |client| 35 | expect_set client, value: "42", flags: 42 36 | expect_decr client, expect: 41 37 | expect_get client, value: "41", flags: 42 38 | end 39 | end 40 | 41 | it "does not overwrite flags" do 42 | with_bashcached_and_client do |client| 43 | expect_set client, value: "42", exptime: 2 44 | expect_decr client, expect: 41 45 | sleep 2.5 46 | expect_not_get client 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/readme_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | describe "readme" do 4 | it "'bashcached --help' output is same" do 5 | skip "this spec is not related to memcached" if TEST_MEMCACHED 6 | 7 | readme = File.read("README.md") 8 | .match(%r{(?<=^\$ \.\/bashcached --help\n).*?(?=^\$ )}m) 9 | .to_s 10 | help = `./bashcached --help` 11 | _(readme).must_equal help 12 | end 13 | 14 | it "example can run" do 15 | example = File.read("README.md") 16 | .match(%r{(?<=^\$ telnet localhost 25252\n).*?(?=^```)}m) 17 | .to_s 18 | .lines 19 | .map(&:chomp) 20 | # TODO: .lines(chomp: true) 21 | # ruby on ubuntu 17.10 is still 2.3.x... 22 | with_bashcached_and_client do |client| 23 | while example.empty? 24 | case line = example.shift 25 | when /\Aversion/ 26 | client << "#{line}\r\n" 27 | _(client.gets).must_equal "#{example.shift}\r\n" 28 | when /\Aset/ 29 | client << "#{line}\r\n" 30 | client << "#{example.shift}\r\n" 31 | _(client.gets).must_equal "#{example.shift}\r\n" 32 | when /\Aget/ 33 | client << "#{line}\r\n" 34 | _(client.gets).must_equal "#{example.shift}\r\n" 35 | _(client.gets).must_equal "#{example.shift}\r\n" 36 | when /\Aquit/ 37 | client << "#{line}\r\n" 38 | _(client.gets).must_be_nil 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/command/add_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/add" do 4 | it "stores a value if the key does not exist" do 5 | with_bashcached_and_client do |client| 6 | expect_add client, value: "test" 7 | expect_get client, value: "test" 8 | end 9 | end 10 | 11 | it "does not store a value if the key exists (set)" do 12 | with_bashcached_and_client do |client| 13 | expect_set client, value: "test1" 14 | expect_add client, value: "test2", not_stored: true 15 | expect_get client, value: "test1" 16 | end 17 | end 18 | 19 | it "does not store a value if the key exists (add)" do 20 | with_bashcached_and_client do |client| 21 | expect_add client, value: "test1" 22 | expect_add client, value: "test2", not_stored: true 23 | expect_get client, value: "test1" 24 | end 25 | end 26 | 27 | it "stores a value with flags" do 28 | with_bashcached_and_client do |client| 29 | expect_add client, value: "test", flags: 42 30 | expect_get client, value: "test", flags: 42 31 | end 32 | end 33 | 34 | it "stores a value with exptime" do 35 | with_bashcached_and_client do |client| 36 | expect_add client, value: "test", exptime: 2 37 | expect_get client, value: "test" 38 | sleep 2.5 39 | expect_not_get client 40 | end 41 | end 42 | 43 | it "can be sent with noreply" do 44 | with_bashcached_and_client do |client| 45 | expect_add client, value: "test", noreply: true 46 | expect_get client, value: "test" 47 | client << "quit\r\n" 48 | _(client.gets).must_be_nil 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/command/cas_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/cas" do 4 | it "stores a value if cas_unique is correct" do 5 | with_bashcached_and_client do |client| 6 | expect_set client, value: "test1" 7 | expect_cas client, value: "test2", cas_unique: 1, result: "STORED" 8 | expect_gets client, value: "test2", cas_unique: 2 9 | end 10 | end 11 | 12 | it "does not store a value if cas_unique is wrong" do 13 | with_bashcached_and_client do |client| 14 | expect_set client, value: "test1" 15 | expect_set client, value: "test2" 16 | expect_cas client, value: "test3", cas_unique: 1, result: "EXISTS" 17 | expect_gets client, value: "test2", cas_unique: 2 18 | end 19 | end 20 | 21 | it "does not store a value if the key is not found" do 22 | with_bashcached_and_client do |client| 23 | expect_cas client, value: "test1", cas_unique: 1, result: "NOT_FOUND" 24 | expect_not_get client 25 | end 26 | end 27 | 28 | it "can set a flag" do 29 | with_bashcached_and_client do |client| 30 | expect_set client, value: "test1" 31 | expect_cas client, value: "test2", flags: 42, cas_unique: 1, result: "STORED" 32 | expect_gets client, value: "test2", flags: 42, cas_unique: 2 33 | end 34 | end 35 | 36 | it "can set an exptime" do 37 | with_bashcached_and_client do |client| 38 | expect_set client, value: "test1" 39 | expect_cas client, value: "test2", exptime: 2, cas_unique: 1, result: "STORED" 40 | expect_gets client, value: "test2", cas_unique: 2 41 | sleep 2.5 42 | expect_not_get client 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/command/append_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/append" do 4 | it "does not append a value if the key does not exist" do 5 | with_bashcached_and_client do |client| 6 | expect_append client, value: "test", not_stored: true 7 | end 8 | end 9 | 10 | it "appends a value if the key exists" do 11 | with_bashcached_and_client do |client| 12 | expect_set client, value: "test1" 13 | expect_append client, value: "test2" 14 | expect_get client, value: "test1test2" 15 | end 16 | end 17 | 18 | it "does not overwrite flags" do 19 | with_bashcached_and_client do |client| 20 | expect_set client, value: "test1", flags: 42 21 | expect_append client, value: "test2", flags: 21 22 | expect_get client, value: "test1test2", flags: 42 23 | end 24 | end 25 | 26 | it "does not overwrite exptime" do 27 | with_bashcached_and_client do |client| 28 | expect_set client, value: "test1", exptime: 2 29 | expect_append client, value: "test2" 30 | expect_get client, value: "test1test2" 31 | sleep 2.5 32 | expect_not_get client 33 | end 34 | end 35 | 36 | it "appends values multiple times" do 37 | with_bashcached_and_client do |client| 38 | expect_set client, value: "test" 39 | value = "test" 40 | 5.times do |i| 41 | expect_append client, value: "test#{i}" 42 | value = "#{value}test#{i}" 43 | end 44 | expect_get client, value: value 45 | end 46 | end 47 | 48 | it "can be sent with noreply" do 49 | with_bashcached_and_client do |client| 50 | expect_append client, value: "test", noreply: true 51 | expect_not_get client 52 | client << "quit\r\n" 53 | _(client.gets).must_be_nil 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/command/prepend_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/preppend" do 4 | it "does not prepend a value if the key does not exist" do 5 | with_bashcached_and_client do |client| 6 | expect_prepend client, value: "test", not_stored: true 7 | end 8 | end 9 | 10 | it "prepends a value if the key exists" do 11 | with_bashcached_and_client do |client| 12 | expect_set client, value: "test1" 13 | expect_prepend client, value: "test2" 14 | expect_get client, value: "test2test1" 15 | end 16 | end 17 | 18 | it "does not overwrite flags" do 19 | with_bashcached_and_client do |client| 20 | expect_set client, value: "test1", flags: 42 21 | expect_prepend client, value: "test2", flags: 21 22 | expect_get client, value: "test2test1", flags: 42 23 | end 24 | end 25 | 26 | it "does not overwrite exptime" do 27 | with_bashcached_and_client do |client| 28 | expect_set client, value: "test1", exptime: 2 29 | expect_prepend client, value: "test2" 30 | expect_get client, value: "test2test1" 31 | sleep 2.5 32 | expect_not_get client 33 | end 34 | end 35 | 36 | it "prepends values multiple times" do 37 | with_bashcached_and_client do |client| 38 | expect_set client, value: "test" 39 | value = "test" 40 | 5.times do |i| 41 | expect_prepend client, value: "test#{i}" 42 | value = "test#{i}#{value}" 43 | end 44 | expect_get client, value: value 45 | end 46 | end 47 | 48 | it "can be sent with noreply" do 49 | with_bashcached_and_client do |client| 50 | expect_append client, value: "test", noreply: true 51 | expect_not_get client 52 | client << "quit\r\n" 53 | _(client.gets).must_be_nil 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/command/gets_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/gets" do 4 | it "can get cas unique" do 5 | with_bashcached_and_client do |client| 6 | expect_set client, value: "test" 7 | expect_gets client, value: "test", cas_unique: 1 8 | end 9 | end 10 | 11 | it "returns same cas unique when not changed" do 12 | with_bashcached_and_client do |client| 13 | expect_set client, value: "test" 14 | expect_gets client, value: "test", cas_unique: 1 15 | expect_gets client, value: "test", cas_unique: 1 16 | end 17 | end 18 | 19 | it "returns another cas unique when changed" do 20 | with_bashcached_and_client do |client| 21 | expect_set client, value: "test1" 22 | expect_gets client, value: "test1", cas_unique: 1 23 | expect_set client, value: "test2" 24 | expect_gets client, value: "test2", cas_unique: 2 25 | end 26 | end 27 | 28 | it "returns another cas unique by each keys" do 29 | with_bashcached_and_client do |client| 30 | expect_set client, key: "test1", value: "test1" 31 | expect_gets client, key: "test1", value: "test1", cas_unique: 1 32 | expect_set client, key: "test2", value: "test2" 33 | expect_gets client, key: "test2", value: "test2", cas_unique: 2 34 | end 35 | end 36 | 37 | it "can get many keys at once" do 38 | with_bashcached_and_client do |client| 39 | expect_set client, key: "test1", value: "test1" 40 | expect_set client, key: "test2", value: "test2" 41 | expect_get_many client, { 42 | "test1" => {value: "test1", cas_unique: 1}, 43 | "test2" => {value: "test2", cas_unique: 2}, 44 | } 45 | end 46 | end 47 | 48 | it "can get flags" do 49 | with_bashcached_and_client do |client| 50 | expect_set client, value: "test", flags: 42 51 | expect_gets client, value: "test", flags: 42, cas_unique: 1 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/command/set_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/set" do 4 | it "sets a value with key and responds with 'STORED'" do 5 | with_bashcached_and_client do |client| 6 | expect_set client, value: "test" 7 | end 8 | end 9 | 10 | it "can set a value multiple times" do 11 | with_bashcached_and_client do |client| 12 | expect_set client, key: "test1", value: "test" 13 | expect_set client, key: "test2", value: "test" 14 | end 15 | end 16 | 17 | it "can set a value with flags and exptime" do 18 | with_bashcached_and_client do |client| 19 | expect_set client, value: "test", flags: 42 20 | expect_set client, value: "test", exptime: 42 21 | expect_set client, value: "test", flags: 42, exptime: 42 22 | end 23 | end 24 | 25 | it "can be sent with noreply" do 26 | with_bashcached_and_client do |client| 27 | expect_set client, value: "test", noreply: true 28 | expect_get client, value: "test" 29 | client << "quit\r\n" 30 | _(client.gets).must_be_nil 31 | end 32 | end 33 | 34 | it "can set an empty value" do 35 | with_bashcached_and_client do |client| 36 | expect_set client, value: "" 37 | end 38 | end 39 | 40 | it "can set a value including a white space" do 41 | with_bashcached_and_client do |client| 42 | expect_set client, value: "test test" 43 | end 44 | end 45 | 46 | it "can set a value including a null character" do 47 | with_bashcached_and_client do |client| 48 | expect_set client, value: "test\0test" 49 | end 50 | end 51 | 52 | it "can set a value including new lines" do 53 | with_bashcached_and_client do |client| 54 | expect_set client, value: "test\ntest\rtest" 55 | end 56 | end 57 | 58 | it "can set a value including control characters" do 59 | with_bashcached_and_client do |client| 60 | expect_set client, value: "test\ttest\atest" 61 | end 62 | end 63 | 64 | it "can set a large value" do 65 | with_bashcached_and_client do |client| 66 | expect_set client, value: "test\r\n" * 1000 67 | end 68 | end 69 | 70 | it "can set a UTF-8 value" do 71 | with_bashcached_and_client do |client| 72 | expect_set client, value: "これはテストです。💯" 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/command/replace_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/replace" do 4 | it "does not store a value if the key does not exist" do 5 | with_bashcached_and_client do |client| 6 | expect_replace client, value: "test", not_stored: true 7 | end 8 | end 9 | 10 | it "stores a value if the key exists (set)" do 11 | with_bashcached_and_client do |client| 12 | expect_set client, value: "test1" 13 | expect_replace client, value: "test2" 14 | expect_get client, value: "test2" 15 | end 16 | end 17 | 18 | it "stores a value if the key exists (set -> replace)" do 19 | with_bashcached_and_client do |client| 20 | expect_set client, value: "test1" 21 | expect_replace client, value: "test2" 22 | expect_replace client, value: "test3" 23 | expect_get client, value: "test3" 24 | end 25 | end 26 | 27 | it "stores a value if the key exists (add)" do 28 | with_bashcached_and_client do |client| 29 | expect_add client, value: "test1" 30 | expect_replace client, value: "test2" 31 | expect_get client, value: "test2" 32 | end 33 | end 34 | 35 | it "stores a value with flags" do 36 | with_bashcached_and_client do |client| 37 | expect_set client, value: "test1", flags: 21 38 | expect_replace client, value: "test2", flags: 42 39 | expect_get client, value: "test2", flags: 42 40 | end 41 | end 42 | 43 | it "stores a value with exptime" do 44 | with_bashcached_and_client do |client| 45 | expect_set client, value: "test1" 46 | expect_replace client, value: "test2", exptime: 2 47 | expect_get client, value: "test2" 48 | sleep 2.5 49 | expect_not_get client 50 | end 51 | end 52 | 53 | it "overwrites exptime" do 54 | with_bashcached_and_client do |client| 55 | expect_set client, value: "test1" 56 | expect_replace client, value: "test2", exptime: 2 57 | expect_replace client, value: "test3", exptime: 0 58 | expect_get client, value: "test3" 59 | sleep 2.5 60 | expect_get client, value: "test3" 61 | end 62 | end 63 | 64 | it "can be sent with noreply" do 65 | with_bashcached_and_client do |client| 66 | expect_replace client, value: "test", noreply: true 67 | expect_not_get client 68 | client << "quit\r\n" 69 | _(client.gets).must_be_nil 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bashcached 2 | 3 | > memcached server built on [bash] + [socat] 4 | 5 | [bash]: https://www.gnu.org/software/bash/ 6 | [socat]: http://www.dest-unreach.org/socat/ 7 | 8 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/makenowjust/bashcached/test.yml) 9 | 10 | ## Feature 11 | 12 | It is one file script (small, `(($(< bashcached wc -l) < 100))`!), and it requires only: 13 | 14 | - `bash` 15 | - `socat` 16 | 17 | So, you can use it as soon as you download it. 18 | 19 | It supports multiple connections and implements almost all memcached commands: 20 | 21 | - `set`, `add`, `replace`, `append` and `prepend` 22 | - `get`, `delete` and `touch` 23 | - `incr` and `decr` 24 | - `gets` and `cas` 25 | - `flush_all` 26 | - `version` and `quit` 27 | 28 | And, it supports to serve over `tcp` and `unix` domain socket. 29 | 30 | ## Install 31 | 32 | You could install `base64`, `bash` and `socat` via `brew` if you use macOS: 33 | 34 | ```console 35 | $ brew install base64 bash socat 36 | ``` 37 | 38 | (In fact, `bash` is installed in the default on macOS, however it is *too old* to run `bashcached`.) 39 | 40 | Or, you could install `socat` via `apt` if you use Ubuntu: 41 | 42 | ```console 43 | $ sudo apt install socat 44 | ``` 45 | 46 | then, download and chmod. 47 | 48 | ```console 49 | $ curl -LO https://git.io/bashcached 50 | $ chmod +x bashcached 51 | ``` 52 | 53 | Or, you could use [`bpkg`](https://github.com/bpkg/bpkg) instaed of downloading script: 54 | 55 | ```console 56 | $ bpkg install makenowjust/bashcached -g 57 | ``` 58 | 59 | ## Usage 60 | 61 | ```console 62 | $ ./bashcached --help 63 | bashcached - memcached built on bash + socat 64 | (C) TSUYUSATO "MakeNowJust" Kitsune 2016-2024 65 | 66 | USAGE: bashcached [--help] [--version] [--license] [--protocol=tcp|unix] [--port=PORT] [--check=CHECK] 67 | 68 | OPTIONS: 69 | --protocol=tcp|unix protocol name to bind and listen (default: tcp) 70 | --port=PORT port (or filename) to bind and listen (default: 25252) 71 | --check=CHECK interval to check each cache's expire (default: 60) 72 | --help show this help 73 | --version show bashcached's version 74 | --license show bashcached's license 75 | $ ./bashcached & 76 | $ telnet localhost 25252 77 | version 78 | VERSION 5.2.0-bashcached 79 | set hello 0 0 11 80 | hello world 81 | STORED 82 | get hello 83 | VALUE hello 0 11 84 | hello world 85 | END 86 | quit 87 | ``` 88 | 89 | ## License and Copyright 90 | 91 | MIT and [:sushi:](https://github.com/MakeNowJust/sushi-ware) 92 | © TSUYUSATO "[MakeNowJust](https://quine.codes)" Kitsune <> 2016-2024 93 | -------------------------------------------------------------------------------- /test/command/get_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | describe "command/get" do 4 | it "gets a stored value" do 5 | with_bashcached_and_client do |client| 6 | expect_set client, value: "test" 7 | expect_get client, value: "test" 8 | end 9 | end 10 | 11 | it "can get a stored value by another client" do 12 | with_bashcached do 13 | with_client do |client| 14 | expect_set client, value: "test" 15 | end 16 | with_client do |client| 17 | expect_get client, value: "test" 18 | end 19 | end 20 | end 21 | 22 | it "can get a value multiple times" do 23 | with_bashcached_and_client do |client| 24 | expect_set client, value: "test" 25 | expect_get client, value: "test" 26 | expect_get client, value: "test" 27 | end 28 | end 29 | 30 | it "can get many values at once" do 31 | with_bashcached_and_client do |client| 32 | expect_set client, key: "test1", value: "test_value1" 33 | expect_set client, key: "test2", value: "test_value2" 34 | expect_get_many client, { 35 | "test1" => {value: "test_value1"}, 36 | "test2" => {value: "test_value2"}, 37 | } 38 | end 39 | end 40 | 41 | it "can get a value with flags" do 42 | with_bashcached_and_client do |client| 43 | expect_set client, value: "test", flags: 42 44 | expect_get client, value: "test", flags: 42 45 | end 46 | end 47 | 48 | it "cannot get a value after exptime (<= 2592000)" do 49 | with_bashcached_and_client do |client| 50 | expect_set client, value: "test", exptime: 2 51 | expect_get client, value: "test" 52 | sleep 2.5 53 | expect_not_get client 54 | end 55 | end 56 | 57 | it "cannot get a value after exptime (> 2592000)" do 58 | with_bashcached_and_client do |client| 59 | expect_set client, value: "test", exptime: Time.now.to_i + 2 60 | expect_get client, value: "test" 61 | sleep 2.5 62 | expect_not_get client 63 | end 64 | end 65 | 66 | it "overwrites exptime" do 67 | with_bashcached_and_client do |client| 68 | expect_set client, value: "test1", exptime: 2 69 | expect_set client, value: "test2", exptime: 0 70 | expect_get client, value: "test2" 71 | sleep 2.5 72 | expect_get client, value: "test2" 73 | end 74 | end 75 | 76 | it "can get an empty value" do 77 | with_bashcached_and_client do |client| 78 | expect_set client, value: "" 79 | expect_get client, value: "" 80 | end 81 | end 82 | 83 | it "can get a value including a white space" do 84 | with_bashcached_and_client do |client| 85 | expect_set client, value: "test test" 86 | expect_get client, value: "test test" 87 | end 88 | end 89 | 90 | it "can get a value including a null character" do 91 | with_bashcached_and_client do |client| 92 | expect_set client, value: "test\0test" 93 | expect_get client, value: "test\0test" 94 | end 95 | end 96 | 97 | it "can get a value including new lines" do 98 | with_bashcached_and_client do |client| 99 | expect_set client, value: "test\ntest\rtest" 100 | expect_get client, value: "test\ntest\rtest" 101 | end 102 | end 103 | 104 | it "can get a value including control characters" do 105 | with_bashcached_and_client do |client| 106 | expect_set client, value: "test\ttest\atest" 107 | expect_get client, value: "test\ttest\atest" 108 | end 109 | end 110 | 111 | it "can get a large value" do 112 | with_bashcached_and_client do |client| 113 | expect_set client, value: "test\r\n" * 1000 114 | expect_get client, value: "test\r\n" * 1000 115 | end 116 | end 117 | 118 | it "can get a UTF-8 value" do 119 | with_bashcached_and_client do |client| 120 | expect_set client, value: "これはテストです。💯" 121 | expect_get client, value: "これはテストです。💯" 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /bashcached: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # bashcached - memcached built on bash + socat 3 | # (C) TSUYUSATO "MakeNowJust" Kitsune 2016-2024 4 | # 5 | # USAGE: bashcached [--help] [--version] [--license] [--protocol=tcp|unix] [--port=PORT] [--check=CHECK] 6 | # 7 | # OPTIONS: 8 | # --protocol=tcp|unix protocol name to bind and listen (default: tcp) 9 | # --port=PORT port (or filename) to bind and listen (default: 25252) 10 | # --check=CHECK interval to check each cache's expire (default: 60) 11 | # --help show this help 12 | # --version show bashcached's version 13 | # --license show bashcached's license 14 | IFS=$' \t\r' VERSION=5.2.0-bashcached; export LANG=C 15 | trap 'exit 0' INT TERM 16 | 17 | if [[ "$SOCAT_VERSION" ]]; then 18 | send="$(mktemp -u)"; mkfifo -m 600 "$send" 19 | recv="$(mktemp -u)"; mkfifo -m 600 "$recv"; while [[ -p "$recv" ]]; do cat "$recv" 2>/dev/null; done & 20 | trap 'rm -f "$recv" "$send"' EXIT; while read -ra cmd; do case ${cmd[0]} in 21 | set|add|replace|append|prepend|cas) 22 | while true; do printf 'recv=%q send=%q cmd=%q\n' "$recv" "$send" "${cmd[*]}" >"$BASHCACHED_PIPE" 2>/dev/null && break 23 | done; head -c "${cmd[4]-0}" >"$send";; 24 | quit) exit;; '') ;; *) while true; do 25 | printf 'recv=%q send=%q cmd=%q\n' "$recv" "$send" "${cmd[*]}" >"$BASHCACHED_PIPE" 2>/dev/null && break; done;; 26 | esac; done 27 | else 28 | help() { tail -n+2 <"$0" | head -n12 | cut -c3-; exit; }; version() { echo $VERSION; exit; } 29 | license() { curl -s 'https://raw.githubusercontent.com/MakeNowJust/bashcached/master/LICENSE.md'; echo; 30 | curl -s 'https://raw.githubusercontent.com/MakeNowJust/bashcached/master/LICENSE.%F0%9F%8D%A3.md'; exit; } 31 | for v in "$@"; do [[ $v == --* ]] && eval "${v:2}"; done 32 | 33 | # global variables 34 | unique=1 before=$(printf '%(%s)T' -1) 35 | declare -A flags=() exptime=() casUnique=() data=() 36 | 37 | # cache operator 38 | cache_has() { t=${exptime[$1]} && [[ $t && ( $t -eq 0 || $t -gt $time ) ]]; } 39 | cache_update() { data[$1]=$2; [[ $3 ]] && casUnique[$1]="$3" || casUnique[$1]=$((unique++)); } 40 | cache_set() { flags[$1]=$2 exptime[$1]=$((0 < $3 && $3 <= 2592000 ? $3 + time : $3)); cache_update "$1" "$4" "$5"; } 41 | cache_get() { cache_has "$1" && d="${data[$1]}" && printf $'VALUE %s %s %s%s\r\n' \ 42 | "$1" "${flags[$1]}" "$(echo -n "$d" | base64 -d | wc -c)" "$([[ $2 ]] && echo " ${casUnique[$1]}")" && 43 | echo -n "$d" | base64 -d && echo -e '\r'; } 44 | cache_delete() { unset "flags[$1]" "exptime[$1]" "casUnique[$1]" "data[$1]"; } 45 | 46 | # utils 47 | read_data() { d="$(head -c "$1" "$send" | base64)"; } 48 | base64_cat() { cat <(echo -n "$1" | base64 -d) <(echo -n "$2" | base64 -d) | base64; } 49 | 50 | BASHCACHED_PIPE="$(mktemp -u)"; export BASHCACHED_PIPE; mkfifo -m 600 "$BASHCACHED_PIPE" 51 | trap 'rm -f "$BASHCACHED_PIPE"' EXIT 52 | (( ${check-60} != 0 )) && while [[ -p "$BASHCACHED_PIPE" ]]; do echo; sleep "${check-60}"; done >"$BASHCACHED_PIPE" & 53 | 54 | while [[ -p "$BASHCACHED_PIPE" ]]; do cat "$BASHCACHED_PIPE" 2>/dev/null; done | while read -r line; do 55 | cmd=() recv='' send=; eval "$line"; read -ra cmd<<<"${cmd[*]}"; time=$(printf '%(%s)T' -1) 56 | (( time - before >= ${check-60} )) && for k in "${!exptime[@]}"; do 57 | ! cache_has $k && cache_delete $k; done && before=$time 58 | [[ ! -p $recv ]] && continue 59 | case ${cmd[0]} in 60 | set) read_data "${cmd[4]}" || return 1; cache_set "${cmd[1]}" "${cmd[2]}" "${cmd[3]}" "$d" 61 | [[ "${cmd[5]}" != noreply ]] && echo -e 'STORED\r'>"$recv";; 62 | add) read_data "${cmd[4]}" || return 1; ! cache_has "${cmd[1]}" && cache_set "${cmd[1]}" "${cmd[2]}" "${cmd[3]}" "$d" && 63 | result=STORED || result=NOT_STORED 64 | [[ "${cmd[5]}" != noreply ]] && echo -e "$result\\r">"$recv";; 65 | replace) read_data "${cmd[4]}" || return 1; cache_has "${cmd[1]}" && cache_set "${cmd[1]}" "${cmd[2]}" "${cmd[3]}" "$d" && 66 | result=STORED || result=NOT_STORED 67 | [[ "${cmd[5]}" != noreply ]] && echo -e "$result\\r">"$recv";; 68 | append) read_data "${cmd[4]}" || return 1; cache_has "${cmd[1]}" && cache_update "${cmd[1]}" \ 69 | "$(base64_cat "${data[${cmd[1]}]}" "$d")" && result=STORED || result=NOT_STORED 70 | [[ "${cmd[5]}" != noreply ]] && echo -e "$result\\r">"$recv";; 71 | prepend) read_data "${cmd[4]}" || return 1; cache_has "${cmd[1]}" && cache_update "${cmd[1]}" \ 72 | "$(base64_cat "$d" "${data[${cmd[1]}]}")" && result=STORED || result=NOT_STORED 73 | [[ "${cmd[5]}" != noreply ]] && echo -e "$result\\r">"$recv";; 74 | cas) read_data "${cmd[4]}" || return 1; if ! cache_has "${cmd[1]}"; then result=NOT_FOUND 75 | else [[ "${casUnique[${cmd[1]}]}" -eq "${cmd[5]}" ]] && cache_set "${cmd[1]}" "${cmd[2]}" "${cmd[3]}" "$d" && 76 | result=STORED || result=EXISTS; fi 77 | [[ "${cmd[6]}" != noreply ]] && echo -e "$result\\r">"$recv";; 78 | get) (for ((i=1; i < ${#cmd[@]}; i++)); do cache_get "${cmd[$i]}"; done 79 | echo -e 'END\r')>"$recv";; 80 | gets) (for ((i=1; i < ${#cmd[@]}; i++)); do cache_get "${cmd[$i]}" 1; done 81 | echo -e 'END\r')>"$recv";; 82 | delete) cache_has "${cmd[1]}" && cache_delete "${cmd[1]}" && 83 | result=DELETED || result=NOT_FOUND 84 | [[ "${cmd[2]}" != noreply ]] && echo -e "$result\\r">"$recv";; 85 | incr) cache_has "${cmd[1]}" && result=$(($(echo -n "${data[${cmd[1]}]}" | base64 -d) + ${cmd[2]-0})) && 86 | cache_update "${cmd[1]}" "$(echo -n "$result" | base64)" || result=NOT_FOUND 87 | [[ "${cmd[3]}" != noreply ]] && echo -e "$result\\r">"$recv";; 88 | decr) cache_has "${cmd[1]}" && result=$(($(echo -n "${data[${cmd[1]}]}" | base64 -d) - ${cmd[2]-0})) && 89 | cache_update "${cmd[1]}" "$(echo -n $result | base64)" || result=NOT_FOUND 90 | [[ "${cmd[3]}" != noreply ]] && echo -e "$result\\r">"$recv";; 91 | touch) cache_has "${cmd[1]}" && 92 | cache_set "${cmd[1]}" "${flags[${cmd[1]}]}" "${cmd[2]}" "${data[${cmd[1]}]}" "${casUnique[${cmd[1]}]}" && 93 | result=TOUCHED || result=NOT_FOUND 94 | [[ "${cmd[3]}" != noreply ]] && echo -e "$result\\r">"$recv";; 95 | flush_all) for k in "${!exptime[@]}"; do exptime[$k]=$((time + ${cmd[1]-0})); done 96 | [[ "${cmd[-1]}" != noreply ]] && echo -e 'OK\r'>"$recv";; 97 | version) echo -e "VERSION $VERSION\\r">"$recv" &;; 98 | esac; done & 99 | socat "${protocol-tcp}-listen:${port-25252},reuseaddr,fork" system:"$0"; fi 100 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "minitest/pride" 3 | 4 | require "open3" 5 | require "socket" 6 | require "timeout" 7 | 8 | TEST_MEMCACHED = ENV.has_key?("TEST_MEMCACHED") 9 | 10 | class Minitest::Test 11 | def retry_on_error(times: 10, delay: 0.1, error_class: StandardError) 12 | (1..times).each do |i| 13 | begin 14 | return yield 15 | rescue error_class 16 | sleep delay 17 | raise if i == times 18 | next 19 | end 20 | end 21 | end 22 | 23 | def with_command(command, timeout:) 24 | Open3.popen3(*command, pgroup: true) do |*, thread| 25 | begin 26 | Timeout.timeout(timeout) { yield } 27 | ensure 28 | # Wait to prevent to send a signal to unstable process 29 | # TODO: not perfect, but it works 30 | sleep 0.1 31 | 32 | # Send SIGINT to the process group 33 | Process.kill "INT", -thread.pid 34 | end 35 | 36 | begin 37 | Timeout.timeout(0.1) { _(thread.value.success?).must_equal true } 38 | rescue Timeout::Error 39 | Process.kill "INT", -thread.pid 40 | # TODO: retry limit is needed? 41 | retry 42 | end 43 | 44 | # Cool down... 45 | sleep 0.1 46 | end 47 | end 48 | 49 | def with_bashcached(opts = "", timeout: 5) 50 | command = TEST_MEMCACHED ? "memcached -p 25252" : "./bashcached" 51 | with_command("#{command} #{opts}", timeout: timeout) do 52 | # Wait to start a bashcached server 53 | # TODO: find better way (e.g. using 'expect' library) 54 | sleep 0.1 55 | yield 56 | end 57 | nil 58 | end 59 | 60 | def with_client(port = 25252) 61 | # Sometimes TCPSocket.new is failed, so retry_on_error is needed. 62 | client = retry_on_error { TCPSocket.new("localhost", port) } 63 | yield client 64 | nil 65 | ensure 66 | client&.close 67 | end 68 | 69 | def with_bashcached_and_client 70 | with_bashcached do 71 | with_client do |client| 72 | yield client 73 | end 74 | end 75 | end 76 | 77 | def write_store_command(client, command, key, flags, exptime, value, noreply, cas_unique = nil) 78 | client << "#{command} #{key} #{flags} #{exptime} #{value.bytesize}#{cas_unique && " #{cas_unique}"}#{noreply ? " noreply" : ""}\r\n" 79 | client << "#{value.b}\r\n" 80 | end 81 | 82 | def expect_set(client, key: "test", value:, flags: 0, exptime: 0, noreply: false) 83 | write_store_command client, "set", key, flags, exptime, value, noreply 84 | _(client.gets).must_equal "STORED\r\n" unless noreply 85 | end 86 | 87 | def expect_add(client, key: "test", value:, flags: 0, exptime: 0, noreply: false, not_stored: false) 88 | write_store_command client, "add", key, flags, exptime, value, noreply 89 | unless noreply 90 | _(client.gets).must_equal "#{not_stored ? "NOT_STORED" : "STORED"}\r\n" 91 | end 92 | end 93 | 94 | def expect_replace(client, key: "test", value:, flags: 0, exptime: 0, noreply: false, not_stored: false) 95 | write_store_command client, "replace", key, flags, exptime, value, noreply 96 | unless noreply 97 | _(client.gets).must_equal "#{not_stored ? "NOT_STORED" : "STORED"}\r\n" 98 | end 99 | end 100 | 101 | def expect_append(client, key: "test", value:, flags: 0, exptime: 0, noreply: false, not_stored: false) 102 | write_store_command client, "append", key, flags, exptime, value, noreply 103 | unless noreply 104 | _(client.gets).must_equal "#{not_stored ? "NOT_STORED" : "STORED"}\r\n" 105 | end 106 | end 107 | 108 | def expect_prepend(client, key: "test", value:, flags: 0, exptime: 0, noreply: false, not_stored: false) 109 | write_store_command client, "prepend", key, flags, exptime, value, noreply 110 | unless noreply 111 | _(client.gets).must_equal "#{not_stored ? "NOT_STORED" : "STORED"}\r\n" 112 | end 113 | end 114 | 115 | def expect_cas(client, key: "test", value:, flags: 0, exptime: 0, cas_unique:, noreply: false, result:) 116 | write_store_command client, "cas", key, flags, exptime, value, noreply, cas_unique 117 | unless noreply 118 | _(client.gets).must_equal "#{result}\r\n" 119 | end 120 | end 121 | 122 | def expect_get(client, key: "test", value:, flags: 0) 123 | expect_get_many client, key => {value: value, flags: flags} 124 | end 125 | 126 | def expect_not_get(client, key: "test") 127 | client << "get #{key}\r\n" 128 | _(client.gets).must_equal "END\r\n" 129 | end 130 | 131 | def expect_gets(client, key: "test", value:, cas_unique:, flags: 0) 132 | expect_get_many client, key => {value: value, cas_unique: cas_unique, flags: flags} 133 | end 134 | 135 | VALUE_LINE_RE = %r{ 136 | \A 137 | VALUE 138 | \ (?[^\ ]+) 139 | \ (?\d+) 140 | \ (?\d+) 141 | (?:\ (?\d+))? 142 | \r\n 143 | \z 144 | }x 145 | 146 | def expect_get_many(client, expects) 147 | command = expects.first.last.has_key?(:cas_unique) ? "gets" : "get" 148 | client << "#{command} #{expects.keys.join " "}\r\n" 149 | while !expects.empty? && (line = client.gets) 150 | if line =~ VALUE_LINE_RE 151 | expect = expects.delete $~[:key] 152 | expect_value = expect[:value] 153 | expect_flags = expect[:flags]&.to_s || "0" 154 | expect_cas_unique = expect[:cas_unique]&.to_s 155 | 156 | _($~[:flags]).must_equal expect_flags 157 | _($~[:cas_unique]).must_equal expect_cas_unique if expect_cas_unique 158 | _(client.read($~[:bytes].to_i)).must_equal expect_value.b 159 | _(client.gets).must_equal "\r\n" 160 | end 161 | end 162 | _(client.gets).must_equal "END\r\n" 163 | end 164 | 165 | def expect_delete(client, key: "test", noreply: false, not_found: false) 166 | client << "delete #{key}#{noreply ? " noreply" : ""}\r\n" 167 | unless noreply 168 | _(client.gets).must_equal "#{not_found ? "NOT_FOUND" : "DELETED"}\r\n" 169 | end 170 | end 171 | 172 | def expect_touch(client, key: "test", exptime:, noreply: false, not_found: false) 173 | client << "touch #{key} #{exptime}#{noreply ? " noreply" : ""}\r\n" 174 | unless noreply 175 | _(client.gets).must_equal "#{not_found ? "NOT_FOUND" : "TOUCHED"}\r\n" 176 | end 177 | end 178 | 179 | def expect_incr(client, key: "test", value: 1, noreply: false, expect:) 180 | client << "incr #{key} #{value}#{noreply ? " noreply" : ""}\r\n" 181 | unless noreply 182 | _(client.gets).must_equal "#{expect}\r\n" 183 | end 184 | end 185 | 186 | def expect_decr(client, key: "test", value: 1, noreply: false, expect:) 187 | client << "decr #{key} #{value}#{noreply ? " noreply" : ""}\r\n" 188 | unless noreply 189 | _(client.gets).must_equal "#{expect}\r\n" 190 | end 191 | end 192 | end 193 | --------------------------------------------------------------------------------