Browse Source

Merge branch '4.next' into 5.x

Mark Story 3 years ago
parent
commit
60889efd6e

+ 2 - 1
src/Console/ConsoleInputArgument.php

@@ -96,7 +96,8 @@ class ConsoleInputArgument
      */
     public function isEqualTo(ConsoleInputArgument $argument): bool
     {
-        return $this->usage() === $argument->usage();
+        return $this->name() === $argument->name() &&
+            $this->usage() === $argument->usage();
     }
 
     /**

+ 3 - 1
src/Database/Schema/MysqlSchemaDialect.php

@@ -231,7 +231,9 @@ class MysqlSchemaDialect extends SchemaDialect
             $name = $type = TableSchema::CONSTRAINT_PRIMARY;
         }
 
-        $columns[] = $row['Column_name'];
+        if (!empty($row['Column_name'])) {
+            $columns[] = $row['Column_name'];
+        }
 
         if ($row['Index_type'] === 'FULLTEXT') {
             $type = TableSchema::INDEX_FULLTEXT;

+ 8 - 0
src/Error/Renderer/WebExceptionRenderer.php

@@ -230,10 +230,18 @@ class WebExceptionRenderer implements ExceptionRendererInterface
         }
         $response = $response->withStatus($code);
 
+        $exceptions = [$exception];
+        $previous = $exception->getPrevious();
+        while ($previous != null) {
+            $exceptions[] = $previous;
+            $previous = $previous->getPrevious();
+        }
+
         $viewVars = [
             'message' => $message,
             'url' => h($url),
             'error' => $exception,
+            'exceptions' => $exceptions,
             'code' => $code,
         ];
         $serialize = ['message', 'url', 'code'];

+ 2 - 0
src/ORM/Table.php

@@ -52,6 +52,8 @@ use Cake\Validation\ValidatorAwareTrait;
 use Closure;
 use Exception;
 use InvalidArgumentException;
+use ReflectionMethod;
+use RuntimeException;
 
 /**
  * Represents a single database table.

+ 52 - 46
templates/element/exception_stack_trace.php

@@ -17,58 +17,64 @@
  */
 use Cake\Error\Debugger;
 
-foreach ($trace as $i => $stack):
-    $excerpt = $params = [];
+foreach ($exceptions as $level => $exc):
+    $stackTrace = Debugger::formatTrace($exc->getTrace(), [
+        'format' => 'array',
+        'args' => true,
+    ]);
+    foreach ($stackTrace as $i => $stack):
+        $excerpt = $params = [];
 
-    $line = null;
-    if (isset($stack['file'], $stack['line']) && is_numeric($stack['line'])):
-        $line = $stack['line'];
-        $excerpt = Debugger::excerpt($stack['file'], $line, 4);
-    endif;
-
-    if (isset($stack['file'])):
-        $file = $stack['file'];
-    else:
-        $file = '[internal function]';
-    endif;
+        $line = null;
+        if (isset($stack['file'], $stack['line']) && is_numeric($stack['line'])):
+            $line = $stack['line'];
+            $excerpt = Debugger::excerpt($stack['file'], $line, 4);
+        endif;
 
-    if (isset($stack['function'])):
-        if (!empty($stack['args'])):
-            foreach ((array)$stack['args'] as $arg):
-                $params[] = Debugger::exportVar($arg, 4);
-            endforeach;
+        if (isset($stack['file'])):
+            $file = $stack['file'];
         else:
-            $params[] = 'No arguments';
+            $file = '[internal function]';
         endif;
-    endif;
-?>
-    <div id="stack-frame-<?= $i ?>" style="display:<?= $i === 0 ? 'block' : 'none'; ?>;" class="stack-details">
-        <div class="stack-frame-header">
-            <span class="stack-frame-file">
-                <?php if ($line !== null): ?>
-                    <?= $this->Html->link(Debugger::trimPath($file), Debugger::editorUrl($file, $line)); ?>
-                <?php else: ?>
-                    <?= h(Debugger::trimPath($file)); ?>
-                <?php endif; ?>
-            </span>
-            <a href="#" class="toggle-link stack-frame-args" data-target="stack-args-<?= $i ?>">Toggle Arguments</a>
-        </div>
 
