001/**************************************************************** 002 * Licensed to the Apache Software Foundation (ASF) under one * 003 * or more contributor license agreements. See the NOTICE file * 004 * distributed with this work for additional information * 005 * regarding copyright ownership. The ASF licenses this file * 006 * to you under the Apache License, Version 2.0 (the * 007 * "License"); you may not use this file except in compliance * 008 * with the License. You may obtain a copy of the License at * 009 * * 010 * http://www.apache.org/licenses/LICENSE-2.0 * 011 * * 012 * Unless required by applicable law or agreed to in writing, * 013 * software distributed under the License is distributed on an * 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 015 * KIND, either express or implied. See the License for the * 016 * specific language governing permissions and limitations * 017 * under the License. * 018 ****************************************************************/ 019 020package org.apache.james.mpt.protocol; 021 022import java.io.IOException; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.Iterator; 026import java.util.List; 027import java.util.Map; 028import java.util.concurrent.TimeUnit; 029import java.util.regex.Pattern; 030 031import org.apache.james.mpt.api.ProtocolInteractor; 032import org.apache.james.mpt.api.Session; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036import com.google.common.base.Stopwatch; 037 038/** 039 * A protocol session which can be run against a reader and writer, which checks 040 * the server response against the expected values. TODO make ProtocolSession 041 * itself be a permissible ProtocolElement, so that we can nest and reuse 042 * sessions. 043 * 044 * @author Darrell DeBoer <darrell@apache.org> 045 */ 046public class ProtocolSession implements ProtocolInteractor { 047 048 private static final Logger LOGGER = LoggerFactory.getLogger(ProtocolSession.class); 049 050 private boolean continued = false; 051 052 private boolean continuationExpected = false; 053 054 private int maxSessionNumber; 055 056 protected List<ProtocolElement> testElements = new ArrayList<ProtocolElement>(); 057 058 private Iterator<ProtocolElement> elementsIterator; 059 060 private Session[] sessions; 061 062 private ProtocolElement nextTest; 063 064 private boolean continueAfterFailure = false; 065 066 private Map<String, Stopwatch> timers = new HashMap<String, Stopwatch>(); 067 068 public final boolean isContinueAfterFailure() { 069 return continueAfterFailure; 070 } 071 072 enum LolLevel { 073 Debug, 074 Info, 075 Warn, 076 Err 077 } 078 079 public final void setContinueAfterFailure(boolean continueAfterFailure) { 080 this.continueAfterFailure = continueAfterFailure; 081 } 082 083 /** 084 * Returns the number of sessions required to run this ProtocolSession. If 085 * the number of readers and writers provided is less than this number, an 086 * exception will occur when running the tests. 087 */ 088 public int getSessionCount() { 089 return maxSessionNumber + 1; 090 } 091 092 /** 093 * Executes the ProtocolSession in real time against the readers and writers 094 * supplied, writing client requests and reading server responses in the 095 * order that they appear in the test elements. The index of a reader/writer 096 * in the array corresponds to the number of the session. If an exception 097 * occurs, no more test elements are executed. 098 * 099 * @param out 100 * The client requests are written to here. 101 * @param in 102 * The server responses are read from here. 103 */ 104 public void runSessions(Session[] sessions) throws Exception { 105 this.sessions = sessions; 106 elementsIterator = testElements.iterator(); 107 while (elementsIterator.hasNext()) { 108 Object obj = elementsIterator.next(); 109 if (obj instanceof ProtocolElement) { 110 ProtocolElement test = (ProtocolElement) obj; 111 test.testProtocol(sessions, continueAfterFailure); 112 } 113 } 114 } 115 116 public void doContinue() { 117 try { 118 if (continuationExpected) { 119 continued = true; 120 while (elementsIterator.hasNext()) { 121 Object obj = elementsIterator.next(); 122 if (obj instanceof ProtocolElement) { 123 nextTest = (ProtocolElement) obj; 124 125 if (!nextTest.isClient()) { 126 break; 127 } 128 nextTest.testProtocol(sessions, continueAfterFailure); 129 } 130 } 131 if (!elementsIterator.hasNext()) { 132 nextTest = null; 133 } 134 } 135 else { 136 throw new RuntimeException("Unexpected continuation"); 137 } 138 } 139 catch (Exception e) { 140 throw new RuntimeException(e); 141 } 142 } 143 144 /** 145 * adds a new Client request line to the test elements 146 */ 147 public void CL(String clientLine) { 148 testElements.add(new ClientRequest(clientLine)); 149 } 150 151 /** 152 * adds a new Server Response line to the test elements, with the specified 153 * location. 154 */ 155 public void SL(String serverLine, String location) { 156 testElements.add(new ServerResponse(serverLine, location)); 157 } 158 159 /** 160 * adds a new Server Unordered Block to the test elements. 161 */ 162 public void SUB(List<String> serverLines, String location) { 163 testElements.add(new ServerUnorderedBlockResponse(serverLines, location)); 164 } 165 166 /** 167 * adds a new Client request line to the test elements 168 */ 169 public void CL(int sessionNumber, String clientLine) { 170 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber); 171 testElements.add(new ClientRequest(sessionNumber, clientLine)); 172 } 173 174 /** 175 * Adds a continuation. To allow one thread to be used for testing. 176 */ 177 public void CONT(int sessionNumber) throws Exception { 178 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber); 179 testElements.add(new ContinuationElement(sessionNumber)); 180 } 181 182 /** 183 * adds a new Server Response line to the test elements, with the specified 184 * location. 185 */ 186 public void SL(int sessionNumber, String serverLine, String location, String lastClientMessage) { 187 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber); 188 testElements.add(new ServerResponse(sessionNumber, serverLine, location, lastClientMessage)); 189 } 190 191 /** 192 * adds a new Server Unordered Block to the test elements. 193 */ 194 public void SUB(int sessionNumber, List<String> serverLines, String location, String lastClientMessage) { 195 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber); 196 testElements.add(new ServerUnorderedBlockResponse(sessionNumber, serverLines, location, lastClientMessage)); 197 } 198 199 /** 200 * adds a Wait condition 201 */ 202 public void WAIT(int sessionNumber, long timeToWaitInMs) { 203 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber); 204 testElements.add(new WaitElement(timeToWaitInMs)); 205 } 206 207 public void LOG(int sessionNumber, LolLevel level, String message) { 208 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber); 209 testElements.add(new LogElement(level, message)); 210 } 211 212 public void REINIT(int sessionNumber) { 213 this.maxSessionNumber = Math.max(this.maxSessionNumber, sessionNumber); 214 testElements.add(new ReinitElement(sessionNumber)); 215 } 216 217 public void TIMER(TimerCommand timerCommand, String timerName) { 218 testElements.add(new TimerElement(timerCommand, timerName)); 219 } 220 221 /** 222 * A client request, which write the specified message to a Writer. 223 */ 224 private static class ClientRequest implements ProtocolElement { 225 private final int sessionNumber; 226 227 private final String message; 228 229 /** 230 * Initialises the ClientRequest with the supplied message. 231 */ 232 public ClientRequest(String message) { 233 this(-1, message); 234 } 235 236 /** 237 * Initialises the ClientRequest, with a message and session number. 238 * 239 * @param sessionNumber 240 * @param message 241 */ 242 public ClientRequest(int sessionNumber, String message) { 243 this.sessionNumber = sessionNumber; 244 this.message = message; 245 } 246 247 /** 248 * Writes the request message to the PrintWriters. If the sessionNumber 249 * == -1, the request is written to *all* supplied writers, otherwise, 250 * only the writer for this session is writted to. 251 * 252 * @throws Exception 253 */ 254 public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception { 255 if (sessionNumber < 0) { 256 for (Session session : sessions) { 257 writeMessage(session); 258 } 259 } 260 else { 261 Session session = sessions[sessionNumber]; 262 writeMessage(session); 263 } 264 } 265 266 private void writeMessage(Session session) throws Exception { 267 session.writeLine(message); 268 } 269 270 public boolean isClient() { 271 return true; 272 } 273 } 274 275 /** 276 * Represents a single-line server response, which reads a line from a 277 * reader, and compares it with the defined regular expression definition of 278 * this line. 279 */ 280 private class ServerResponse implements ProtocolElement { 281 private final String lastClientMessage; 282 283 private final int sessionNumber; 284 285 private final String expectedLine; 286 287 protected String location; 288 289 /** 290 * Sets up a server response. 291 * 292 * @param expectedPattern 293 * A Perl regular expression pattern used to test the line 294 * recieved. 295 * @param location 296 * A descriptive value to use in error messages. 297 */ 298 public ServerResponse(String expectedPattern, String location) { 299 this(-1, expectedPattern, location, null); 300 } 301 302 /** 303 * Sets up a server response. 304 * 305 * @param sessionNumber 306 * The number of session for a multi-session test 307 * @param expectedPattern 308 * A Perl regular expression pattern used to test the line 309 * recieved. 310 * @param location 311 * A descriptive value to use in error messages. 312 */ 313 public ServerResponse(int sessionNumber, String expectedPattern, String location, String lastClientMessage) { 314 this.sessionNumber = sessionNumber; 315 this.expectedLine = expectedPattern; 316 this.location = location; 317 this.lastClientMessage = lastClientMessage; 318 } 319 320 /** 321 * Reads a line from the supplied reader, and tests that it matches the 322 * expected regular expression. If the sessionNumber == -1, then all 323 * readers are tested, otherwise, only the reader for this session is 324 * tested. 325 * 326 * @param out 327 * Is ignored. 328 * @param in 329 * The server response is read from here. 330 * @throws InvalidServerResponseException 331 * If the actual server response didn't match the regular 332 * expression expected. 333 */ 334 public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception { 335 if (sessionNumber < 0) { 336 for (Session session : sessions) { 337 checkResponse(session, continueAfterFailure); 338 } 339 } 340 else { 341 Session session = sessions[sessionNumber]; 342 checkResponse(session, continueAfterFailure); 343 } 344 } 345 346 protected void checkResponse(Session session, boolean continueAfterFailure) throws Exception { 347 String testLine = readLine(session); 348 if (!match(expectedLine, testLine)) { 349 String errMsg = "\nLocation: " + location + "\nLastClientMsg: " + lastClientMessage + "\nExpected: '" 350 + expectedLine + "'\nActual : '" + testLine + "'"; 351 if (continueAfterFailure) { 352 System.out.println(errMsg); 353 } 354 else { 355 throw new InvalidServerResponseException(errMsg); 356 } 357 } 358 } 359 360 /** 361 * A convenience method which returns true if the actual string matches 362 * the expected regular expression. 363 * 364 * @param expected 365 * The regular expression used for matching. 366 * @param actual 367 * The actual message to match. 368 * @return <code>true</code> if the actual matches the expected. 369 */ 370 protected boolean match(String expected, String actual) { 371 return Pattern.matches(expected, actual); 372 } 373 374 /** 375 * Grabs a line from the server and throws an error message if it 376 * doesn't work out 377 * 378 * @return String of the line from the server 379 */ 380 protected String readLine(Session session) throws Exception { 381 try { 382 return session.readLine(); 383 } 384 catch (IOException e) { 385 String errMsg = "\nLocation: " + location + "\nExpected: " + expectedLine + "\nReason: Server Timeout."; 386 throw new InvalidServerResponseException(errMsg); 387 } 388 } 389 390 public boolean isClient() { 391 return false; 392 } 393 } 394 395 /** 396 * Represents a set of lines which must be recieved from the server, in a 397 * non-specified order. 398 */ 399 private class ServerUnorderedBlockResponse extends ServerResponse { 400 private List<String> expectedLines = new ArrayList<String>(); 401 402 /** 403 * Sets up a ServerUnorderedBlockResponse with the list of expected 404 * lines. 405 * 406 * @param expectedLines 407 * A list containing a reqular expression for each expected 408 * line. 409 * @param location 410 * A descriptive location string for error messages. 411 */ 412 public ServerUnorderedBlockResponse(List<String> expectedLines, String location) { 413 this(-1, expectedLines, location, null); 414 } 415 416 /** 417 * Sets up a ServerUnorderedBlockResponse with the list of expected 418 * lines. 419 * 420 * @param sessionNumber 421 * The number of the session to expect this block, for a 422 * multi-session test. 423 * @param expectedLines 424 * A list containing a reqular expression for each expected 425 * line. 426 * @param location 427 * A descriptive location string for error messages. 428 */ 429 public ServerUnorderedBlockResponse(int sessionNumber, List<String> expectedLines, String location, 430 String lastClientMessage) { 431 super(sessionNumber, "<Unordered Block>", location, lastClientMessage); 432 this.expectedLines = expectedLines; 433 } 434 435 /** 436 * Reads lines from the server response and matches them against the 437 * list of expected regular expressions. Each regular expression in the 438 * expected list must be matched by only one server response line. 439 * 440 * @param reader 441 * Server responses are read from here. 442 * @throws InvalidServerResponseException 443 * If a line is encountered which doesn't match one of the 444 * expected lines. 445 */ 446 protected void checkResponse(Session session, boolean continueAfterFailure) throws Exception { 447 List<String> testLines = new ArrayList<String>(expectedLines); 448 while (testLines.size() > 0) { 449 String actualLine = readLine(session); 450 451 boolean foundMatch = false; 452 for (int i = 0; i < testLines.size(); i++) { 453 String expected = (String) testLines.get(i); 454 if (match(expected, actualLine)) { 455 foundMatch = true; 456 testLines.remove(expected); 457 break; 458 } 459 } 460 461 if (!foundMatch) { 462 StringBuffer errMsg = new StringBuffer().append("\nLocation: ").append(location) 463 .append("\nExpected one of: "); 464 Iterator<String> iter = expectedLines.iterator(); 465 while (iter.hasNext()) { 466 errMsg.append("\n "); 467 errMsg.append(iter.next()); 468 } 469 errMsg.append("\nActual: ").append(actualLine); 470 if (continueAfterFailure) { 471 System.out.println(errMsg.toString()); 472 } 473 else { 474 throw new InvalidServerResponseException(errMsg.toString()); 475 } 476 } 477 } 478 } 479 } 480 481 private class ContinuationElement implements ProtocolElement { 482 483 private final int sessionNumber; 484 485 public ContinuationElement(int sessionNumber) throws Exception { 486 this.sessionNumber = Math.max(0, sessionNumber); 487 } 488 489 public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception { 490 Session session = sessions[sessionNumber]; 491 continuationExpected = true; 492 continued = false; 493 String testLine = session.readLine(); 494 if (!"+".equals(testLine) || !continued) { 495 final String message = "Expected continuation"; 496 if (continueAfterFailure) { 497 System.out.print(message); 498 } 499 else { 500 throw new InvalidServerResponseException(message); 501 } 502 } 503 continuationExpected = false; 504 continued = false; 505 506 if (nextTest != null) { 507 nextTest.testProtocol(sessions, continueAfterFailure); 508 } 509 } 510 511 public boolean isClient() { 512 return false; 513 } 514 } 515 516 private class ReinitElement implements ProtocolElement { 517 518 private final int sessionNumber; 519 520 public ReinitElement(int sessionNumber) { 521 this.sessionNumber = Math.max(0, sessionNumber); 522 } 523 524 public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception { 525 Session session = sessions[sessionNumber]; 526 session.restart(); 527 } 528 529 public boolean isClient() { 530 return false; 531 } 532 } 533 534 protected enum TimerCommand { 535 START, PRINT, RESET; 536 537 public static TimerCommand from(String value) throws InvalidServerResponseException { 538 if (value.equalsIgnoreCase("start")) { 539 return START; 540 } 541 if (value.equalsIgnoreCase("print")) { 542 return PRINT; 543 } 544 if (value.equalsIgnoreCase("reset")) { 545 return RESET; 546 } 547 throw new InvalidServerResponseException("Invalid TIMER command '" + value + "'"); 548 } 549 } 550 551 private class TimerElement implements ProtocolElement { 552 553 private TimerCommand timerCommand; 554 private String timerName; 555 556 public TimerElement(TimerCommand timerCommand, String timerName) { 557 this.timerCommand = timerCommand; 558 this.timerName = timerName; 559 } 560 561 @Override 562 public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception { 563 switch(timerCommand) { 564 case START: 565 start(); 566 break; 567 case PRINT: 568 print(); 569 break; 570 case RESET: 571 reset(); 572 break; 573 default: 574 throw new InvalidServerResponseException("Invalid TIMER command '" + timerCommand + "' for timer name: '" + timerName + "'"); 575 } 576 } 577 578 private void start() { 579 timers.put(timerName, Stopwatch.createStarted()); 580 } 581 582 private void print() throws InvalidServerResponseException { 583 Stopwatch stopwatch = timers.get(timerName); 584 if (stopwatch == null) { 585 throw new InvalidServerResponseException("TIMER '" + timerName + "' undefined"); 586 } 587 LOGGER.info("Time spent in '" + timerName + "': " + stopwatch.elapsed(TimeUnit.MILLISECONDS) + " ms"); 588 } 589 590 private void reset() throws InvalidServerResponseException { 591 Stopwatch stopwatch = timers.get(timerName); 592 if (stopwatch == null) { 593 throw new InvalidServerResponseException("TIMER '" + timerName + "' undefined"); 594 } 595 stopwatch.reset(); 596 stopwatch.start(); 597 } 598 599 @Override 600 public boolean isClient() { 601 return false; 602 } 603 } 604 605 /** 606 * Allow you to wait a given time at a given point of the test script 607 */ 608 private class WaitElement implements ProtocolElement { 609 610 private final long timeToWaitInMs; 611 612 public WaitElement(long timeToWaitInMs) { 613 this.timeToWaitInMs = timeToWaitInMs; 614 } 615 616 @Override 617 public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception { 618 Thread.sleep(timeToWaitInMs); 619 } 620 621 @Override 622 public boolean isClient() { 623 return false; 624 } 625 } 626 627 /** 628 * Allow you to wait a given time at a given point of the test script 629 */ 630 private class LogElement implements ProtocolElement { 631 632 private final LolLevel level; 633 private final String message; 634 635 public LogElement(LolLevel level, String message) { 636 this.level = level; 637 this.message = message; 638 } 639 640 @Override 641 public void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception { 642 switch (level) { 643 case Debug: 644 LOGGER.debug(message); 645 break; 646 case Info: 647 LOGGER.info(message); 648 break; 649 case Warn: 650 LOGGER.warn(message); 651 break; 652 case Err: 653 LOGGER.error(message); 654 break; 655 } 656 } 657 658 @Override 659 public boolean isClient() { 660 return false; 661 } 662 } 663 664 /** 665 * Represents a generic protocol element, which may write requests to the 666 * server, read responses from the server, or both. Implementations should 667 * test the server response against an expected response, and throw an 668 * exception on mismatch. 669 */ 670 private interface ProtocolElement { 671 /** 672 * Executes the ProtocolElement against the supplied session. 673 * 674 * @param continueAfterFailure 675 * TODO 676 * @throws Exception 677 */ 678 void testProtocol(Session[] sessions, boolean continueAfterFailure) throws Exception; 679 680 boolean isClient(); 681 } 682 683 /** 684 * An exception which is thrown when the actual response from a server is 685 * different from that expected. 686 */ 687 @SuppressWarnings("serial") 688 public static class InvalidServerResponseException extends Exception { 689 public InvalidServerResponseException(String message) { 690 super(message); 691 } 692 } 693 694 /** 695 * Constructs a <code>String</code> with all attributes in name = value 696 * format. 697 * 698 * @return a <code>String</code> representation of this object. 699 */ 700 public String toString() { 701 final String TAB = " "; 702 703 String result = "ProtocolSession ( " + "continued = " + this.continued + TAB + "continuationExpected = " 704 + this.continuationExpected + TAB + "maxSessionNumber = " + this.maxSessionNumber + TAB 705 + "testElements = " + this.testElements + TAB + "elementsIterator = " + this.elementsIterator + TAB 706 + "sessions = " + this.sessions + TAB + "nextTest = " + this.nextTest + TAB + "continueAfterFailure = " 707 + this.continueAfterFailure + TAB + " )"; 708 709 return result; 710 } 711 712}