Skip to main content
  1. Posts/

Day 38 - Testy Housekeeping

OldDays seitan-spin ruby SQL

Yes, that gif again.
Yes, that gif again.

Coding
#

Cleanup - database
#

So we’ve got a small set of working tests, but after a while there’s a problem..

SELECT * FROM [DSpineBackup].[dbo].[Account] WHERE account LIKE 'test%' AND email LIKE '%301days.com'
  
 (1018 row(s) affected)

We’re cluttering up our database something fierce, and at some point we’re bound to hit a limit on the server. We need to clean up after ourselves, which means the test code has to hit the database. So we add an After hook to the Cucumber code.

hooks.rb
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
After('@db_cleanup') do |scenario|
  connect_hash = {username: ENV['DB_USERNAME'], password: ENV['DB_PASSWORD'], dataserver: ENV['DB_DATASERVER'], database: ENV['DB_NAME']}
  puts "After hook connecting to #{ENV['DB_DATASERVER']} database #{ENV['DB_NAME']}..."
  client = TinyTds::Client.new(connect_hash) 
  puts "  Connected." if client.active?
  result = client.execute("DELETE
    FROM [#{ENV['DB_NAME']}].[dbo].[Account]
    WHERE account LIKE 'test%'
    AND email LIKE '%301days.com'")
  puts "Rows deleted from [Account]: #{result.do}"
  result = client.execute("SELECT playerID, account, name
    FROM [#{ENV['DB_NAME']}].[dbo].[Player]
    WHERE account LIKE 'test%'
    AND name LIKE 'Dude.%'")
  players_to_delete = []
  result.each(:symbolize_keys => true) do |rowset|
  	players_to_delete << rowset[:playerID]
  end
  puts "Players to delete: #{players_to_delete}"
  player_tables = ["PlayerBelt","PlayerEffects","PlayerFlags","PlayerHeld","PlayerLocker","PlayerQuests","PlayerRings","PlayerSack","PlayerSettings","PlayerSkills","PlayerSpells","PlayerWearing","Player"]
  players_to_delete.each do |player_to_delete|
  	puts "Deleting player #{player_to_delete}"
  	player_tables.each do |table|
	  result = client.execute("DELETE
		FROM [#{ENV['DB_NAME']}].[dbo].[#{table}]
		WHERE playerID = '#{player_to_delete}'")
	  puts "Rows deleted from [#{table}]: #{result.do}"
	end
  end
end

As long as we have all of the appropriate environment variables set, and the @db_cleanup tag on the test, all of the test accounts/characters will get cleaned up after the test runs.

  After hook connecting to AQUALUNG\SQLEXPRESS database DSpineBackup...
    Connected.
  Rows deleted from [Account]: 1
  Players to delete: [3005]
  Deleting player 3005
  Rows deleted from [PlayerBelt]: 8
  Rows deleted from [PlayerEffects]: 20
  Rows deleted from [PlayerFlags]: 1
  Rows deleted from [PlayerHeld]: 2
  Rows deleted from [PlayerLocker]: 20
  Rows deleted from [PlayerQuests]: 0
  Rows deleted from [PlayerRings]: 8
  Rows deleted from [PlayerSack]: 21
  Rows deleted from [PlayerSettings]: 1
  Rows deleted from [PlayerSkills]: 1
  Rows deleted from [PlayerSpells]: 35
  Rows deleted from [PlayerWearing]: 20
  Rows deleted from [Player]: 1
d:\>sqlcmd -S AQUALUNG\SQLEXPRESS -U sa -d DSpineBackup -Q "SELECT
account FROM [DSpineBackup].[dbo].[Account] WHERE account LIKE 'test%' AND email
 LIKE '%301days.com'"
 
account
--------------------

(0 rows affected)

Of course our test code just got tightly coupled to the server database schema, which is generally a bad thing. A better solution is to just restore the database to the pre-test state (optionally saving the state post-test for debugging), although that can be either slow (for non-trivial db) or a problem (if the server can’t tolerate it).

Cleanup - codebase
#

features/steps/login_steps.rb
25
26
27
28
29
30
31
32
33
34
35
36
37
38
When(/^I create an account and character$/) do
  @connection.cmd({"String" => "", "Match" => /Login\: /}) { |c| puts "0 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => "new", "Match" => /account\: /}) { |c| puts "1 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_name], "Match" => /address\: /}) { |c| puts "2 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_email], "Match" => /address\: /}) { |c| puts "3 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_email], "Match" => /12\)\: /}) { |c| puts "4 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_password], "Match" => /password\: /}) { |c| puts "5 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:acct_password], "Match" => /Gender\: /}) { |c| puts "6 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_gender], "Match" => /a Race\: /}) { |c| puts "7 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_race], "Match" => /Class\: /}) { |c| puts "8 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_class], "Match" => /\(y,n\)\: /}) { |c| puts "9 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => "n", "Match" => /character\: /}) { |c| puts "10 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
  @connection.cmd({"String" => @user[:char_name], "Match" => /Command\: /}) { |c| puts "11 #{c.gsub(/\e/, "[ESC]")}" unless c.nil? }
end

Well, that’s getting kind of ugly. Let’s do some refactoring. A few helper methods would be good:

features/lib/misc_helper.rb
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def telnet_commands(connection, commands)
  commands.each do |command|
    output = telnet_command(connection, command[0], command[1])
  end
  output
end

def telnet_command(connection, string, match, overrides = {})
  puts "Sending #{string} and waiting for #{match}..."
  connection.cmd({"String" => string, "Match" => match}.merge(overrides)) { |c| puts ansi_strip(c) unless c.nil? }
end

def ansi_strip(string)
  parsed = string.gsub(/\e\[[^a-zA-Z]*[a-zA-Z]/, "")
  # puts "I made #{string} into #{parsed}!"
end

So now I can change the step to:

features/steps/login_steps.rb
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
When(/^I create an account and character$/) do
  telnet_command(@connection, "", /Login\: /, {"Timeout" => 120})
  telnet_commands(@connection, [["new", /account\: /],
                                [@user[:acct_name], /address\: /],
                                [@user[:acct_email], /address\: /],
                                [@user[:acct_email], /12\)\: /],
                                [@user[:acct_password], /password\: /],
                                [@user[:acct_password], /Gender\: /],
                                [@user[:char_gender], /a Race\: /],
                                [@user[:char_race], /Class\: /],
                                [@user[:char_class], /\(y,n\)\: /],
                                ["n", /character\: /],
                                [@user[:char_name], /Command\: /]
                               ])
end

which seems a bit more manageable. Let the refactoring commence!

Wait, what?
#

So I keep refactoring, doing stuff like this:

features/steps/login_steps.rb
65
66
67
68
69
70
71
72
73
When(/^I log in with the wrong then right password$/) do
  @connection = create_telnet_connection
  telnet_command(@connection, "", /Login\: /, {"Timeout" => 120})
  output = telnet_commands(@connection, [[@user[:acct_name], /Password\: /],
                                         ["oops", /:/]])
  expect(output).to include("Invalid password.")
  telnet_commands(@connection, [[@user[:acct_name], /Password\: /],
                                [@user[:acct_password], /Command\: /]])
end

and I get this:

Sending oops and waiting for /:/...
Invalid password.
login:
expected output to some stream to include "Invalid password.", but it does not respond to `include?`
Diff:
@@ -1,2 +1,2 @@
-["Invalid password."]
+(output to some stream)
 (RSpec::Expectations::ExpectationNotMetError)
./features/steps/login_steps.rb:70:in `/^I log in with the wrong then right password$/'
features/login.feature:25:in `And I log in with the wrong then right password'

What the heck is “output to some stream”? Maybe what’s coming back from Net::Telnet#cmd isn’t quite what I expected. I’ll add a .to_s to the return from telnet_commands to sort that out, and:

expected "#<RSpec::Matchers::BuiltIn::Output:0x3114558>" to include "Invalid password." (RSpec::Expectations::ExpectationNotMetError)
./features/steps/login_steps.rb:70:in `/^I log in with the wrong then right password$/'

Oh? Oh. Rspec 3 has a built-in matcher called output. I’ll have to remember that in future and name things better.


Clarification
#

Just to be clear, that “too sick to go to work but not too sick to make games” thing really never happens. I’m a software engineer with a decent internet connection; if I can do gamedev I can do my job. So it waits until after hours, sick or no.


More to come
More to come

Day 38 code - tests

Tool - TinyTDS