-        <table class="code-excerpt" cellspacing="0" cellpadding="0">
-        <?php $lineno = isset($stack['line']) && is_numeric($stack['line']) ? $stack['line'] - 4 : 0 ?>
-        <?php foreach ($excerpt as $l => $line): ?>
-            <tr>
-                <td class="excerpt-number" data-number="<?= $lineno + $l ?>"></td>
-                <td class="excerpt-line"><?= $line ?></td>
-            </tr>
-        <?php endforeach; ?>
-        </table>
+        if (isset($stack['function'])):
+            if (!empty($stack['args'])):
+                foreach ((array)$stack['args'] as $arg):
+                    $params[] = Debugger::exportVar($arg, 4);
+                endforeach;
+            else:
+                $params[] = 'No arguments';
+            endif;
+        endif;
+    ?>
+        <div id="stack-frame-<?= $i ?>" style="display:<?= $i === 0 ? 'block' : 'none'; ?>;" class="stack-details">
+            <div class="stack-frame-header">
+                <span class="stack-frame-file">
+                    <?php if ($line !== null): ?>
+                        <?= $this->Html->link(Debugger::trimPath($file), Debugger::editorUrl($file, $line)); ?>
+                    <?php else: ?>
+                        <?= h(Debugger::trimPath($file)); ?>
+                    <?php endif; ?>
+                </span>
+                <a href="#" class="toggle-link stack-frame-args" data-target="stack-args-<?= $i ?>">Toggle Arguments</a>
+            </div>
 
-        <div id="stack-args-<?= $i ?>" class="cake-debug" style="display: none;">
-            <h4>Arguments</h4>
-            <?php foreach ($params as $param): ?>
-                <div class="cake-debug"><?= $param ?></div>
+            <table class="code-excerpt" cellspacing="0" cellpadding="0">
+            <?php $lineno = isset($stack['line']) && is_numeric($stack['line']) ? $stack['line'] - 4 : 0 ?>
+            <?php foreach ($excerpt as $l => $line): ?>
+                <tr>
+                    <td class="excerpt-number" data-number="<?= $lineno + $l ?>"></td>
+                    <td class="excerpt-line"><?= $line ?></td>
+                </tr>
             <?php endforeach; ?>
+            </table>
+
+            <div id="stack-args-<?= $i ?>" class="cake-debug" style="display: none;">
+                <h4>Arguments</h4>
+                <?php foreach ($params as $param): ?>
+                    <div class="cake-debug"><?= $param ?></div>
+                <?php endforeach; ?>
+            </div>
         </div>
-    </div>
+    <?php endforeach; ?>
 <?php endforeach; ?>

+ 31 - 22
templates/element/exception_stack_trace_nav.php

@@ -12,32 +12,41 @@
  * @since         3.0.0
  * @license       https://opensource.org/licenses/mit-license.php MIT License
  * @var array $trace
+ * @var \Throwable $error
+ * @var array<\Throwable> $exceptions
  */
 use Cake\Error\Debugger;
 ?>
 <a href="#" class="toggle-link toggle-vendor-frames">Toggle Vendor Stack Frames</a>
 
-<ul class="stack-trace">
-<?php foreach ($trace as $i => $stack): ?>
-    <?php
-    $class = isset($stack['file']) && !str_contains($stack['file'], APP) ? 'vendor-frame' : 'app-frame';
-    $class .= $i == 0 ? ' active' : '';
-    ?>
-    <li class="stack-frame <?= $class ?>">
-        <a href="#" data-target="stack-frame-<?= $i ?>">
-            <?php if (isset($stack['class'])): ?>
-                <span class="stack-function"><?= h($stack['class'] . $stack['type'] . $stack['function']) ?></span>
-            <?php elseif (isset($stack['function'])): ?>
-                <span class="stack-function"><?= h($stack['function']) ?></span>
-            <?php endif; ?>
-            <span class="stack-file">
-            <?php if (isset($stack['file'], $stack['line'])): ?>
-                <?= h(Debugger::trimPath($stack['file'])) ?>:<?= $stack['line'] ?>
-            <?php else: ?>
-                [internal function]
-            <?php endif ?>
-            </span>
-        </a>
-    </li>
+<?php foreach ($exceptions as $level => $exc): ?>
+    <?php if ($level > 0): ?>
+        <li class="stack-previous">
+            <span class="stack-function">Caused by</span> <?= h(get_class($exc)) ?>
+        </li>
+    <?php endif; ?>
+    <?php $stackTrace = Debugger::formatTrace($exc->getTrace(), ['format' => 'array']); ?>
+    <?php foreach ($stackTrace as $i => $stack): ?>
+        <?php
+        $class = isset($stack['file']) && str_contains($stack['file'], APP) ? 'vendor-frame' : 'app-frame';
+        $class .= $i == 0 ? ' active' : '';
+        ?>
+        <li class="stack-frame <?= $class ?>">
+            <a href="#" data-target="stack-frame-<?= $i ?>">
+                <?php if (isset($stack['class'])): ?>
+                    <span class="stack-function"><?= h($stack['class'] . $stack['type'] . $stack['function']) ?></span>
+                <?php elseif (isset($stack['function'])): ?>
+                    <span class="stack-function"><?= h($stack['function']) ?></span>
+                <?php endif; ?>
+                <span class="stack-file">
+                <?php if (isset($stack['file'], $stack['line'])): ?>
+                    <?= h(Debugger::trimPath($stack['file'])) ?>:<?= $stack['line'] ?>
+                <?php else: ?>
+                    [internal function]
+                <?php endif ?>
+                </span>
+            </a>
+        </li>
+    <?php endforeach; ?>
 <?php endforeach; ?>
 </ul>

