diff options
| author | Caleb Maclennan <caleb@alerque.com> | 2026-08-31 10:57:58 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-31 10:57:58 +0300 |
| commit | 8f18ce95bb38c7f5c4bef5b3684cf1b0df1fc266 (patch) | |
| tree | c11ae97c461d7f9c3cfe84773b9975de48d5ba9c /test | |
| parent | 535178a3f0e2cff59f4d59ee3a655bee263a5c90 (diff) | |
| parent | 4863f32b358a8d533f629084a86fb4932ebbd2f3 (diff) | |
| download | luasocket-8f18ce95bb38c7f5c4bef5b3684cf1b0df1fc266.tar.gz luasocket-8f18ce95bb38c7f5c4bef5b3684cf1b0df1fc266.tar.bz2 luasocket-8f18ce95bb38c7f5c4bef5b3684cf1b0df1fc266.zip | |
Merge pull request #463 from lunarmodules/feat/receive-limit
feat(receive): add maxsize argument to bound memory usage
Diffstat (limited to 'test')
| -rw-r--r-- | test/testclnt.lua | 195 | ||||
| -rw-r--r-- | test/utestclnt.lua | 45 |
2 files changed, 240 insertions, 0 deletions
diff --git a/test/testclnt.lua b/test/testclnt.lua index 170e187..bbe48bb 100644 --- a/test/testclnt.lua +++ b/test/testclnt.lua | |||
| @@ -619,6 +619,198 @@ remote([[ | |||
| 619 | end | 619 | end |
| 620 | 620 | ||
| 621 | ------------------------------------------------------------------------ | 621 | ------------------------------------------------------------------------ |
| 622 | function test_maxsize() | ||
| 623 | -- Group A: argument errors, raised before any I/O | ||
| 624 | reconnect() | ||
| 625 | printf("argument errors: ") | ||
| 626 | -- bounds pre-implementation calls (where arg 4 is silently dropped and | ||
| 627 | -- these become real, otherwise-unbounded blocking reads on an idle | ||
| 628 | -- socket) so a meaningful failure doesn't hang the suite | ||
| 629 | data:settimeout(0.2) | ||
| 630 | local ok | ||
| 631 | ok = pcall(data.receive, data, "*l", nil, 0) | ||
| 632 | assert(not ok, "A1 failed: maxsize=0 should raise") | ||
| 633 | ok = pcall(data.receive, data, "*l", nil, -1) | ||
| 634 | assert(not ok, "A2 failed: maxsize=-1 should raise") | ||
| 635 | ok = pcall(data.receive, data, "*l", nil, "abc") | ||
| 636 | assert(not ok, "A3 failed: non-number maxsize should raise") | ||
| 637 | ok = pcall(data.receive, data, "*l", string.rep("x", 10), 10) | ||
| 638 | assert(not ok, "A4 failed: #prefix == maxsize should raise") | ||
| 639 | ok = pcall(data.receive, data, "*l", string.rep("x", 11), 10) | ||
| 640 | assert(not ok, "A5 failed: #prefix > maxsize should raise") | ||
| 641 | ok = pcall(data.receive, data, 100, nil, 50) | ||
| 642 | assert(not ok, "A6 failed: wanted > maxsize should raise") | ||
| 643 | ok = pcall(data.receive, data, "*l", nil, math.huge) | ||
| 644 | assert(not ok, "A7 failed: maxsize=math.huge should raise (size_t overflow)") | ||
| 645 | ok = pcall(data.receive, data, math.huge) | ||
| 646 | assert(not ok, "A8 failed: wanted=math.huge should raise (size_t overflow)") | ||
| 647 | data:settimeout(0.1) | ||
| 648 | ok = pcall(data.receive, data, 50, nil, 100) | ||
| 649 | assert(ok, "A9 failed: wanted <= maxsize should not raise") | ||
| 650 | data:settimeout(-1) | ||
| 651 | remote [[ data:send('intact\n') ]] | ||
| 652 | local line, err = data:receive("*l", nil, 100) | ||
| 653 | assert(line == "intact", | ||
| 654 | "A10 failed: socket touched by a failed argcheck (err=" .. | ||
| 655 | tostring(err) .. ")") | ||
| 656 | pass("ok") | ||
| 657 | |||
| 658 | -- Group B: *l boundary | ||
| 659 | reconnect() | ||
| 660 | printf("*l boundary: ") | ||
| 661 | remote [[ data:send('hello\n') ]] | ||
| 662 | local d, e, p = data:receive("*l", nil, 100) | ||
| 663 | assert(d == "hello" and e == nil, "B1 failed") | ||
| 664 | |||
| 665 | remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 100)) | ||
| 666 | d, e = data:receive("*l", nil, 100) | ||
| 667 | assert(d == string.rep("a", 100) and e == nil, | ||
| 668 | "B2 failed: exact-budget line should succeed") | ||
| 669 | |||
| 670 | remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 101)) | ||
| 671 | d, e, p = data:receive("*l", nil, 100) | ||
| 672 | assert(d == nil and e == "oversized" and p == string.rep("a", 100), | ||
| 673 | "B3 failed: one-over-budget should be oversized") | ||
| 674 | d, e = data:receive("*l", nil, 100) | ||
| 675 | assert(d == "a" and e == nil, | ||
| 676 | "B3 failed: leftover byte and terminator should still be there") | ||
| 677 | |||
| 678 | remote(string.format([[data:send(string.rep('\r',%d) .. string.rep('a',%d) .. '\n')]], 50, 100)) | ||
| 679 | d, e = data:receive("*l", nil, 100) | ||
| 680 | assert(d == string.rep("a", 100) and e == nil, "B4 failed: CRs must not count") | ||
| 681 | |||
| 682 | remote(string.format([[data:send(string.rep('a',%d) .. '\r\n')]], 100)) | ||
| 683 | d, e = data:receive("*l", nil, 100) | ||
| 684 | assert(d == string.rep("a", 100) and e == nil, "B5 failed: CRLF at boundary") | ||
| 685 | pass("ok") | ||
| 686 | |||
| 687 | remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 101)) | ||
| 688 | d, e, p = data:receive("*l", nil, 100) | ||
| 689 | assert(d == nil and e == "oversized" and p == string.rep("a", 100), | ||
| 690 | "B6 failed: one-over-budget should be oversized") | ||
| 691 | d, e = data:receive("*l", p, 150) -- increase limit, try again with prefix | ||
| 692 | assert(d == string.rep("a", 101) and e == nil, | ||
| 693 | "B6 failed: leftover byte and terminator should still be there") | ||
| 694 | |||
| 695 | -- Group C: *l and I1 (timeout/close at the cap) | ||
| 696 | reconnect() | ||
| 697 | printf("I1 (timeout/closed at the cap): ") | ||
| 698 | remote(string.format([[data:send(string.rep('a',%d))]], 50)) | ||
| 699 | data:settimeout(0.5) | ||
| 700 | d, e, p = data:receive("*l", nil, 100) | ||
| 701 | assert(d == nil and e == "timeout", "C1 failed: expected timeout below cap") | ||
| 702 | assert(#p < 100, "C1 failed: timeout partial must be strictly < maxsize") | ||
| 703 | ok = pcall(data.receive, data, "*l", p, 100) | ||
| 704 | assert(ok, "C1 failed: retry with prefix=partial, same maxsize must not raise") | ||
| 705 | |||
| 706 | reconnect() | ||
| 707 | remote(string.format([[data:send(string.rep('a',%d))]], 100)) | ||
| 708 | data:settimeout(0.5) | ||
| 709 | d, e, p = data:receive("*l", nil, 100) | ||
| 710 | assert(e == "oversized" and #p == 100, | ||
| 711 | "C2 failed: exactly-at-cap timeout must be oversized, got " .. tostring(e)) | ||
| 712 | |||
| 713 | reconnect() | ||
| 714 | remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 100)) | ||
| 715 | d, e, p = data:receive("*l", nil, 100) | ||
| 716 | assert(e == "oversized" and #p == 100, | ||
| 717 | "C3 failed: exactly-at-cap close must be oversized, got " .. tostring(e)) | ||
| 718 | pass("ok") | ||
| 719 | |||
| 720 | -- Group D: drain idiom | ||
| 721 | -- 5030 (not a multiple of the 100 cap) so the boundary of the 50th | ||
| 722 | -- oversized chunk doesn't land exactly on the '\n': when it does, the | ||
| 723 | -- terminator check wins over the cap check (by design -- see B2/I1) and | ||
| 724 | -- the would-be 50th oversized chunk instead succeeds outright. | ||
| 725 | reconnect() | ||
| 726 | printf("drain idiom: ") | ||
| 727 | remote(string.format([[data:send(string.rep('x',%d) .. '\n' .. 'next\n')]], 5030)) | ||
| 728 | local iterations = 0 | ||
| 729 | repeat | ||
| 730 | d, e, p = data:receive("*l", "", 100) | ||
| 731 | if e == "oversized" then | ||
| 732 | assert(#p == 100, "D1 failed: oversized partial length " .. #p) | ||
| 733 | iterations = iterations + 1 | ||
| 734 | end | ||
| 735 | until e ~= "oversized" | ||
| 736 | assert(iterations == 50, | ||
| 737 | "D1 failed: expected 50 oversized iterations, got " .. iterations) | ||
| 738 | assert(d == string.rep("x", 30) and e == nil, "D1 failed: final call should succeed") | ||
| 739 | local nextline = data:receive("*l") | ||
| 740 | assert(nextline == "next", "D1 failed: stream misaligned, got " .. tostring(nextline)) | ||
| 741 | pass("ok") | ||
| 742 | |||
| 743 | -- Group E: *a | ||
| 744 | reconnect() | ||
| 745 | printf("*a boundary: ") | ||
| 746 | remote [[ data:send('abc') data:close() data = nil ]] | ||
| 747 | d, e = data:receive("*a", nil, 100) | ||
| 748 | assert(d == "abc" and e == nil, "E1 failed") | ||
| 749 | |||
| 750 | reconnect() | ||
| 751 | remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 100)) | ||
| 752 | d, e = data:receive("*a", nil, 100) | ||
| 753 | assert(d == string.rep("a", 100) and e == nil, | ||
| 754 | "E2 failed: completion should beat the cap") | ||
| 755 | |||
| 756 | reconnect() | ||
| 757 | remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 250)) | ||
| 758 | d, e, p = data:receive("*a", "", 100) | ||
| 759 | assert(e == "oversized" and #p == 100, "E3a failed: first chunk") | ||
| 760 | d, e, p = data:receive("*a", "", 100) | ||
| 761 | assert(e == "oversized" and #p == 100, "E3a failed: second chunk") | ||
| 762 | d, e = data:receive("*a", "", 100) | ||
| 763 | assert(d == string.rep("a", 50) and e == nil, "E3a failed: final chunk") | ||
| 764 | |||
| 765 | reconnect() | ||
| 766 | remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 250)) | ||
| 767 | d, e, p = data:receive("*a", "", 100) | ||
| 768 | assert(e == "oversized" and #p == 100, "E3b failed: first chunk") | ||
| 769 | d, e, p = data:receive("*a", p, 200) -- increase maxsize, try again with prefix | ||
| 770 | assert(e == "oversized" and #p == 200, "E3b failed: second chunk") | ||
| 771 | d, e = data:receive("*a", p, 300) -- increase maxsize, try again with prefix | ||
| 772 | assert(d == string.rep("a", 250) and e == nil, "E3b failed: final chunk") | ||
| 773 | |||
| 774 | reconnect() | ||
| 775 | remote(string.format([[data:send(string.rep('a',%d))]], 100)) | ||
| 776 | data:settimeout(0.3) | ||
| 777 | d, e, p = data:receive("*a", nil, 100) | ||
| 778 | assert(e == "oversized" and #p == 100, | ||
| 779 | "E4 failed: expected oversized not timeout, got " .. tostring(e)) | ||
| 780 | |||
| 781 | reconnect() | ||
| 782 | remote(string.format([[data:send(string.rep('a',%d))]], 50)) | ||
| 783 | data:settimeout(0.3) | ||
| 784 | d, e, p = data:receive("*a", nil, 100) | ||
| 785 | assert(e == "timeout" and #p < 100, "E5 failed") | ||
| 786 | pass("ok") | ||
| 787 | |||
| 788 | -- Group F: numeric pattern (recvraw untouched) | ||
| 789 | reconnect() | ||
| 790 | printf("numeric pattern (recvraw unchanged): ") | ||
| 791 | remote(string.format([[data:send(string.rep('a',%d))]], 50)) | ||
| 792 | d, e = data:receive(50, nil, 100) | ||
| 793 | assert(d == string.rep("a", 50) and e == nil, "F1 failed") | ||
| 794 | |||
| 795 | reconnect() | ||
| 796 | remote(string.format([[data:send(string.rep('a',%d))]], 25)) | ||
| 797 | d, e = data:receive(50, string.rep("p", 25), 100) | ||
| 798 | assert(e == nil and #d == 50, "F2 failed") | ||
| 799 | pass("ok") | ||
| 800 | |||
| 801 | -- Group G: no-maxsize regression snapshot | ||
| 802 | reconnect() | ||
| 803 | printf("no-maxsize regression: ") | ||
| 804 | remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 5000)) | ||
| 805 | d, e = data:receive("*l") | ||
| 806 | assert(d == string.rep("a", 5000) and e == nil, "G2 failed: plain *l") | ||
| 807 | remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 5000)) | ||
| 808 | d, e = data:receive("*l", "") | ||
| 809 | assert(d == string.rep("a", 5000) and e == nil, "G2 failed: *l with prefix") | ||
| 810 | pass("ok") | ||
| 811 | end | ||
| 812 | |||
| 813 | ------------------------------------------------------------------------ | ||
| 622 | test("method registration") | 814 | test("method registration") |
| 623 | 815 | ||
| 624 | local tcp_methods = { | 816 | local tcp_methods = { |
| @@ -796,6 +988,9 @@ test_blockingtimeoutreceive(800091, 2, 3) | |||
| 796 | test_blockingtimeoutreceive(800091, 3, 2) | 988 | test_blockingtimeoutreceive(800091, 3, 2) |
| 797 | test_blockingtimeoutreceive(800091, 3, 1) | 989 | test_blockingtimeoutreceive(800091, 3, 1) |
| 798 | 990 | ||
| 991 | test("receive maxsize") | ||
| 992 | test_maxsize() | ||
| 993 | |||
| 799 | test("shutting server down") | 994 | test("shutting server down") |
| 800 | reconnect() | 995 | reconnect() |
| 801 | remote("os.exit()") | 996 | remote("os.exit()") |
diff --git a/test/utestclnt.lua b/test/utestclnt.lua index 7f10643..395260c 100644 --- a/test/utestclnt.lua +++ b/test/utestclnt.lua | |||
| @@ -511,6 +511,48 @@ remote(string.format([[ | |||
| 511 | end | 511 | end |
| 512 | 512 | ||
| 513 | ------------------------------------------------------------------------ | 513 | ------------------------------------------------------------------------ |
| 514 | function test_maxsize() | ||
| 515 | -- A4: #prefix == maxsize raises (mirrors testclnt.lua group A) | ||
| 516 | reconnect() | ||
| 517 | pass("argument errors") | ||
| 518 | -- bounds the pre-implementation call (arg 4 silently dropped, so this | ||
| 519 | -- becomes a real blocking read on an idle socket) so a meaningful | ||
| 520 | -- failure doesn't hang the suite | ||
| 521 | data:settimeout(0.2) | ||
| 522 | local ok = pcall(data.receive, data, "*l", string.rep("x", 10), 10) | ||
| 523 | assert(not ok, "A4 failed: #prefix == maxsize should raise") | ||
| 524 | data:settimeout(-1) | ||
| 525 | |||
| 526 | -- B2/B3: *l boundary (mirrors testclnt.lua group B) | ||
| 527 | pass("*l boundary") | ||
| 528 | remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 100)) | ||
| 529 | local d, e, p = data:receive("*l", nil, 100) | ||
| 530 | assert(d == string.rep("a", 100) and e == nil, "B2 failed") | ||
| 531 | |||
| 532 | remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 101)) | ||
| 533 | d, e, p = data:receive("*l", nil, 100) | ||
| 534 | assert(d == nil and e == "oversized" and p == string.rep("a", 100), "B3 failed") | ||
| 535 | d, e = data:receive("*l", nil, 100) | ||
| 536 | assert(d == "a" and e == nil, "B3 failed: leftover byte") | ||
| 537 | |||
| 538 | -- C2: timeout exactly at the cap is oversized, not timeout (I1) | ||
| 539 | reconnect() | ||
| 540 | remote(string.format([[data:send(string.rep('a',%d))]], 100)) | ||
| 541 | data:settimeout(0.5) | ||
| 542 | d, e, p = data:receive("*l", nil, 100) | ||
| 543 | assert(e == "oversized" and #p == 100, | ||
| 544 | "C2 failed: expected oversized, got " .. tostring(e)) | ||
| 545 | |||
| 546 | -- E2: completion beats the cap (I2) | ||
| 547 | reconnect() | ||
| 548 | remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 100)) | ||
| 549 | d, e = data:receive("*a", nil, 100) | ||
| 550 | assert(d == string.rep("a", 100) and e == nil, | ||
| 551 | "E2 failed: completion should beat the cap") | ||
| 552 | pass("ok") | ||
| 553 | end | ||
| 554 | |||
| 555 | ------------------------------------------------------------------------ | ||
| 514 | 556 | ||
| 515 | test("method registration") | 557 | test("method registration") |
| 516 | test_methods(socket.unix(), { | 558 | test_methods(socket.unix(), { |
| @@ -641,4 +683,7 @@ test_blockingtimeoutreceive(800091, 2, 3) | |||
| 641 | test_blockingtimeoutreceive(800091, 3, 2) | 683 | test_blockingtimeoutreceive(800091, 3, 2) |
| 642 | test_blockingtimeoutreceive(800091, 3, 1) | 684 | test_blockingtimeoutreceive(800091, 3, 1) |
| 643 | 685 | ||
| 686 | test("receive maxsize") | ||
| 687 | test_maxsize() | ||
| 688 | |||
| 644 | test(string.format("done in %.2fs", socket.gettime() - start)) | 689 | test(string.format("done in %.2fs", socket.gettime() - start)) |
