6 from framework import VppTestCase, VppTestRunner
7 from vpp_ip_route import IpRoute, RoutePath
8 from vpp_lo_interface import VppLoInterface
10 from scapy.layers.l2 import Ether, getmacbyip
11 from scapy.layers.inet import IP, UDP, ICMP
12 from scapy.layers.inet6 import IPv6, in6_getnsmac, in6_mactoifaceid
13 from scapy.layers.dhcp import DHCP, BOOTP, DHCPTypes
14 from scapy.layers.dhcp6 import DHCP6, DHCP6_Solicit, DHCP6_RelayForward, \
15 DHCP6_RelayReply, DHCP6_Advertise, DHCP6OptRelayMsg, DHCP6OptIfaceId, \
16 DHCP6OptStatusCode, DHCP6OptVSS, DHCP6OptClientLinkLayerAddr
17 from socket import AF_INET, AF_INET6
18 from scapy.utils import inet_pton, inet_ntop
19 from scapy.utils6 import in6_ptop
21 DHCP4_CLIENT_PORT = 68
22 DHCP4_SERVER_PORT = 67
23 DHCP6_CLIENT_PORT = 547
24 DHCP6_SERVER_PORT = 546
29 euid = in6_mactoifaceid(mac)
30 addr = "fe80::" + euid
34 class TestDHCP(VppTestCase):
35 """ DHCP Test Case """
38 super(TestDHCP, self).setUp()
40 # create 3 pg interfaces
41 self.create_pg_interfaces(range(4))
43 # pg0 and 1 are IP configured in VRF 0 and 1.
44 # pg2 and 3 are non IP-configured in VRF 0 and 1
46 for i in self.pg_interfaces[:2]:
48 i.set_table_ip4(table_id)
49 i.set_table_ip6(table_id)
57 for i in self.pg_interfaces[2:]:
59 i.set_table_ip4(table_id)
60 i.set_table_ip6(table_id)
63 def send_and_assert_no_replies(self, intf, pkts, remark):
65 self.pg_enable_capture(self.pg_interfaces)
67 for i in self.pg_interfaces:
68 i.assert_nothing_captured(remark=remark)
70 def validate_option_82(self, pkt, intf, ip_addr):
75 for i in dhcp.options:
77 if i[0] == "relay_agent_Information":
79 # There are two sb-options present - each of length 6.
82 self.assertEqual(len(data), 12)
85 # First sub-option is ID 1, len 4, then encoded
86 # sw_if_index. This test uses low valued indicies
88 # The ID space is VPP internal - so no matching value
91 self.assertEqual(ord(data[0]), 1)
92 self.assertEqual(ord(data[1]), 4)
93 self.assertEqual(ord(data[2]), 0)
94 self.assertEqual(ord(data[3]), 0)
95 self.assertEqual(ord(data[4]), 0)
96 self.assertEqual(ord(data[5]), intf._sw_if_index)
99 # next sub-option is the IP address of the client side
101 # sub-option ID=5, length (of a v4 address)=4
103 claddr = socket.inet_pton(AF_INET, ip_addr)
105 self.assertEqual(ord(data[6]), 5)
106 self.assertEqual(ord(data[7]), 4)
107 self.assertEqual(data[8], claddr[0])
108 self.assertEqual(data[9], claddr[1])
109 self.assertEqual(data[10], claddr[2])
110 self.assertEqual(data[11], claddr[3])
113 self.assertTrue(found)
117 def verify_dhcp_offer(self, pkt, intf, check_option_82=True):
119 self.assertEqual(ether.dst, "ff:ff:ff:ff:ff:ff")
120 self.assertEqual(ether.src, intf.local_mac)
123 self.assertEqual(ip.dst, "255.255.255.255")
124 self.assertEqual(ip.src, intf.local_ip4)
127 self.assertEqual(udp.dport, DHCP4_CLIENT_PORT)
128 self.assertEqual(udp.sport, DHCP4_SERVER_PORT)
132 for o in dhcp.options:
134 if o[0] == "message-type" \
135 and DHCPTypes[o[1]] == "offer":
137 self.assertTrue(is_offer)
140 data = self.validate_option_82(pkt, intf, intf.local_ip4)
142 def verify_dhcp_discover(self, pkt, intf, src_intf=None,
143 option_82_present=True):
145 self.assertEqual(ether.dst, intf.remote_mac)
146 self.assertEqual(ether.src, intf.local_mac)
149 self.assertEqual(ip.dst, intf.remote_ip4)
150 self.assertEqual(ip.src, intf.local_ip4)
153 self.assertEqual(udp.dport, DHCP4_SERVER_PORT)
154 self.assertEqual(udp.sport, DHCP4_CLIENT_PORT)
159 for o in dhcp.options:
161 if o[0] == "message-type" \
162 and DHCPTypes[o[1]] == "discover":
164 self.assertTrue(is_discover)
166 if option_82_present:
167 data = self.validate_option_82(pkt, src_intf, src_intf.local_ip4)
170 for i in dhcp.options:
172 self.assertNotEqual(i[0], "relay_agent_Information")
174 def verify_dhcp6_solicit(self, pkt, intf,
179 self.assertEqual(ether.dst, intf.remote_mac)
180 self.assertEqual(ether.src, intf.local_mac)
183 self.assertEqual(in6_ptop(ip.dst), in6_ptop(intf.remote_ip6))
184 self.assertEqual(in6_ptop(ip.src), in6_ptop(intf.local_ip6))
187 self.assertEqual(udp.dport, DHCP6_CLIENT_PORT)
188 self.assertEqual(udp.sport, DHCP6_SERVER_PORT)
190 relay = pkt[DHCP6_RelayForward]
191 self.assertEqual(in6_ptop(relay.peeraddr), in6_ptop(peer_ip))
192 oid = pkt[DHCP6OptIfaceId]
193 cll = pkt[DHCP6OptClientLinkLayerAddr]
194 self.assertEqual(cll.optlen, 8)
195 self.assertEqual(cll.lltype, 1)
196 self.assertEqual(cll.clladdr, peer_mac)
198 vss = pkt[DHCP6OptVSS]
199 self.assertEqual(vss.optlen, 8)
200 self.assertEqual(vss.type, 1)
201 # the OUI and FIB-id are really 3 and 4 bytes resp.
202 # but the tested range is small
203 self.assertEqual(ord(vss.data[0]), 0)
204 self.assertEqual(ord(vss.data[1]), 0)
205 self.assertEqual(ord(vss.data[2]), oui)
206 self.assertEqual(ord(vss.data[3]), 0)
207 self.assertEqual(ord(vss.data[4]), 0)
208 self.assertEqual(ord(vss.data[5]), 0)
209 self.assertEqual(ord(vss.data[6]), fib_id)
211 # the relay message should be an encoded Solicit
212 msg = pkt[DHCP6OptRelayMsg]
213 sol = DHCP6_Solicit()
214 self.assertEqual(msg.optlen, len(str(sol)))
215 self.assertEqual(str(sol), (str(msg[1]))[:msg.optlen])
217 def verify_dhcp6_advert(self, pkt, intf, peer):
219 self.assertEqual(ether.dst, "ff:ff:ff:ff:ff:ff")
220 self.assertEqual(ether.src, intf.local_mac)
223 self.assertEqual(in6_ptop(ip.dst), in6_ptop(peer))
224 self.assertEqual(in6_ptop(ip.src), in6_ptop(intf.local_ip6))
227 self.assertEqual(udp.dport, DHCP6_SERVER_PORT)
228 self.assertEqual(udp.sport, DHCP6_CLIENT_PORT)
230 # not sure why this is not decoding
231 # adv = pkt[DHCP6_Advertise]
233 def test_dhcp_proxy(self):
237 # Verify no response to DHCP request without DHCP config
239 p_disc_vrf0 = (Ether(dst="ff:ff:ff:ff:ff:ff",
240 src=self.pg2.remote_mac) /
241 IP(src="0.0.0.0", dst="255.255.255.255") /
242 UDP(sport=DHCP4_CLIENT_PORT,
243 dport=DHCP4_SERVER_PORT) /
245 DHCP(options=[('message-type', 'discover'), ('end')]))
246 pkts_disc_vrf0 = [p_disc_vrf0]
247 p_disc_vrf1 = (Ether(dst="ff:ff:ff:ff:ff:ff",
248 src=self.pg3.remote_mac) /
249 IP(src="0.0.0.0", dst="255.255.255.255") /
250 UDP(sport=DHCP4_CLIENT_PORT,
251 dport=DHCP4_SERVER_PORT) /
253 DHCP(options=[('message-type', 'discover'), ('end')]))
254 pkts_disc_vrf1 = [p_disc_vrf0]
256 self.send_and_assert_no_replies(self.pg2, pkts_disc_vrf0,
257 "DHCP with no configuration")
258 self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1,
259 "DHCP with no configuration")
262 # Enable DHCP proxy in VRF 0
264 server_addr = self.pg0.remote_ip4n
265 src_addr = self.pg0.local_ip4n
267 self.vapi.dhcp_proxy_config(server_addr,
272 # Now a DHCP request on pg2, which is in the same VRF
273 # as the DHCP config, will result in a relayed DHCP
274 # message to the [fake] server
276 self.pg2.add_stream(pkts_disc_vrf0)
277 self.pg_enable_capture(self.pg_interfaces)
280 rx = self.pg0.get_capture(1)
284 # Rx'd packet should be to the server address and from the configured
286 # UDP source ports are unchanged
287 # we've no option 82 config so that should be absent
289 self.verify_dhcp_discover(rx, self.pg0, option_82_present=False)
292 # Inject a response from the server
293 # VPP will only relay the offer if option 82 is present.
294 # so this one is dropped
296 p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
297 IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) /
298 UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) /
300 DHCP(options=[('message-type', 'offer'), ('end')]))
303 self.send_and_assert_no_replies(self.pg0, pkts,
304 "DHCP offer no option 82")
307 # Configure sending option 82 in relayed messages
309 self.vapi.dhcp_proxy_config(server_addr,
316 # again dropped, but ths time because there is no IP addrees on the
317 # clinet interfce to fill in the option.
319 self.send_and_assert_no_replies(self.pg2, pkts_disc_vrf0,
320 "DHCP no relay address")
323 # configure an IP address on the client facing interface
325 self.pg2.config_ip4()
328 # Try again with a discover packet
329 # Rx'd packet should be to the server address and from the configured
331 # UDP source ports are unchanged
332 # we've no option 82 config so that should be absent
334 self.pg2.add_stream(pkts_disc_vrf0)
335 self.pg_enable_capture(self.pg_interfaces)
338 rx = self.pg0.get_capture(1)
341 option_82 = self.verify_dhcp_discover(rx, self.pg0, src_intf=self.pg2)
344 # Create an DHCP offer reply from the server with a correctly formatted
345 # option 82. i.e. send back what we just captured
346 # The offer, sent mcast to the client, still has option 82.
348 p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
349 IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) /
350 UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) /
352 DHCP(options=[('message-type', 'offer'),
353 ('relay_agent_Information', option_82),
357 self.pg0.add_stream(pkts)
358 self.pg_enable_capture(self.pg_interfaces)
361 rx = self.pg2.get_capture(1)
364 self.verify_dhcp_offer(rx, self.pg2)
369 # 1. not our IP address = not checked by VPP? so offer is replayed
371 bad_ip = option_82[0:8] + chr(33) + option_82[9:]
373 p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
374 IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) /
375 UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) /
377 DHCP(options=[('message-type', 'offer'),
378 ('relay_agent_Information', bad_ip),
382 self.pg0.add_stream(pkts)
383 self.pg_enable_capture(self.pg_interfaces)
385 rx = self.pg2.get_capture(1)
388 self.verify_dhcp_offer(rx, self.pg2, check_option_82=False)
389 self.pg0.assert_nothing_captured(remark="")
391 # 2. Not a sw_if_index VPP knows
392 bad_if_index = option_82[0:2] + chr(33) + option_82[3:]
394 p = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
395 IP(src=self.pg0.remote_ip4, dst=self.pg0.local_ip4) /
396 UDP(sport=DHCP4_SERVER_PORT, dport=DHCP4_SERVER_PORT) /
398 DHCP(options=[('message-type', 'offer'),
399 ('relay_agent_Information', bad_if_index),
402 self.send_and_assert_no_replies(self.pg0, pkts,
403 "DHCP offer option 82 bad if index")
406 # Send a DHCP request in VRF 1. should be dropped.
408 self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1,
409 "DHCP with no configuration VRF 1")
412 # Delete the DHCP config in VRF 0
413 # Should now drop requests.
415 self.vapi.dhcp_proxy_config(server_addr,
421 self.send_and_assert_no_replies(self.pg2, pkts_disc_vrf0,
422 "DHCP config removed VRF 0")
423 self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1,
424 "DHCP config removed VRF 1")
427 # Add DHCP config for VRF 1
429 server_addr = self.pg1.remote_ip4n
430 src_addr = self.pg1.local_ip4n
431 self.vapi.dhcp_proxy_config(server_addr,
438 # Confim DHCP requests ok in VRF 1.
439 # - dropped on IP config on client interface
441 self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1,
442 "DHCP config removed VRF 1")
445 # configure an IP address on the client facing interface
447 self.pg3.config_ip4()
449 self.pg3.add_stream(pkts_disc_vrf1)
450 self.pg_enable_capture(self.pg_interfaces)
453 rx = self.pg1.get_capture(1)
455 self.verify_dhcp_discover(rx, self.pg1, src_intf=self.pg3)
458 # remove DHCP config to cleanup
460 self.vapi.dhcp_proxy_config(server_addr,
467 self.send_and_assert_no_replies(self.pg2, pkts_disc_vrf0,
468 "DHCP cleanup VRF 0")
469 self.send_and_assert_no_replies(self.pg3, pkts_disc_vrf1,
470 "DHCP cleanup VRF 1")
472 def test_dhcp6_proxy(self):
475 # Verify no response to DHCP request without DHCP config
477 dhcp_solicit_dst = "ff02::1:2"
478 dhcp_solicit_src_vrf0 = mk_ll_addr(self.pg2.remote_mac)
479 dhcp_solicit_src_vrf1 = mk_ll_addr(self.pg3.remote_mac)
480 server_addr_vrf0 = self.pg0.remote_ip6n
481 src_addr_vrf0 = self.pg0.local_ip6n
482 server_addr_vrf1 = self.pg1.remote_ip6n
483 src_addr_vrf1 = self.pg1.local_ip6n
486 # Add the Route to receive the DHCP packets
488 route_dhcp_vrf0 = IpRoute(self, dhcp_solicit_dst, 128,
489 [], is_local=1, is_ip6=1)
490 route_dhcp_vrf0.add_vpp_config()
491 route_dhcp_vrf1 = IpRoute(self, dhcp_solicit_dst, 128,
492 [], is_local=1, is_ip6=1,
494 route_dhcp_vrf1.add_vpp_config()
496 dmac = in6_getnsmac(inet_pton(socket.AF_INET6, dhcp_solicit_dst))
497 p_solicit_vrf0 = (Ether(dst=dmac, src=self.pg2.remote_mac) /
498 IPv6(src=dhcp_solicit_src_vrf0,
499 dst=dhcp_solicit_dst) /
500 UDP(sport=DHCP6_SERVER_PORT,
501 dport=DHCP6_CLIENT_PORT) /
503 pkts_solicit_vrf0 = [p_solicit_vrf0]
504 p_solicit_vrf1 = (Ether(dst=dmac, src=self.pg3.remote_mac) /
505 IPv6(src=dhcp_solicit_src_vrf1,
506 dst=dhcp_solicit_dst) /
507 UDP(sport=DHCP6_SERVER_PORT,
508 dport=DHCP6_CLIENT_PORT) /
510 pkts_solicit_vrf1 = [p_solicit_vrf1]
512 self.send_and_assert_no_replies(self.pg2, pkts_solicit_vrf0,
513 "DHCP with no configuration")
514 self.send_and_assert_no_replies(self.pg3, pkts_solicit_vrf1,
515 "DHCP with no configuration")
518 # DHCPv6 config in VRF 0.
519 # Packets still dropped because the client facing interface has no
522 self.vapi.dhcp_proxy_config(server_addr_vrf0,
529 self.send_and_assert_no_replies(self.pg2, pkts_solicit_vrf0,
530 "DHCP with no configuration")
531 self.send_and_assert_no_replies(self.pg3, pkts_solicit_vrf1,
532 "DHCP with no configuration")
535 # configure an IP address on the client facing interface
537 self.pg2.config_ip6()
540 # Now the DHCP requests are relayed to the server
542 self.pg2.add_stream(pkts_solicit_vrf0)
543 self.pg_enable_capture(self.pg_interfaces)
546 rx = self.pg0.get_capture(1)
548 self.verify_dhcp6_solicit(rx, self.pg0,
549 dhcp_solicit_src_vrf0,
553 # Exception cases for rejected relay responses
556 # 1 - not a relay reply
557 p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
558 IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) /
559 UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) /
561 pkts_adv_vrf0 = [p_adv_vrf0]
562 self.send_and_assert_no_replies(self.pg2, pkts_adv_vrf0,
563 "DHCP6 not a relay reply")
565 # 2 - no relay message option
566 p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
567 IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) /
568 UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) /
571 pkts_adv_vrf0 = [p_adv_vrf0]
572 self.send_and_assert_no_replies(self.pg2, pkts_adv_vrf0,
573 "DHCP not a relay message")
576 p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
577 IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) /
578 UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) /
580 DHCP6OptRelayMsg(optlen=0) /
582 pkts_adv_vrf0 = [p_adv_vrf0]
583 self.send_and_assert_no_replies(self.pg2, pkts_adv_vrf0,
584 "DHCP6 no circuit ID")
585 # 4 - wrong circuit ID
586 p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
587 IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) /
588 UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) /
590 DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x05') /
591 DHCP6OptRelayMsg(optlen=0) /
593 pkts_adv_vrf0 = [p_adv_vrf0]
594 self.send_and_assert_no_replies(self.pg2, pkts_adv_vrf0,
595 "DHCP6 wrong circuit ID")
598 # Send the relay response (the advertisement)
600 p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
601 IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) /
602 UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) /
604 DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x03') /
605 DHCP6OptRelayMsg(optlen=0) /
606 DHCP6_Advertise(trid=1) /
607 DHCP6OptStatusCode(statuscode=0))
608 pkts_adv_vrf0 = [p_adv_vrf0]
610 self.pg0.add_stream(pkts_adv_vrf0)
611 self.pg_enable_capture(self.pg_interfaces)
614 rx = self.pg2.get_capture(1)
616 self.verify_dhcp6_advert(rx, self.pg2, "::")
619 # Send the relay response (the advertisement)
620 # - with peer address
621 p_adv_vrf0 = (Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) /
622 IPv6(dst=self.pg0.local_ip6, src=self.pg0.remote_ip6) /
623 UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) /
624 DHCP6_RelayReply(peeraddr=dhcp_solicit_src_vrf0) /
625 DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x03') /
626 DHCP6OptRelayMsg(optlen=0) /
627 DHCP6_Advertise(trid=1) /
628 DHCP6OptStatusCode(statuscode=0))
629 pkts_adv_vrf0 = [p_adv_vrf0]
631 self.pg0.add_stream(pkts_adv_vrf0)
632 self.pg_enable_capture(self.pg_interfaces)
635 rx = self.pg2.get_capture(1)
637 self.verify_dhcp6_advert(rx, self.pg2, dhcp_solicit_src_vrf0)
640 # Add all the config for VRF 1
642 self.vapi.dhcp_proxy_config(server_addr_vrf1,
648 self.pg3.config_ip6()
653 self.pg3.add_stream(pkts_solicit_vrf1)
654 self.pg_enable_capture(self.pg_interfaces)
657 rx = self.pg1.get_capture(1)
659 self.verify_dhcp6_solicit(rx, self.pg1,
660 dhcp_solicit_src_vrf1,
666 p_adv_vrf1 = (Ether(dst=self.pg1.local_mac, src=self.pg1.remote_mac) /
667 IPv6(dst=self.pg1.local_ip6, src=self.pg1.remote_ip6) /
668 UDP(sport=DHCP6_SERVER_PORT, dport=DHCP6_SERVER_PORT) /
669 DHCP6_RelayReply(peeraddr=dhcp_solicit_src_vrf1) /
670 DHCP6OptIfaceId(optlen=4, ifaceid='\x00\x00\x00\x04') /
671 DHCP6OptRelayMsg(optlen=0) /
672 DHCP6_Advertise(trid=1) /
673 DHCP6OptStatusCode(statuscode=0))
674 pkts_adv_vrf1 = [p_adv_vrf1]
676 self.pg1.add_stream(pkts_adv_vrf1)
677 self.pg_enable_capture(self.pg_interfaces)
680 rx = self.pg3.get_capture(1)
682 self.verify_dhcp6_advert(rx, self.pg3, dhcp_solicit_src_vrf1)
686 # table=1, fib=id=1, oui=4
687 self.vapi.dhcp_proxy_set_vss(1, 1, 4, is_ip6=1)
689 self.pg3.add_stream(pkts_solicit_vrf1)
690 self.pg_enable_capture(self.pg_interfaces)
693 rx = self.pg1.get_capture(1)
695 self.verify_dhcp6_solicit(rx, self.pg1,
696 dhcp_solicit_src_vrf1,
702 # Remove the VSS config
703 # relayed DHCP has default vlaues in the option.
705 self.vapi.dhcp_proxy_set_vss(1, 1, 4, is_ip6=1, is_add=0)
707 self.pg3.add_stream(pkts_solicit_vrf1)
708 self.pg_enable_capture(self.pg_interfaces)
711 rx = self.pg1.get_capture(1)
713 self.verify_dhcp6_solicit(rx, self.pg1,
714 dhcp_solicit_src_vrf1,
720 self.vapi.dhcp_proxy_config(server_addr_vrf1,
727 self.vapi.dhcp_proxy_config(server_addr_vrf1,
735 route_dhcp_vrf0.remove_vpp_config()
736 route_dhcp_vrf1.remove_vpp_config()
738 if __name__ == '__main__':
739 unittest.main(testRunner=VppTestRunner)