+ 3 - 0
templates/layout/dev_error.php

@@ -146,6 +146,9 @@ use Cake\Error\Debugger;
         margin: 0;
         padding: 0;
     }
+    .stack-previous {
+        margin: 24px 0 12px 8px;
+    }
     .stack-frame {
         background: #e5e5e5;
         padding: 10px;

+ 16 - 1
tests/TestCase/Console/ConsoleOptionParserTest.php

@@ -573,7 +573,7 @@ class ConsoleOptionParserTest extends TestCase
     /**
      * test positional argument parsing.
      */
-    public function testPositionalArgument(): void
+    public function testAddArgument(): void
     {
         $parser = new ConsoleOptionParser('test', false);
         $result = $parser->addArgument('name', ['help' => 'An argument']);
@@ -581,6 +581,21 @@ class ConsoleOptionParserTest extends TestCase
     }
 
     /**
+     * Add arguments that were once considered the same
+     */
+    public function testAddArgumentDuplicate(): void
+    {
+        $parser = new ConsoleOptionParser('test', false);
+        $parser
+            ->addArgument('first', ['help' => 'An argument', 'choices' => [1, 2]])
+            ->addArgument('second', ['help' => 'An argument', 'choices' => [1, 2]]);
+        $args = $parser->arguments();
+        $this->assertCount(2, $args);
+        $this->assertEquals('first', $args[0]->name());
+        $this->assertEquals('second', $args[1]->name());
+    }
+
+    /**
      * test addOption with an object.
      */
     public function testAddArgumentObject(): void

+ 33 - 0
tests/TestCase/Database/Schema/MysqlSchemaTest.php

@@ -24,6 +24,7 @@ use Cake\Database\Schema\MysqlSchemaDialect;
 use Cake\Database\Schema\TableSchema;
 use Cake\Datasource\ConnectionManager;
 use Cake\TestSuite\TestCase;
+use Exception;
 use PDO;
 
 /**
@@ -489,6 +490,38 @@ SQL;
     }
 
     /**
+     * Test describing a table with conditional constraints
+     */
+    public function testDescribeTableConditionalConstraint(): void
+    {
+        $connection = ConnectionManager::get('test');
+        $connection->execute('DROP TABLE IF EXISTS conditional_constraint');
+        $table = <<<SQL
+CREATE TABLE conditional_constraint (
+    id INT AUTO_INCREMENT PRIMARY KEY,
+    config_id INT UNSIGNED NOT NULL,
+    status ENUM ('new', 'processing', 'completed', 'failed') DEFAULT 'new' NOT NULL,
+    CONSTRAINT unique_index UNIQUE (config_id, (
+        (CASE WHEN ((`status` = "new") OR (`status` = "processing")) THEN `status` END)
+    ))
+);
+SQL;
+        try {
+            $connection->execute($table);
+        } catch (Exception $e) {
+            $this->markTestSkipped('Could not create table with conditional constraint');
+        }
+        $schema = new SchemaCollection($connection);
+        $result = $schema->describe('conditional_constraint');
+        $connection->execute('DROP TABLE IF EXISTS conditional_constraint');
+
+        $constraint = $result->getConstraint('unique_index');
+        $this->assertNotEmpty($constraint);
+        $this->assertEquals('unique', $constraint['type']);
+        $this->assertEquals(['config_id'], $constraint['columns']);
+    }
+
+    /**
      * Test describing a table creates options
      */
     public function testDescribeTableOptions(): void

+ 2 - 1
tests/TestCase/Error/ExceptionTrapTest.php

@@ -205,7 +205,8 @@ class ExceptionTrapTest extends TestCase
         $this->assertStringContainsString('<!DOCTYPE', $out);
         $this->assertStringContainsString('<html', $out);
         $this->assertStringContainsString('nope', $out);
-        $this->assertStringContainsString('ExceptionTrapTest', $out);
+        $this->assertStringContainsString('class="stack-frame-header"', $out);
+        $this->assertStringContainsString('Toggle Arguments', $out);
     }
 
     public function testLogException()

+ 2 - 1
tests/TestCase/View/ViewTest.php

@@ -1063,11 +1063,12 @@ class ViewTest extends TestCase
     {
         $error = new MyPDOException();
         $error->queryString = 'this is sql string';
+        $exceptions = [$error];
         $message = 'it works';
         $trace = $error->getTrace();
 
         $View = $this->PostsController->createView(TestView::class);
-        $View->set(compact('error', 'message', 'trace'));
+        $View->set(compact('error', 'exceptions', 'message', 'trace'));
         $View->setTemplatePath('Error');
 
         $result = $View->render('pdo_error', 'error');